From c51f252ff2007f7dbb0abd9a6b0af37476bbca9d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 01:13:42 -0800 Subject: [PATCH] light mode (#4) * init components * use them * Fix select trigger styling in dark mode and add open state highlight Remove dark mode background bleed on select triggers and add accent background when popup is open. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- apps/desktop/scripts/smoke-test.mjs | 4 +- apps/renderer/components.json | 25 + apps/renderer/index.html | 2 +- apps/renderer/package.json | 5 + apps/renderer/src/App.tsx | 6 +- apps/renderer/src/components/ChatMarkdown.tsx | 2 +- apps/renderer/src/components/ChatView.tsx | 1015 +++++------------ apps/renderer/src/components/DiffPanel.tsx | 28 +- apps/renderer/src/components/Sidebar.tsx | 214 ++-- apps/renderer/src/components/ui/accordion.tsx | 67 ++ .../src/components/ui/alert-dialog.tsx | 167 +++ apps/renderer/src/components/ui/alert.tsx | 80 ++ .../src/components/ui/autocomplete.tsx | 308 +++++ apps/renderer/src/components/ui/avatar.tsx | 44 + apps/renderer/src/components/ui/badge.tsx | 58 + apps/renderer/src/components/ui/button.tsx | 71 ++ apps/renderer/src/components/ui/card.tsx | 242 ++++ .../src/components/ui/checkbox-group.tsx | 14 + apps/renderer/src/components/ui/checkbox.tsx | 58 + .../src/components/ui/collapsible.tsx | 43 + apps/renderer/src/components/ui/combobox.tsx | 422 +++++++ apps/renderer/src/components/ui/command.tsx | 262 +++++ apps/renderer/src/components/ui/dialog.tsx | 194 ++++ apps/renderer/src/components/ui/empty.tsx | 127 +++ apps/renderer/src/components/ui/field.tsx | 72 ++ apps/renderer/src/components/ui/fieldset.tsx | 27 + apps/renderer/src/components/ui/form.tsx | 15 + .../src/components/ui/input-group.tsx | 99 ++ apps/renderer/src/components/ui/input.tsx | 64 ++ apps/renderer/src/components/ui/kbd.tsx | 28 + apps/renderer/src/components/ui/label.tsx | 26 + apps/renderer/src/components/ui/menu.tsx | 308 +++++ apps/renderer/src/components/ui/popover.tsx | 102 ++ apps/renderer/src/components/ui/progress.tsx | 79 ++ .../src/components/ui/radio-group.tsx | 34 + .../src/components/ui/scroll-area.tsx | 62 + apps/renderer/src/components/ui/select.tsx | 187 +++ apps/renderer/src/components/ui/separator.tsx | 23 + apps/renderer/src/components/ui/sheet.tsx | 201 ++++ apps/renderer/src/components/ui/skeleton.tsx | 16 + apps/renderer/src/components/ui/spinner.tsx | 18 + apps/renderer/src/components/ui/switch.tsx | 25 + apps/renderer/src/components/ui/table.tsx | 126 ++ apps/renderer/src/components/ui/tabs.tsx | 85 ++ apps/renderer/src/components/ui/textarea.tsx | 49 + apps/renderer/src/components/ui/toast.tsx | 267 +++++ apps/renderer/src/hooks/useTheme.ts | 90 ++ apps/renderer/src/index.css | 224 +++- apps/renderer/src/lib/utils.ts | 6 + apps/renderer/tsconfig.json | 6 +- apps/renderer/vite.config.ts | 6 + bun.lock | 97 +- 52 files changed, 4877 insertions(+), 923 deletions(-) create mode 100644 apps/renderer/components.json create mode 100644 apps/renderer/src/components/ui/accordion.tsx create mode 100644 apps/renderer/src/components/ui/alert-dialog.tsx create mode 100644 apps/renderer/src/components/ui/alert.tsx create mode 100644 apps/renderer/src/components/ui/autocomplete.tsx create mode 100644 apps/renderer/src/components/ui/avatar.tsx create mode 100644 apps/renderer/src/components/ui/badge.tsx create mode 100644 apps/renderer/src/components/ui/button.tsx create mode 100644 apps/renderer/src/components/ui/card.tsx create mode 100644 apps/renderer/src/components/ui/checkbox-group.tsx create mode 100644 apps/renderer/src/components/ui/checkbox.tsx create mode 100644 apps/renderer/src/components/ui/collapsible.tsx create mode 100644 apps/renderer/src/components/ui/combobox.tsx create mode 100644 apps/renderer/src/components/ui/command.tsx create mode 100644 apps/renderer/src/components/ui/dialog.tsx create mode 100644 apps/renderer/src/components/ui/empty.tsx create mode 100644 apps/renderer/src/components/ui/field.tsx create mode 100644 apps/renderer/src/components/ui/fieldset.tsx create mode 100644 apps/renderer/src/components/ui/form.tsx create mode 100644 apps/renderer/src/components/ui/input-group.tsx create mode 100644 apps/renderer/src/components/ui/input.tsx create mode 100644 apps/renderer/src/components/ui/kbd.tsx create mode 100644 apps/renderer/src/components/ui/label.tsx create mode 100644 apps/renderer/src/components/ui/menu.tsx create mode 100644 apps/renderer/src/components/ui/popover.tsx create mode 100644 apps/renderer/src/components/ui/progress.tsx create mode 100644 apps/renderer/src/components/ui/radio-group.tsx create mode 100644 apps/renderer/src/components/ui/scroll-area.tsx create mode 100644 apps/renderer/src/components/ui/select.tsx create mode 100644 apps/renderer/src/components/ui/separator.tsx create mode 100644 apps/renderer/src/components/ui/sheet.tsx create mode 100644 apps/renderer/src/components/ui/skeleton.tsx create mode 100644 apps/renderer/src/components/ui/spinner.tsx create mode 100644 apps/renderer/src/components/ui/switch.tsx create mode 100644 apps/renderer/src/components/ui/table.tsx create mode 100644 apps/renderer/src/components/ui/tabs.tsx create mode 100644 apps/renderer/src/components/ui/textarea.tsx create mode 100644 apps/renderer/src/components/ui/toast.tsx create mode 100644 apps/renderer/src/hooks/useTheme.ts create mode 100644 apps/renderer/src/lib/utils.ts diff --git a/apps/desktop/scripts/smoke-test.mjs b/apps/desktop/scripts/smoke-test.mjs index 62884bcde3eed6134d9edfdb845671a2028b5a86..482f260591ef7064e6a71c2dd572de39947f9b37 100644 --- a/apps/desktop/scripts/smoke-test.mjs +++ b/apps/desktop/scripts/smoke-test.mjs @@ -13,7 +13,7 @@ * the first render). For production mode (no React DevTools message), we * instead check that no fatal errors appeared. */ -import { spawn, execSync } from "node:child_process"; +import { execSync, spawn } from "node:child_process"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -75,7 +75,7 @@ child.on("exit", () => { for (const f of failures) { console.error(` • ${f}`); } - console.error("\nFull output:\n" + output); + console.error(`\nFull output:\n${output}`); process.exit(1); } diff --git a/apps/renderer/components.json b/apps/renderer/components.json new file mode 100644 index 0000000000000000000000000000000000000000..8d7bf441ac8f58d7271472634227d4502d8fe851 --- /dev/null +++ b/apps/renderer/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "#/components", + "utils": "#/lib/utils", + "ui": "#/components/ui", + "lib": "#/lib", + "hooks": "#/hooks" + }, + "registries": { + "@coss": "https://coss.com/ui/r/{name}.json" + } +} diff --git a/apps/renderer/index.html b/apps/renderer/index.html index 7084d1a08b198f7188a00f59ff3d6328c62f9d3f..c2c8793a7c00635add4a75de3659a3449480ce04 100644 --- a/apps/renderer/index.html +++ b/apps/renderer/index.html @@ -5,7 +5,7 @@ Electron Todo - +
diff --git a/apps/renderer/package.json b/apps/renderer/package.json index b91d3d1dae74e61b449a2bf191f3475f41342506..8ab450ea8cc8fb5d340389c6a2ded4fc450e98b4 100644 --- a/apps/renderer/package.json +++ b/apps/renderer/package.json @@ -12,12 +12,17 @@ }, "dependencies": { "@acme/contracts": "workspace:*", + "@base-ui/react": "^1.1.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "highlight.js": "^11.11.1", + "lucide-react": "^0.563.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", "rehype-highlight": "^7.0.2", "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.4.0", "zod": "^3.24.1" }, "devDependencies": { diff --git a/apps/renderer/src/App.tsx b/apps/renderer/src/App.tsx index 252e1cd4455782b1abe2754a3a823eb4e26a8ce7..a525c7caebec9c3705211e7722daca99c8cecbd1 100644 --- a/apps/renderer/src/App.tsx +++ b/apps/renderer/src/App.tsx @@ -31,10 +31,10 @@ function Layout() { if (!api) { return ( -
+
-

+

Native bridge unavailable. Launch through Electron.

@@ -43,7 +43,7 @@ function Layout() { } return ( -
+
diff --git a/apps/renderer/src/components/ChatMarkdown.tsx b/apps/renderer/src/components/ChatMarkdown.tsx index 1d51a76851897ea6d488f956681013d31d2928bd..5104ddced5e66cfd9c8d77207f41406f36bdf69d 100644 --- a/apps/renderer/src/components/ChatMarkdown.tsx +++ b/apps/renderer/src/components/ChatMarkdown.tsx @@ -15,7 +15,7 @@ const markdownComponents: Components = { export default function ChatMarkdown({ text }: ChatMarkdownProps) { return ( -
+
| undefined { - if (!value || typeof value !== "object") return undefined; - return value as Record; -} - -function asString(value: unknown): string | undefined { - return typeof value === "string" ? value : undefined; -} - -function approvalDetail(event: ProviderEvent): string | undefined { - const payload = asRecord(event.payload); - const command = asString(payload?.command); - if (command) return command; - return asString(payload?.reason); -} - -function derivePendingApprovals( - events: ProviderEvent[], -): PendingApprovalCard[] { - const pending = new Map(); - const ordered = [...events].reverse(); - - for (const event of ordered) { - if ( - event.method === "session/closed" || - event.method === "session/exited" - ) { - pending.clear(); - continue; - } - - const requestId = - event.requestId ?? asString(asRecord(event.payload)?.requestId); - if (!requestId) continue; - - if ( - event.kind === "request" && - (event.requestKind === "command" || event.requestKind === "file-change") - ) { - const detail = approvalDetail(event); - pending.set(requestId, { - requestId, - requestKind: event.requestKind, - createdAt: event.createdAt, - ...(detail ? { detail } : {}), - }); - continue; - } - - if (event.method === "item/requestApproval/decision") { - pending.delete(requestId); - } - } - - return Array.from(pending.values()); + if (tone === "error") return "text-destructive/50"; + if (tone === "tool") return "text-muted-foreground"; + if (tone === "thinking") return "text-muted-foreground/70"; + return "text-muted-foreground/60"; } export default function ChatView() { @@ -123,46 +65,30 @@ export default function ChatView() { const [prompt, setPrompt] = useState(""); const [isSending, setIsSending] = useState(false); const [isConnecting, setIsConnecting] = useState(false); - const [isModelMenuOpen, setIsModelMenuOpen] = useState(false); - const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false); - const [lastEditor, setLastEditor] = useState(() => { - const stored = localStorage.getItem(LAST_EDITOR_KEY); - return EDITORS.some((e) => e.id === stored) - ? (stored as EditorId) - : EDITORS[0].id; - }); const [selectedEffort, setSelectedEffort] = useState(DEFAULT_REASONING); - const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); - const [respondingRequestIds, setRespondingRequestIds] = useState( - [], - ); const [nowTick, setNowTick] = useState(() => Date.now()); const messagesEndRef = useRef(null); - const textareaRef = useRef(null); - const modelMenuRef = useRef(null); - const editorMenuRef = useRef(null); const activeThread = state.threads.find((t) => t.id === state.activeThreadId); - const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); + const activeProject = state.projects.find( + (p) => p.id === activeThread?.projectId, + ); const selectedModel = resolveModelSlug( activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL, ); const phase = derivePhase(activeThread?.session ?? null); const isWorking = phase === "running" || isSending || isConnecting; + const activeTurnId = activeThread?.session?.activeTurnId; const nowIso = new Date(nowTick).toISOString(); const modelOptions = MODEL_OPTIONS; const workLogEntries = useMemo( - () => deriveWorkLogEntries(activeThread?.events ?? [], undefined), - [activeThread?.events], - ); - const pendingApprovals = useMemo( - () => derivePendingApprovals(activeThread?.events ?? []), - [activeThread?.events], + () => deriveWorkLogEntries(activeThread?.events ?? [], activeTurnId), + [activeThread?.events, activeTurnId], ); const assistantCompletionByItemId = useMemo(() => { const map = new Map(); - const ordered = [...(activeThread?.events ?? [])].toReversed(); + const ordered = [...(activeThread?.events ?? [])].reverse(); for (const event of ordered) { if (event.method !== "item/completed") continue; if (!event.itemId) continue; @@ -171,113 +97,27 @@ export default function ChatView() { return map; }, [activeThread?.events]); const timelineEntries = useMemo( - () => deriveTimelineEntries(activeThread?.messages ?? [], workLogEntries), - [activeThread?.messages, workLogEntries], + () => + deriveTimelineEntries( + activeThread?.messages ?? [], + isWorking ? workLogEntries : [], + ), + [activeThread?.messages, isWorking, workLogEntries], ); - const completionSummary = useMemo(() => { - if (!activeThread?.latestTurnStartedAt) return null; - if (!activeThread.latestTurnCompletedAt) return null; - if (workLogEntries.length === 0) return null; - - if ( - typeof activeThread.latestTurnDurationMs === "number" && - Number.isFinite(activeThread.latestTurnDurationMs) && - activeThread.latestTurnDurationMs >= 0 - ) { - return `Worked for ${formatDuration(activeThread.latestTurnDurationMs)}`; - } - - const elapsed = formatElapsed( - activeThread.latestTurnStartedAt, - activeThread.latestTurnCompletedAt, - ); - return elapsed ? `Worked for ${elapsed}` : null; - }, [ - activeThread?.latestTurnStartedAt, - activeThread?.latestTurnCompletedAt, - activeThread?.latestTurnDurationMs, - workLogEntries.length, - ]); - const completionDividerBeforeEntryId = useMemo(() => { - if (!activeThread?.latestTurnStartedAt) return null; - if (!activeThread.latestTurnCompletedAt) return null; - if (workLogEntries.length === 0) return null; - - const turnStartedAt = Date.parse(activeThread.latestTurnStartedAt); - if (Number.isNaN(turnStartedAt)) return null; - - const entry = timelineEntries.find((timelineEntry) => { - if (timelineEntry.kind !== "message") return false; - if (timelineEntry.message.role !== "assistant") return false; - const messageAt = Date.parse(timelineEntry.message.createdAt); - return !Number.isNaN(messageAt) && messageAt >= turnStartedAt; - }); - return entry?.id ?? null; - }, [ - activeThread?.latestTurnStartedAt, - activeThread?.latestTurnCompletedAt, - timelineEntries, - workLogEntries.length, - ]); - const runtimeSessionConfig = - state.runtimeMode === "full-access" - ? ({ - approvalPolicy: "never", - sandboxMode: "danger-full-access", - } as const) - : ({ - approvalPolicy: "on-request", - sandboxMode: "workspace-write", - } as const); - - const handleRuntimeModeChange = async ( - mode: "approval-required" | "full-access", - ) => { - if (mode === state.runtimeMode) return; - dispatch({ type: "SET_RUNTIME_MODE", mode }); - if (!api) return; - - const sessionIds = state.threads - .map((t) => t.session) - .filter( - (s): s is NonNullable => - s !== null && s.status !== "closed", - ) - .map((s) => s.sessionId); - - if (sessionIds.length === 0) return; - - setIsSwitchingRuntimeMode(true); - try { - await Promise.all( - sessionIds.map((id) => - api.providers.stopSession({ sessionId: id }).catch(() => undefined), - ), - ); - } finally { - setIsSwitchingRuntimeMode(false); - } - }; // Auto-scroll on new messages const messageCount = activeThread?.messages.length ?? 0; const workLogCount = workLogEntries.length; + // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on message count change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messageCount]); + // biome-ignore lint/correctness/useExhaustiveDependencies: auto-scroll while active work-log events stream in useEffect(() => { if (phase !== "running") return; messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [phase, workLogCount]); - // Auto-resize textarea - useEffect(() => { - const ta = textareaRef.current; - if (!ta) return; - ta.style.height = "auto"; - ta.style.height = `${Math.min(ta.scrollHeight, 200)}px`; - }, [prompt]); - useEffect(() => { if (phase !== "running") return; const timer = window.setInterval(() => { @@ -288,63 +128,6 @@ export default function ChatView() { }; }, [phase]); - useEffect(() => { - if (!isModelMenuOpen) return; - - const handleClickOutside = (event: MouseEvent) => { - if (!modelMenuRef.current) return; - if (event.target instanceof Node && !modelMenuRef.current.contains(event.target)) { - setIsModelMenuOpen(false); - } - }; - - window.addEventListener("mousedown", handleClickOutside); - return () => { - window.removeEventListener("mousedown", handleClickOutside); - }; - }, [isModelMenuOpen]); - - useEffect(() => { - if (!isEditorMenuOpen) return; - - const handleClickOutside = (event: MouseEvent) => { - if (!editorMenuRef.current) return; - if ( - event.target instanceof Node && - !editorMenuRef.current.contains(event.target) - ) { - setIsEditorMenuOpen(false); - } - }; - - window.addEventListener("mousedown", handleClickOutside); - return () => { - window.removeEventListener("mousedown", handleClickOutside); - }; - }, [isEditorMenuOpen]); - - // Cmd+O / Ctrl+O to open in last-used editor - useEffect(() => { - const handler = (e: globalThis.KeyboardEvent) => { - if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { - if (api && activeProject) { - e.preventDefault(); - void api.shell.openInEditor(activeProject.cwd, lastEditor); - } - } - }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [api, activeProject, lastEditor]); - - const openInEditor = (editorId: EditorId) => { - if (!api || !activeProject) return; - void api.shell.openInEditor(activeProject.cwd, editorId); - setLastEditor(editorId); - localStorage.setItem(LAST_EDITOR_KEY, editorId); - setIsEditorMenuOpen(false); - }; - const ensureSession = async (): Promise => { if (!api || !activeThread || !activeProject) return null; if (activeThread.session && activeThread.session.status !== "closed") { @@ -357,8 +140,6 @@ export default function ChatView() { provider: "codex", cwd: activeProject.cwd || undefined, model: selectedModel || undefined, - approvalPolicy: runtimeSessionConfig.approvalPolicy, - sandboxMode: runtimeSessionConfig.sandboxMode, }); dispatch({ type: "UPDATE_SESSION", @@ -386,7 +167,8 @@ export default function ChatView() { // Auto-title from first message if (activeThread.messages.length === 0) { - const title = trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed; + const title = + trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed; dispatch({ type: "SET_THREAD_TITLE", threadId: activeThread.id, @@ -437,37 +219,6 @@ export default function ChatView() { }); }; - const onRespondToApproval = async ( - requestId: string, - decision: ProviderApprovalDecision, - ) => { - if (!api || !activeThread?.session) return; - - setRespondingRequestIds((existing) => - existing.includes(requestId) ? existing : [...existing, requestId], - ); - try { - await api.providers.respondToRequest({ - sessionId: activeThread.session.sessionId, - requestId, - decision, - }); - } catch (err) { - dispatch({ - type: "SET_ERROR", - threadId: activeThread.id, - error: - err instanceof Error - ? err.message - : "Failed to submit approval decision.", - }); - } finally { - setRespondingRequestIds((existing) => - existing.filter((id) => id !== requestId), - ); - } - }; - const onModelSelect = (model: string) => { if (!activeThread) return; dispatch({ @@ -475,7 +226,6 @@ export default function ChatView() { threadId: activeThread.id, model: resolveModelSlug(model), }); - setIsModelMenuOpen(false); }; const onKeyDown = (e: KeyboardEvent) => { @@ -488,11 +238,13 @@ export default function ChatView() { // Empty state: no active thread if (!activeThread) { return ( -
+
-

Select a thread or create a new one to get started.

+

+ Select a thread or create a new one to get started. +

@@ -500,55 +252,61 @@ export default function ChatView() { } return ( -
+
{/* Top bar */} -
+
-

+

{activeThread.title}

-
-
- {/* Open in editor */} {activeProject && ( -
- - {isEditorMenuOpen && ( -
- {EDITORS.map((editor) => ( - - ))} -
- )} -
+ + {activeProject.name} + )} +
+
+ {/* Status indicator */} +
+ + {(phase === "running" || phase === "connecting") && ( + + )} + + + {statusLabel(phase)} +
{/* Diff toggle */} - - - -
-
- ); - })} -
- )} - {/* Messages */}
{activeThread.messages.length === 0 && !isWorking ? (
-

Send a message to start the conversation.

+

+ Send a message to start the conversation. +

) : (
- {timelineEntries.map((timelineEntry, index) => ( - - {timelineEntry.kind === "message" && - timelineEntry.message.role === "assistant" && - (completionDividerBeforeEntryId === timelineEntry.id || - timelineEntries[index - 1]?.kind === "work") && ( -
- - - {completionSummary - ? `Response • ${completionSummary}` - : "Response"} - - -
- )} - {timelineEntry.kind === "work" ? ( -
- -

- {timelineEntry.entry.detail ? ( - <> - {timelineEntry.entry.label} - - {timelineEntry.entry.detail} - - - ) : ( - timelineEntry.entry.label - )} -

-
- ) : timelineEntry.message.role === "user" ? ( -
-
-
-                        {timelineEntry.message.text}
-                      
-

- {formatTimestamp(timelineEntry.message.createdAt)} -

-
-
- ) : ( -
- - {timelineEntry.message.streaming && ( -
- - - - - - - Thinking + {timelineEntries.map((timelineEntry) => + timelineEntry.kind === "work" ? ( +
+

+ {timelineEntry.entry.detail ? ( + <> + {timelineEntry.entry.label} + + {timelineEntry.entry.detail} -

+ + ) : ( + timelineEntry.entry.label )} -

- {formatMessageMeta( - timelineEntry.message.createdAt, - timelineEntry.message.streaming - ? formatElapsed( - timelineEntry.message.createdAt, - nowIso, - ) - : formatElapsed( - timelineEntry.message.createdAt, - assistantCompletionByItemId.get( - timelineEntry.message.id, +

+
+ ) : ( +
+ {timelineEntry.message.role === "user" ? ( +
+
+
+                          {timelineEntry.message.text}
+                        
+

+ {formatTimestamp(timelineEntry.message.createdAt)} +

+
+
+ ) : ( +
+ + {timelineEntry.message.streaming && ( +
+ + + + + + + Thinking + +
+ )} +

+ {formatMessageMeta( + timelineEntry.message.createdAt, + timelineEntry.message.streaming + ? formatElapsed( + timelineEntry.message.createdAt, + nowIso, + ) + : formatElapsed( + timelineEntry.message.createdAt, + assistantCompletionByItemId.get( + timelineEntry.message.id, + ), ), - ), - )} -

-
- )} - - ))} + )} +

+
+ )} +
+ ), + )} {isWorking && ( -
- +
- - - + + +
@@ -757,271 +428,101 @@ export default function ChatView() { {/* Input bar */}
-
- {/* Textarea area */} -
-