Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/app/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function AppShell({
}) {
const t = locales[locale];
const [collapsed, setCollapsed] = useState(
() => localStorage.getItem(sidebarStorageKey) === "true",
() => sessionStorage.getItem(sidebarStorageKey) === "true",
);
const pageDescriptions = localeMessages<Record<Page, string>>(
locale,
Expand Down Expand Up @@ -106,7 +106,7 @@ export function AppShell({
);
const activeRunLabel = activeRunCount > 99 ? "99+" : String(activeRunCount);
useEffect(
() => localStorage.setItem(sidebarStorageKey, String(collapsed)),
() => sessionStorage.setItem(sidebarStorageKey, String(collapsed)),
[collapsed],
);
useEffect(() => {
Expand Down
110 changes: 92 additions & 18 deletions frontend/src/components/chat-assistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Send,
Settings,
SquareTerminal,
Trash2,
X,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
Expand Down Expand Up @@ -69,9 +70,16 @@ type ChatAssistantCopy = {
message: string;
placeholder: string;
send: string;
clearHistory: string;
clearHistoryPrompt: string;
cancel: string;
confirmClearHistory: string;
};
const positionKey = "orbit.chat.position";
const windowPositionKey = "orbit.chat.window.position";
const messagesKey = "orbit.chat.messages.v1";
const openKey = "orbit.chat.open";
const maxStoredMessages = 100;
const defaultToolSettings: ToolSettings = {
workspace_root: "",
file_read_enabled: true,
Expand All @@ -94,7 +102,7 @@ const normalizedToolSettings = (
});

const initialPosition = (): Position => {
const saved = localStorage.getItem(positionKey);
const saved = sessionStorage.getItem(positionKey);
if (saved) {
try {
const position = JSON.parse(saved) as Position;
Expand All @@ -120,7 +128,7 @@ const initialPosition = (): Position => {
};

const initialWindowPosition = (): Position | null => {
const saved = localStorage.getItem(windowPositionKey);
const saved = sessionStorage.getItem(windowPositionKey);
if (saved) {
try {
const position = JSON.parse(saved) as Position;
Expand All @@ -133,14 +141,33 @@ const initialWindowPosition = (): Position | null => {
return null;
};

const initialOpen = () => sessionStorage.getItem(openKey) === "true";

const initialMessages = (): Message[] => {
try {
const saved = JSON.parse(sessionStorage.getItem(messagesKey) ?? "[]");
if (!Array.isArray(saved)) return [];
return saved
.filter(
(message): message is Message =>
message &&
(message.role === "user" || message.role === "assistant") &&
typeof message.content === "string",
)
.slice(-maxStoredMessages);
} catch {
return [];
}
};

export function ChatAssistant() {
const { sessionId, handleCommand } = useAssistantUiBridge();
const copy = localeMessages<ChatAssistantCopy>(
resolveLocale(localStorage.getItem("orbit.locale")),
"chatAssistant",
);
const [open, setOpen] = useState(false),
[messages, setMessages] = useState<Message[]>([]),
const [open, setOpen] = useState(initialOpen),
[messages, setMessages] = useState<Message[]>(initialMessages),
[draft, setDraft] = useState(""),
[sending, setSending] = useState(false),
[activityPhase, setActivityPhase] = useState<"thinking" | "working">("thinking"),
Expand All @@ -157,6 +184,7 @@ export function ChatAssistant() {
}),
[terminalMode, setTerminalMode] = useState(false),
[terminalStarted, setTerminalStarted] = useState(false),
[clearConfirmationOpen, setClearConfirmationOpen] = useState(false),
[toolSettings, setToolSettings] =
useState<ToolSettings>(defaultToolSettings);
const drag = useRef<{
Expand All @@ -171,6 +199,13 @@ export function ChatAssistant() {
const messageList = useRef<HTMLDivElement>(null);
const chatWindow = useRef<HTMLElement>(null);
const hasUsedTool = useRef(false);
const clearHistory = () => {
setMessages([]);
sessionStorage.removeItem(messagesKey);
setClearConfirmationOpen(false);
};
const appendMessage = (message: Message) =>
setMessages((current) => [...current, message].slice(-maxStoredMessages));
useEffect(() => {
messageList.current?.scrollTo({ top: messageList.current.scrollHeight });
}, [messages, sending, activityLines]);
Expand All @@ -183,12 +218,23 @@ export function ChatAssistant() {
return () => window.clearInterval(interval);
}, [sending]);
useEffect(() => {
localStorage.setItem(positionKey, JSON.stringify(position));
sessionStorage.setItem(positionKey, JSON.stringify(position));
}, [position]);
useEffect(() => {
if (windowPosition)
localStorage.setItem(windowPositionKey, JSON.stringify(windowPosition));
sessionStorage.setItem(windowPositionKey, JSON.stringify(windowPosition));
}, [windowPosition]);
useEffect(() => {
sessionStorage.setItem(openKey, String(open));
}, [open]);
useEffect(() => {
try {
if (messages.length) sessionStorage.setItem(messagesKey, JSON.stringify(messages));
else sessionStorage.removeItem(messagesKey);
} catch {
// A full or unavailable browser store must not prevent chatting.
}
}, [messages]);
useEffect(() => {
if (open)
api<ApplicationSettings>("/api/application-settings")
Expand Down Expand Up @@ -248,9 +294,14 @@ export function ChatAssistant() {
const send = async () => {
const content = draft.trim();
if (!content || sending) return;
if (content.toLowerCase() === "/clear") {
setDraft("");
setClearConfirmationOpen(true);
return;
}
const history = messages.slice(-12);
setDraft("");
setMessages((current) => [...current, { role: "user", content }]);
appendMessage({ role: "user", content });
setSending(true);
setActivityPhase("thinking");
setActivityLines([]);
Expand Down Expand Up @@ -312,18 +363,12 @@ export function ChatAssistant() {
if (done) break;
}
if (!answer) throw new Error(copy.requestFailed);
setMessages((current) => [
...current,
{ role: "assistant", content: answer },
]);
appendMessage({ role: "assistant", content: answer });
} catch (error) {
setMessages((current) => [
...current,
{
role: "assistant",
content: error instanceof Error ? error.message : copy.requestFailed,
},
]);
appendMessage({
role: "assistant",
content: error instanceof Error ? error.message : copy.requestFailed,
});
} finally {
setSending(false);
}
Expand Down Expand Up @@ -447,6 +492,15 @@ export function ChatAssistant() {
>
<Settings size={17} />
</button>
<button
className="ghost"
aria-label={copy.clearHistory}
disabled={!messages.length || sending}
onPointerDown={(event) => event.stopPropagation()}
onClick={() => setClearConfirmationOpen(true)}
>
<Trash2 size={17} />
</button>
<button
className="ghost"
aria-label={copy.collapse}
Expand All @@ -463,6 +517,26 @@ export function ChatAssistant() {
>
{toolSummary}
</div>
{clearConfirmationOpen && (
<section
className="chat-clear-confirmation"
role="alertdialog"
aria-label={copy.clearHistory}
>
<div>
<strong>{copy.clearHistory}</strong>
<p>{copy.clearHistoryPrompt}</p>
<footer>
<button className="ghost" onClick={() => setClearConfirmationOpen(false)}>
{copy.cancel}
</button>
<button className="reject" onClick={clearHistory}>
{copy.confirmClearHistory}
</button>
</footer>
</div>
</section>
)}
<div className="chat-messages" ref={messageList}>
{messages.length === 0 && (
<p className="chat-empty">{copy.empty}</p>
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/features/assets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -969,7 +969,7 @@ function PromptTemplateEditor({
? template.versions
: [{ version: template.version, content: template.content }];
const [draft, setDraft] = useState(() => {
const saved = localStorage.getItem(draftKey);
const saved = sessionStorage.getItem(draftKey);
return saved ? (JSON.parse(saved) as PromptTemplate) : { ...template };
}),
[selectedVersion, setSelectedVersion] = useState(draft.version),
Expand All @@ -982,8 +982,8 @@ function PromptTemplateEditor({
draft.content !== selectedContent ||
draft.id !== template.id;
useEffect(() => {
if (dirty) localStorage.setItem(draftKey, JSON.stringify(draft));
else localStorage.removeItem(draftKey);
if (dirty) sessionStorage.setItem(draftKey, JSON.stringify(draft));
else sessionStorage.removeItem(draftKey);
}, [draft, dirty, draftKey]);
const close = () => {
if (dirty) setPendingAction({ type: "close" });
Expand All @@ -994,7 +994,7 @@ function PromptTemplateEditor({
if (selected) {
setSelectedVersion(version);
setDraft({ ...template, content: selected.content, version });
localStorage.removeItem(draftKey);
sessionStorage.removeItem(draftKey);
}
};
const selectVersion = (version: number) => {
Expand All @@ -1003,7 +1003,7 @@ function PromptTemplateEditor({
};
const discard = () => {
if (!pendingAction) return;
localStorage.removeItem(draftKey);
sessionStorage.removeItem(draftKey);
if (pendingAction.type === "close") onClose();
else applyVersion(pendingAction.version);
setPendingAction(null);
Expand All @@ -1017,7 +1017,7 @@ function PromptTemplateEditor({
{ id: draft.id, name: draft.name, content: draft.content },
)
.then(async () => {
localStorage.removeItem(draftKey);
sessionStorage.removeItem(draftKey);
await onSaved();
onClose();
})
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/locales/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@
"usingUiInteract": "Interacting with the UI",
"message": "Message",
"placeholder": "Message Orbit…",
"send": "Send message"
"send": "Send message",
"clearHistory": "Clear chat history",
"clearHistoryPrompt": "Delete every message stored in this browser? This cannot be undone.",
"cancel": "Cancel",
"confirmClearHistory": "Clear history"
},
"appMeta": {
"title": "OpenOrbit",
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/locales/languages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@
"usingUiInteract": "UI を操作中",
"message": "メッセージ",
"placeholder": "Orbit にメッセージ…",
"send": "メッセージを送信"
"send": "メッセージを送信",
"clearHistory": "チャット履歴を削除",
"clearHistoryPrompt": "このブラウザーに保存されたすべての会話を削除しますか?この操作は元に戻せません。",
"cancel": "キャンセル",
"confirmClearHistory": "履歴を削除"
},
"appMeta": {
"title": "OpenOrbit",
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/locales/languages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@
"usingUiInteract": "UI 조작 중",
"message": "메시지",
"placeholder": "Orbit에 메시지 보내기…",
"send": "메시지 전송"
"send": "메시지 전송",
"clearHistory": "대화 기록 삭제",
"clearHistoryPrompt": "이 브라우저에 저장된 모든 대화를 삭제할까요? 이 작업은 되돌릴 수 없습니다.",
"cancel": "취소",
"confirmClearHistory": "기록 삭제"
},
"appMeta": {
"title": "OpenOrbit",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/services/build-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function preferredBuildId(
fallback: string,
): string {
try {
const saved = localStorage.getItem(storageKey(scope));
const saved = sessionStorage.getItem(storageKey(scope));
return saved && builds.some((build) => build.id === saved) ? saved : fallback;
} catch {
return fallback;
Expand All @@ -19,15 +19,15 @@ export function preferredBuildId(

export function savePreferredBuildId(scope: "improvements", buildId: string) {
try {
if (buildId) localStorage.setItem(storageKey(scope), buildId);
if (buildId) sessionStorage.setItem(storageKey(scope), buildId);
} catch {
// Storage can be unavailable in private or restricted browser contexts.
}
}

export function preferredRangeHours(scope: "improvements", fallback: number): number {
try {
const saved = Number(localStorage.getItem(rangeStorageKey(scope)));
const saved = Number(sessionStorage.getItem(rangeStorageKey(scope)));
return validRangeHours.has(saved) ? saved : fallback;
} catch {
return fallback;
Expand All @@ -36,7 +36,7 @@ export function preferredRangeHours(scope: "improvements", fallback: number): nu

export function savePreferredRangeHours(scope: "improvements", hours: number) {
try {
if (validRangeHours.has(hours)) localStorage.setItem(rangeStorageKey(scope), String(hours));
if (validRangeHours.has(hours)) sessionStorage.setItem(rangeStorageKey(scope), String(hours));
} catch {
// Storage can be unavailable in private or restricted browser contexts.
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/theme-overrides.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading