From 377d21211a6b2873437e488590da13d81fa9e926 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Mon, 9 Feb 2026 01:19:46 -0800 Subject: [PATCH] Revert "light mode (#4)" This reverts commit c51f252ff2007f7dbb0abd9a6b0af37476bbca9d. --- 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, 923 insertions(+), 4877 deletions(-) delete mode 100644 apps/renderer/components.json delete mode 100644 apps/renderer/src/components/ui/accordion.tsx delete mode 100644 apps/renderer/src/components/ui/alert-dialog.tsx delete mode 100644 apps/renderer/src/components/ui/alert.tsx delete mode 100644 apps/renderer/src/components/ui/autocomplete.tsx delete mode 100644 apps/renderer/src/components/ui/avatar.tsx delete mode 100644 apps/renderer/src/components/ui/badge.tsx delete mode 100644 apps/renderer/src/components/ui/button.tsx delete mode 100644 apps/renderer/src/components/ui/card.tsx delete mode 100644 apps/renderer/src/components/ui/checkbox-group.tsx delete mode 100644 apps/renderer/src/components/ui/checkbox.tsx delete mode 100644 apps/renderer/src/components/ui/collapsible.tsx delete mode 100644 apps/renderer/src/components/ui/combobox.tsx delete mode 100644 apps/renderer/src/components/ui/command.tsx delete mode 100644 apps/renderer/src/components/ui/dialog.tsx delete mode 100644 apps/renderer/src/components/ui/empty.tsx delete mode 100644 apps/renderer/src/components/ui/field.tsx delete mode 100644 apps/renderer/src/components/ui/fieldset.tsx delete mode 100644 apps/renderer/src/components/ui/form.tsx delete mode 100644 apps/renderer/src/components/ui/input-group.tsx delete mode 100644 apps/renderer/src/components/ui/input.tsx delete mode 100644 apps/renderer/src/components/ui/kbd.tsx delete mode 100644 apps/renderer/src/components/ui/label.tsx delete mode 100644 apps/renderer/src/components/ui/menu.tsx delete mode 100644 apps/renderer/src/components/ui/popover.tsx delete mode 100644 apps/renderer/src/components/ui/progress.tsx delete mode 100644 apps/renderer/src/components/ui/radio-group.tsx delete mode 100644 apps/renderer/src/components/ui/scroll-area.tsx delete mode 100644 apps/renderer/src/components/ui/select.tsx delete mode 100644 apps/renderer/src/components/ui/separator.tsx delete mode 100644 apps/renderer/src/components/ui/sheet.tsx delete mode 100644 apps/renderer/src/components/ui/skeleton.tsx delete mode 100644 apps/renderer/src/components/ui/spinner.tsx delete mode 100644 apps/renderer/src/components/ui/switch.tsx delete mode 100644 apps/renderer/src/components/ui/table.tsx delete mode 100644 apps/renderer/src/components/ui/tabs.tsx delete mode 100644 apps/renderer/src/components/ui/textarea.tsx delete mode 100644 apps/renderer/src/components/ui/toast.tsx delete mode 100644 apps/renderer/src/hooks/useTheme.ts delete 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 482f260591ef7064e6a71c2dd572de39947f9b37..62884bcde3eed6134d9edfdb845671a2028b5a86 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 { execSync, spawn } from "node:child_process"; +import { spawn, execSync } 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 deleted file mode 100644 index 8d7bf441ac8f58d7271472634227d4502d8fe851..0000000000000000000000000000000000000000 --- a/apps/renderer/components.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$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 c2c8793a7c00635add4a75de3659a3449480ce04..7084d1a08b198f7188a00f59ff3d6328c62f9d3f 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 8ab450ea8cc8fb5d340389c6a2ded4fc450e98b4..b91d3d1dae74e61b449a2bf191f3475f41342506 100644 --- a/apps/renderer/package.json +++ b/apps/renderer/package.json @@ -12,17 +12,12 @@ }, "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 a525c7caebec9c3705211e7722daca99c8cecbd1..252e1cd4455782b1abe2754a3a823eb4e26a8ce7 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 5104ddced5e66cfd9c8d77207f41406f36bdf69d..1d51a76851897ea6d488f956681013d31d2928bd 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()); } export default function ChatView() { @@ -65,30 +123,46 @@ 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 ?? [], activeTurnId), - [activeThread?.events, activeTurnId], + () => deriveWorkLogEntries(activeThread?.events ?? [], undefined), + [activeThread?.events], + ); + const pendingApprovals = useMemo( + () => derivePendingApprovals(activeThread?.events ?? []), + [activeThread?.events], ); const assistantCompletionByItemId = useMemo(() => { const map = new Map(); - const ordered = [...(activeThread?.events ?? [])].reverse(); + const ordered = [...(activeThread?.events ?? [])].toReversed(); for (const event of ordered) { if (event.method !== "item/completed") continue; if (!event.itemId) continue; @@ -97,27 +171,113 @@ export default function ChatView() { return map; }, [activeThread?.events]); const timelineEntries = useMemo( - () => - deriveTimelineEntries( - activeThread?.messages ?? [], - isWorking ? workLogEntries : [], - ), - [activeThread?.messages, isWorking, workLogEntries], + () => deriveTimelineEntries(activeThread?.messages ?? [], workLogEntries), + [activeThread?.messages, 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(() => { @@ -128,6 +288,63 @@ 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") { @@ -140,6 +357,8 @@ export default function ChatView() { provider: "codex", cwd: activeProject.cwd || undefined, model: selectedModel || undefined, + approvalPolicy: runtimeSessionConfig.approvalPolicy, + sandboxMode: runtimeSessionConfig.sandboxMode, }); dispatch({ type: "UPDATE_SESSION", @@ -167,8 +386,7 @@ 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, @@ -219,6 +437,37 @@ 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({ @@ -226,6 +475,7 @@ export default function ChatView() { threadId: activeThread.id, model: resolveModelSlug(model), }); + setIsModelMenuOpen(false); }; const onKeyDown = (e: KeyboardEvent) => { @@ -238,13 +488,11 @@ 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.

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

+

{activeThread.title}

+
+
+ {/* Open in editor */} {activeProject && ( - - {activeProject.name} - +
+ + {isEditorMenuOpen && ( +
+ {EDITORS.map((editor) => ( + + ))} +
+ )} +
)} -
-
- {/* 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) => - timelineEntry.kind === "work" ? ( -
-

- {timelineEntry.entry.detail ? ( - <> - {timelineEntry.entry.label} - - {timelineEntry.entry.detail} + {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 - - ) : ( - timelineEntry.entry.label - )} -

-
- ) : ( -
- {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, - ), + )} +

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

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

+
+ )} + + ))} {isWorking && ( -
+
+
- - - + + +
@@ -428,101 +757,271 @@ export default function ChatView() { {/* Input bar */}
- - setPrompt(e.target.value)} - onKeyDown={onKeyDown} - placeholder={ - phase === "disconnected" - ? "Ask for follow-up changes" - : "Ask anything..." - } - disabled={isSending || isConnecting} - rows={2} - className="font-sans!" - /> - - {/* Model picker */} - +
+ {/* Textarea area */} +
+