Home

mweinbach / agent-coworker

publicmweinbach/agent-coworker
Overview Code History Branches Pull requestsIssuesInsights
main
HomeOverview Code PRsIssues
mweinbach/agent-coworker/apps/desktop/src/App.tsx
Raw
1import type { CSSProperties } from "react";2import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";3 4import { useAppStore } from "./app/store";5import { type BootstrapStage, disposeAllJsonRpcState } from "./app/store.helpers";
6
import { operationKey } from "./app/store.helpers/operations";
7import { isOneOffChatWorkspace } from "./app/types";
8import { Spinner } from "./components/ui/spinner";
9import { resolveRightRailSizing } from "./lib/adaptiveLayout";
10import { getCanvasSurfaceKind } from "./lib/canvasAppearance";
11import { requestCanvasDocumentTransition } from "./lib/canvasDocumentLifecycle";
12import type { DesktopMenuCommand, SystemAppearance } from "./lib/desktopApi";
13import {
14 getPlatformChrome,
15 getSystemAppearance,
16 onMenuCommand,
17 onPreviewFileChanged,
18 onSystemAppearanceChanged,
19 onUpdateStateChanged,
20 onWindowCloseRequested,
21 onWorkspaceServerExited,
22 onWorkspaceServerStartupProgress,
23 resolveWindowCloseRequest,
24 setWindowAppearance,
25 showCanvasWindow,
26 showNotification,
27 showQuickChatWindow,
28 writeRendererLog,
29} from "./lib/desktopCommands";
30import { onDesktopRailCommand, requestDesktopRailCommand } from "./lib/desktopRailCommands";
31import { getFilePreviewKind, isCanvasSupportedFile } from "./lib/filePreviewKind";
32import { workspaceFileChangeEvents } from "./lib/filePreviewResource";
33import { applyPlatformChromeToDocument, syncPlatformChromeCssVars } from "./lib/platformChromeDom";
34import { canPopOutQuickChatThread } from "./lib/quickChatPopout";
35import { applySystemAppearanceToDocument, readBootstrappedThemeSource } from "./lib/themeBootstrap";
36import { useAdaptiveLayout } from "./lib/useAdaptiveLayout";
37import { cn } from "./lib/utils";
38import { getDesktopWindowMode } from "./lib/windowMode";
39import { Canvas } from "./ui/Canvas";
40import { CommandPalette } from "./ui/CommandPalette";
41import { ConnectionRecoveryBanner } from "./ui/ConnectionRecoveryBanner";
42import { ContextSidebar } from "./ui/ContextSidebar";
43import { InlineErrorBoundary } from "./ui/CrashReportingErrorBoundary";
44import { shouldShowReconnectBanner } from "./ui/chat/chatLogic";
45import { LmStudioStartDialog } from "./ui/chat/LmStudioStartDialog";
46import { FilePreviewModal } from "./ui/FilePreviewModal";
47import { InAppToasts } from "./ui/InAppToasts";
48import { AdaptiveRailSurface } from "./ui/layout/AdaptiveRailSurface";
49import { AppTopBar } from "./ui/layout/AppTopBar";
50import { ContextSidebarResizer } from "./ui/layout/ContextSidebarResizer";
51import { PrimaryContent } from "./ui/layout/PrimaryContent";
52import { SettingsContent } from "./ui/layout/SettingsContent";
53import { SidebarResizer } from "./ui/layout/SidebarResizer";
54import { MenuBarUtilityShell } from "./ui/menuBar/MenuBarUtilityShell";
55import { isEditableEscapeTarget, OverlayStackProvider, useOverlayStack } from "./ui/OverlayStack";
56import { DesktopOnboarding } from "./ui/onboarding/DesktopOnboarding";
57import { QuickChatShell } from "./ui/quickChat/QuickChatShell";
58import { StartupRecovery } from "./ui/recovery/StartupRecovery";
59import { startupStagePresentation } from "./ui/recovery/startupPresentation";
60import { Sidebar } from "./ui/Sidebar";
61import { TranscriptDeliveryRecovery } from "./ui/TranscriptDeliveryRecovery";
62import { TaskContextSidebar } from "./ui/tasks/TaskContextSidebar";
63 
64const EMPTY_AGENTS: never[] = [];
65 
66const LeftSidebarPane = memo(function LeftSidebarPane({
67 active,
68 maximumWidth,
69 onClose,
70 overlay,
71 width,
72}: {
73 active: boolean;
74 maximumWidth: number;
75 onClose: () => void;
76 overlay: boolean;
77 width: number;
78}) {
79 return (
80 <AdaptiveRailSurface
81 active={active}
82 className="app-left-sidebar-pane h-full border-r border-border/70"
83 label="Sidebar"
84 onClose={onClose}
85 overlay={overlay}
86 side="left"
87 width={width}
88 >
89 <div className="absolute inset-y-0 right-0 flex w-full">
90 <Sidebar />
91 </div>
92 {active && !overlay ? (
93 <SidebarResizer effectiveWidth={width} maximumWidth={maximumWidth} />
94 ) : null}
95 </AdaptiveRailSurface>
96 );
97});
98 
99const RightSidebarPane = memo(function RightSidebarPane({
100 active,
101 maximumWidth,
102 minimumWidth,
103 onClose,
104 overlay,
105 width,
106}: {
107 active: boolean;
108 maximumWidth: number;
109 minimumWidth: number;
110 onClose: () => void;
111 overlay: boolean;
112 width: number;
113}) {
114 const filePreview = useAppStore((s) => s.filePreview);
115 const canvasEnabled = useAppStore((s) => s.desktopFeatureFlags?.canvas === true);
116 const isCanvasMaximized = useAppStore((s) => s.isCanvasMaximized);
117 const view = useAppStore((s) => s.view);
118 
119 const isCanvasSupported = filePreview?.path && isCanvasSupportedFile(filePreview.path);
120 const showCanvas = canvasEnabled && isCanvasSupported;
121 const canvasMaximized = showCanvas && isCanvasMaximized;
122 const canvasContainerStyle: CSSProperties = canvasMaximized
123 ? {
124 top: "calc(var(--platform-drag-strip-height) + var(--platform-titlebar-height))",
125 right: 0,
126 bottom: 0,
127 left: 0,
128 }
129 : { width: "100%" };
130 
131 return (
132 <AdaptiveRailSurface
133 active={canvasMaximized || active}
134 className={cn(
135 "app-right-sidebar-pane h-full",
136 canvasMaximized ? "overflow-visible" : "overflow-hidden",
137 )}
138 label="Context"
139 onClose={onClose}
140 overlay={!canvasMaximized && overlay}
141 side="right"
142 width={canvasMaximized ? 0 : width}
143 >
144 {active && !overlay && !canvasMaximized ? (
145 <ContextSidebarResizer
146 effectiveWidth={width}
147 maximumWidth={maximumWidth}
148 minimumWidth={minimumWidth}
149 />
150 ) : null}
151 <div
152 className={cn(
153 "flex bg-background",
154 canvasMaximized ? "fixed z-40" : "absolute top-0 bottom-0 left-0",
155 )}
156 style={canvasContainerStyle}
157 >
158 {showCanvas && filePreview?.path ? (
159 <InlineErrorBoundary label="This canvas couldn't be rendered.">
160 <Canvas path={filePreview.path} />
161 </InlineErrorBoundary>
162 ) : view === "task" ? (
163 <TaskContextSidebar variant="sidebar" />
164 ) : (
165 <ContextSidebar active={canvasMaximized || active} />
166 )}
167 </div>
168 </AdaptiveRailSurface>
169 );
170});
171 
172function runJsonRpcShutdownDisposal() {
173 disposeAllJsonRpcState();
174}
175 
176export function isStopTurnShortcut(event: KeyboardEvent): boolean {
177 return (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key === ".";
178}
179 
180const ChatShell = memo(function ChatShell({
181 init,
182 ready,
183 startupError,
184 bootstrapLoading,
185 bootstrapStage,
186}: {
187 init: () => Promise<void>;
188 ready: boolean;
189 startupError: string | null;
190 bootstrapLoading: boolean;
191 bootstrapStage: BootstrapStage | null;
192}) {
193 const view = useAppStore((s) => s.view);
194 const workspaces = useAppStore((s) => s.workspaces);
195 const threads = useAppStore((s) => s.threads);
196 const selectedThreadId = useAppStore((s) => s.selectedThreadId);
197 const selectedWorkspaceId = useAppStore((s) => s.selectedWorkspaceId);
198 const selectedTask = useAppStore((s) =>
199 s.selectedTaskId ? s.tasksById[s.selectedTaskId] : null,
200 );
201 const selectedThreadBusy = useAppStore((s) =>
202 s.selectedThreadId ? s.threadRuntimeById[s.selectedThreadId]?.busy === true : false,
203 );
204 const selectedSessionUsage = useAppStore((s) =>
205 s.selectedThreadId ? (s.threadRuntimeById[s.selectedThreadId]?.sessionUsage ?? null) : null,
206 );
207 const selectedLastTurnUsage = useAppStore((s) =>
208 s.selectedThreadId ? (s.threadRuntimeById[s.selectedThreadId]?.lastTurnUsage ?? null) : null,
209 );
210 const selectedAgents = useAppStore((s) => {
211 if (!s.selectedThreadId) return EMPTY_AGENTS;
212 return s.threadRuntimeById[s.selectedThreadId]?.agents ?? EMPTY_AGENTS;
213 });
214 const selectedSessionUsageStop = useAppStore((s) =>
215 s.selectedThreadId
216 ? s.threadRuntimeById[s.selectedThreadId]?.sessionUsage?.budgetStatus.stopTriggered === true
217 : false,
218 );
219 const selectedTranscriptOnly = useAppStore((s) =>
220 s.selectedThreadId ? s.threadRuntimeById[s.selectedThreadId]?.transcriptOnly === true : false,
221 );
222 const selectedConnected = useAppStore((s) =>
223 s.selectedThreadId ? s.threadRuntimeById[s.selectedThreadId]?.connected === true : false,
224 );
225 const selectedHydrating = useAppStore((s) =>
226 s.selectedThreadId ? s.threadRuntimeById[s.selectedThreadId]?.hydrating === true : false,
227 );
228 const selectedRuntimeExists = useAppStore((s) =>
229 s.selectedThreadId ? s.threadRuntimeById[s.selectedThreadId] !== undefined : false,
230 );
231 const selectedSessionId = useAppStore((s) =>
232 s.selectedThreadId ? (s.threadRuntimeById[s.selectedThreadId]?.sessionId ?? null) : null,
233 );
234 const workspaceRuntimeById = useAppStore((s) => s.workspaceRuntimeById);
235 const operationsByKey = useAppStore((s) => s.operationsByKey);
236 const reconnectThreadWithFeedback = useAppStore((s) => s.reconnectThreadWithFeedback);
237 const sidebarCollapsed = useAppStore((s) => s.sidebarCollapsed);
238 const sidebarWidth = useAppStore((s) => s.sidebarWidth);
239 const toggleSidebar = useAppStore((s) => s.toggleSidebar);
240 const openNewChatLanding = useAppStore((s) => s.openNewChatLanding);
241 const clearThreadUsageHardCap = useAppStore((s) => s.clearThreadUsageHardCap);
242 const contextSidebarCollapsed = useAppStore((s) => s.contextSidebarCollapsed);
243 const contextSidebarWidth = useAppStore((s) => s.contextSidebarWidth);
244 const canvasSidebarWidth = useAppStore((s) => s.canvasSidebarWidth);
245 const toggleContextSidebar = useAppStore((s) => s.toggleContextSidebar);
246 const filePreview = useAppStore((s) => s.filePreview);
247 const canvasEnabled = useAppStore((s) => s.desktopFeatureFlags?.canvas === true);
248 const closeFilePreview = useAppStore((s) => s.closeFilePreview);
249 const canvasActiveTab = useAppStore((s) => s.canvasActiveTab);
250 const setCanvasActiveTab = useAppStore((s) => s.setCanvasActiveTab);
251 const canvasShowFormattingBar = useAppStore((s) => s.canvasShowFormattingBar);
252 const setCanvasShowFormattingBar = useAppStore((s) => s.setCanvasShowFormattingBar);
253 const isCanvasMaximized = useAppStore((s) => s.isCanvasMaximized);
254 const setCanvasMaximized = useAppStore((s) => s.setCanvasMaximized);
255 const [leftOverlayOpen, setLeftOverlayOpen] = useState(false);
256 const [rightOverlayOpen, setRightOverlayOpen] = useState(false);
257 const hasAnimatedSidebarsRef = useRef(false);
258 const previousSidebarStateRef = useRef({
259 sidebarCollapsed,
260 contextSidebarCollapsed,
261 });
262 
263 const activeThread = useMemo(
264 () => threads.find((thread) => thread.id === selectedThreadId) ?? null,
265 [selectedThreadId, threads],
266 );
267 const activeWorkspace = useMemo(() => {
268 const workspaceId = activeThread?.workspaceId ?? selectedWorkspaceId;
269 return workspaces.find((workspace) => workspace.id === workspaceId) ?? null;
270 }, [activeThread, selectedWorkspaceId, workspaces]);
271 const busy = selectedThreadBusy;
272 const effectiveView = view;
273 const isConversationView = effectiveView === "chat" || effectiveView === "task";
274 const showContextSidebar =
275 (effectiveView === "chat" && activeThread !== null) ||
276 (effectiveView === "task" && selectedTask !== null);
277 const canvasPath = filePreview?.path ?? null;
278 const canvasSupported = canvasPath !== null && isCanvasSupportedFile(canvasPath);
279 const showCanvasSurface = isConversationView && canvasEnabled && canvasSupported;
280 const rightRailKind = showCanvasSurface
281 ? "canvas"
282 : effectiveView === "task"
283 ? "task"
284 : "context";
285 const rightRailSizing = resolveRightRailSizing(rightRailKind, {
286 canvas: canvasSidebarWidth,
287 context: contextSidebarWidth,
288 });
289 const adaptiveLayout = useAdaptiveLayout({
290 contextSidebarCollapsed,
291 hasContextSidebar: showContextSidebar,
292 leftSidebarWidth: sidebarWidth,
293 rightSidebarMaximumWidth: rightRailSizing.maximumWidth,
294 rightSidebarMinimumWidth: rightRailSizing.minimumWidth,
295 rightSidebarWidth: rightRailSizing.preferredWidth,
296 sidebarCollapsed,
297 });
298 const leftOverlayWidth = Math.max(160, Math.min(440, sidebarWidth));
299 const rightOverlayWidth = rightRailSizing.preferredWidth;
300 const leftRailActive =
301 adaptiveLayout.leftInline || (adaptiveLayout.leftOverlay && leftOverlayOpen);
302 const rightRailActive =
303 adaptiveLayout.rightInline || (adaptiveLayout.rightOverlay && rightOverlayOpen);
304 const leftRailWidth = adaptiveLayout.leftOverlay ? leftOverlayWidth : adaptiveLayout.leftWidth;
305 const rightRailWidth = adaptiveLayout.rightOverlay
306 ? rightOverlayWidth
307 : adaptiveLayout.rightWidth;
308 const toggleAdaptiveSidebar = useCallback(() => {
309 if (adaptiveLayout.leftOverlay) {
310 const nextOpen = !leftOverlayOpen;
311 setLeftOverlayOpen(nextOpen);
312 if (nextOpen) setRightOverlayOpen(false);
313 return;
314 }
315 toggleSidebar();
316 }, [adaptiveLayout.leftOverlay, leftOverlayOpen, toggleSidebar]);
317 const toggleAdaptiveContextSidebar = useCallback(() => {
318 if (adaptiveLayout.rightOverlay) {
319 const nextOpen = !rightOverlayOpen;
320 setRightOverlayOpen(nextOpen);
321 if (nextOpen) setLeftOverlayOpen(false);
322 return;
323 }
324 toggleContextSidebar();
325 }, [adaptiveLayout.rightOverlay, rightOverlayOpen, toggleContextSidebar]);
326 const activeWorkspaceId = activeWorkspace?.id ?? null;
327 const workspaceStartupProgress = useMemo(() => {
328 const activeRuntime = activeWorkspaceId ? workspaceRuntimeById[activeWorkspaceId] : null;
329 if (activeRuntime?.starting && !activeRuntime.serverUrl && activeRuntime.startupProgress) {
330 return activeRuntime.startupProgress;
331 }
332 if (activeWorkspaceId) return null;
333 for (const runtime of Object.values(workspaceRuntimeById)) {
334 if (runtime.starting && !runtime.serverUrl && runtime.startupProgress) {
335 return runtime.startupProgress;
336 }
337 }
338 return null;
339 }, [activeWorkspaceId, workspaceRuntimeById]);
340 const topBarTitle =
341 effectiveView === "research"
342 ? "Research"
343 : effectiveView === "task"
344 ? (selectedTask?.title ?? "New task")
345 : activeThread?.title?.trim() || "New chat";
346 const topBarSubtitle: string | null =
347 effectiveView === "research"
348 ? null
349 : isOneOffChatWorkspace(activeWorkspace)
350 ? null
351 : (activeWorkspace?.name ?? "Cowork");
352 const canClearHardCap =
353 selectedSessionUsageStop &&
354 !selectedTranscriptOnly &&
355 selectedConnected &&
356 Boolean(selectedSessionId) &&
357 activeThread?.status === "active";
358 const quickChatPopOutThreadId =
359 effectiveView === "chat" && activeThread && canPopOutQuickChatThread(activeThread)
360 ? activeThread.id
361 : null;
362 const showCanvasInTopBar = showCanvasSurface;
363 const canvasKind = canvasPath !== null ? getFilePreviewKind(canvasPath) : "other";
364 const canvasIsMarkdown = canvasKind === "markdown";
365 const canvasIsSpreadsheet = canvasKind === "csv" || canvasKind === "xlsx";
366 const terminalTaskConversation =
367 effectiveView === "task" &&
368 selectedTask !== null &&
369 (selectedTask.status === "completed" ||
370 selectedTask.status === "cancelled" ||
371 selectedTask.status === "failed");
372 const showReconnectBanner = shouldShowReconnectBanner({
373 conversationVisible: isConversationView,
374 threadId: selectedThreadId,
375 threadStatus: activeThread?.status ?? null,
376 transcriptOnly: selectedTranscriptOnly,
377 connected: selectedConnected,
378 sessionId: selectedSessionId,
379 hydrating:
380 selectedHydrating ||
381 (bootstrapLoading &&
382 Boolean(selectedThreadId) &&
383 activeThread !== null &&
384 !selectedRuntimeExists),
385 workspaceStarting: workspaceStartupProgress !== null,
386 terminalTaskConversation,
387 });
388 const reconnectOperation = selectedThreadId
389 ? operationsByKey[operationKey("thread-reconnect", selectedThreadId)]
390 : undefined;
391 const preserveCachedContentOnStartupError =
392 Boolean(startupError) && ready && (workspaces.length > 0 || threads.length > 0);
393 const startupPresentation = startupStagePresentation(bootstrapStage);
394 const activeCanvasPath = showCanvasSurface ? canvasPath : null;
395 const previousCanvasPathRef = useRef<string | null>(null);
396 const previousRightOverlayRef = useRef(false);
397 const canvasOpenedRightOverlayRef = useRef(false);
398 const overlayScope = `${adaptiveLayout.tier}:${effectiveView}:${selectedThreadId ?? "none"}`;
399 const previousOverlayScopeRef = useRef(overlayScope);
400 
401 useEffect(() => {
402 if (previousOverlayScopeRef.current === overlayScope) return;
403 previousOverlayScopeRef.current = overlayScope;
404 setLeftOverlayOpen(false);
405 setRightOverlayOpen(false);
406 }, [overlayScope]);
407 
408 useEffect(() => {
409 const previousCanvasPath = previousCanvasPathRef.current;
410 const enteredRightOverlay = adaptiveLayout.rightOverlay && !previousRightOverlayRef.current;
411 previousCanvasPathRef.current = activeCanvasPath;
412 previousRightOverlayRef.current = adaptiveLayout.rightOverlay;
413 if (previousCanvasPath !== null && activeCanvasPath === null) {
414 if (adaptiveLayout.rightOverlay && canvasOpenedRightOverlayRef.current) {
415 setRightOverlayOpen(false);
416 }
417 canvasOpenedRightOverlayRef.current = false;
418 return;
419 }
420 if (
421 adaptiveLayout.rightOverlay &&
422 activeCanvasPath !== null &&
423 (activeCanvasPath !== previousCanvasPath || enteredRightOverlay)
424 ) {
425 const openedByCanvas =
426 canvasOpenedRightOverlayRef.current ||
427 enteredRightOverlay ||
428 (previousCanvasPath === null && !rightOverlayOpen);
429 canvasOpenedRightOverlayRef.current = openedByCanvas;
430 if (openedByCanvas) {
431 setLeftOverlayOpen(false);
432 setRightOverlayOpen(true);
433 }
434 return;
435 }
436 if (
437 adaptiveLayout.rightOverlay &&
438 activeCanvasPath !== null &&
439 !rightOverlayOpen &&
440 canvasOpenedRightOverlayRef.current
441 ) {
442 canvasOpenedRightOverlayRef.current = false;
443 }
444 }, [activeCanvasPath, adaptiveLayout.rightOverlay, rightOverlayOpen]);
445 
446 useEffect(
447 () =>
448 onDesktopRailCommand((command) => {
449 if (command === "toggle-sidebar") {
450 toggleAdaptiveSidebar();
451 } else {
452 toggleAdaptiveContextSidebar();
453 }
454 }),
455 [toggleAdaptiveContextSidebar, toggleAdaptiveSidebar],
456 );
457 
458 useEffect(() => {
459 const documentBody = document.body;
460 const windowTarget = window;
461 const sidebarStateChanged =
462 previousSidebarStateRef.current.sidebarCollapsed !== sidebarCollapsed ||
463 previousSidebarStateRef.current.contextSidebarCollapsed !== contextSidebarCollapsed;
464 previousSidebarStateRef.current = {
465 sidebarCollapsed,
466 contextSidebarCollapsed,
467 };
468 
469 if (!hasAnimatedSidebarsRef.current) {
470 hasAnimatedSidebarsRef.current = true;
471 return;
472 }
473 if (!sidebarStateChanged) {
474 return;
475 }
476 documentBody.classList.add("app-animating-sidebars");
477 const timer = windowTarget.setTimeout(() => {
478 documentBody.classList.remove("app-animating-sidebars");
479 }, 340);
480 return () => {
481 windowTarget.clearTimeout(timer);
482 documentBody.classList.remove("app-animating-sidebars");
483 };
484 }, [contextSidebarCollapsed, sidebarCollapsed]);
485 
486 return (
487 <div
488 className="app-shell app-shell--chat flex h-full min-h-0 flex-col text-foreground"
489 data-layout-tier={adaptiveLayout.tier}
490 >
491 <a
492 href="#main-content"
493 className="sr-only z-50 rounded-md bg-background px-3 py-2 text-sm font-medium text-foreground shadow-md focus:not-sr-only focus:absolute focus:left-2 focus:top-2 focus:outline-none focus:ring-2 focus:ring-ring"
494 >
495 Skip to content
496 </a>
497 <div className="app-window-drag-strip" aria-hidden="true" />
498 <AppTopBar
499 busy={isConversationView ? busy : false}
500 onToggleSidebar={toggleAdaptiveSidebar}
501 onNewChat={() => void openNewChatLanding()}
502 sidebarCollapsed={!adaptiveLayout.leftInline}
503 sidebarWidth={adaptiveLayout.leftWidth}
504 sidebarToggleLabel={
505 adaptiveLayout.leftOverlay
506 ? leftOverlayOpen
507 ? "Close sidebar"
508 : "Show sidebar"
509 : undefined
510 }
511 contextSidebarCollapsed={!rightRailActive}
512 contextSidebarToggleLabel={
513 adaptiveLayout.rightOverlay
514 ? rightOverlayOpen
515 ? "Close context"
516 : "Show context"
517 : undefined
518 }
519 onToggleContextSidebar={toggleAdaptiveContextSidebar}
520 onPopOutQuickChat={
521 quickChatPopOutThreadId
522 ? () => void showQuickChatWindow({ threadId: quickChatPopOutThreadId })
523 : undefined
524 }
525 title={topBarTitle}
526 subtitle={adaptiveLayout.tier === "full" ? topBarSubtitle : null}
527 compactToolbar={adaptiveLayout.tier !== "full"}
528 suppressThreadDetails={effectiveView === "research"}
529 hideThreadShell={isConversationView && activeThread === null}
530 sessionUsage={isConversationView ? selectedSessionUsage : null}
531 lastTurnUsage={isConversationView ? selectedLastTurnUsage : null}
532 agents={isConversationView ? selectedAgents : []}
533 canClearHardCap={canClearHardCap}
534 onClearHardCap={
535 selectedThreadId ? () => clearThreadUsageHardCap(selectedThreadId) : undefined
536 }
537 showContextToggle={showContextSidebar && workspaceStartupProgress === null}
538 canvasMode={showCanvasInTopBar}
539 canvasIsMarkdown={canvasIsMarkdown}
540 canvasActiveTab={canvasActiveTab}
541 onSetCanvasActiveTab={setCanvasActiveTab}
542 canvasShowFormattingBar={canvasShowFormattingBar}
543 onToggleCanvasFormattingBar={
544 canvasIsMarkdown && canvasActiveTab === "edit"
545 ? () => setCanvasShowFormattingBar(!canvasShowFormattingBar)
546 : undefined
547 }
548 canvasMaximized={isCanvasMaximized}
549 onToggleCanvasMaximized={
550 showCanvasInTopBar ? () => setCanvasMaximized(!isCanvasMaximized) : undefined
551 }
552 onPopOutCanvas={
553 showCanvasInTopBar && canvasPath && !canvasIsSpreadsheet
554 ? () => {
555 void showCanvasWindow({ path: canvasPath }).catch(() => {});
556 }
557 : undefined
558 }
559 onCloseCanvas={showCanvasInTopBar ? closeFilePreview : undefined}
560 />
561 {preserveCachedContentOnStartupError && startupError ? (
562 <StartupRecovery
563 detail={startupError}
564 init={init}
565 retrying={bootstrapLoading}
566 presentation="banner"
567 />
568 ) : ready && bootstrapLoading ? (
569 <div
570 role="status"
571 data-slot="startup-progress-banner"
572 className="flex shrink-0 items-center gap-2 border-b border-border/60 bg-background/85 px-4 py-2 text-xs text-muted-foreground"
573 >
574 <Spinner className="size-3.5" aria-hidden="true" />
575 <span>
576 {startupPresentation.title}. {startupPresentation.detail}
577 </span>
578 </div>
579 ) : null}
580 {!startupError && !bootstrapLoading && selectedThreadId ? (
581 <ConnectionRecoveryBanner
582 disconnected={showReconnectBanner}
583 operation={reconnectOperation}
584 reconnect={() => reconnectThreadWithFeedback(selectedThreadId)}
585 />
586 ) : null}
587 <div className="app-chat-body relative flex min-h-0 min-w-0 flex-1 flex-row">
588 <LeftSidebarPane
589 active={leftRailActive}
590 maximumWidth={Math.max(160, adaptiveLayout.leftMaximumWidth)}
591 onClose={() => setLeftOverlayOpen(false)}
592 overlay={adaptiveLayout.leftOverlay}
593 width={leftRailWidth}
594 />
595 <main
596 id="main-content"
597 tabIndex={-1}
598 aria-label={
599 effectiveView === "settings"
600 ? "Settings"
601 : effectiveView === "research"
602 ? "Research"
603 : effectiveView === "task"
604 ? "Task"
605 : "Chat"
606 }
607 className="app-main-content flex min-h-0 min-w-0 flex-1 flex-col outline-none"
608 >
609 <div className="flex min-h-0 flex-1 overflow-hidden">
610 <div
611 className="relative min-h-0 min-w-0 flex-1 overflow-hidden"
612 data-slot="primary-content-pane"
613 >
614 <PrimaryContent
615 init={init}
616 ready={ready}
617 bootstrapLoading={bootstrapLoading}
618 bootstrapStage={bootstrapStage}
619 startupError={preserveCachedContentOnStartupError ? null : startupError}
620 workspaceStartupProgress={workspaceStartupProgress}
621 view={
622 effectiveView === "research"
623 ? "research"
624 : effectiveView === "task"
625 ? "task"
626 : "chat"
627 }
628 />
629 </div>
630 {showContextSidebar && workspaceStartupProgress === null ? (
631 <RightSidebarPane
632 active={rightRailActive}
633 maximumWidth={Math.max(
634 rightRailSizing.minimumWidth,
635 adaptiveLayout.rightMaximumWidth,
636 )}
637 minimumWidth={rightRailSizing.minimumWidth}
638 onClose={() => setRightOverlayOpen(false)}
639 overlay={adaptiveLayout.rightOverlay}
640 width={rightRailWidth}
641 />
642 ) : null}
643 </div>
644 </main>
645 </div>
646 </div>
647 );
648});
649 
650function AppContent() {
651 const windowMode = getDesktopWindowMode();
652 const canvasWindowPath =
653 windowMode === "canvas" ? new URLSearchParams(window.location.search).get("path") || "" : "";
654 const canvasSurfaceKind = getCanvasSurfaceKind(canvasWindowPath);
655 const { hasOpenOverlay } = useOverlayStack();
656 const ready = useAppStore((s) => s.ready);
657 const bootstrapPhase = useAppStore((s) => s.bootstrapPhase);
658 const bootstrapStage = useAppStore((s) => s.bootstrapStage);
659 const startupError = useAppStore((s) => s.startupError);
660 const init = useAppStore((s) => s.init);
661 const invalidateBootstrap = useAppStore((s) => s.invalidateBootstrap);
662 const view = useAppStore((s) => s.view);
663 const filePreviewPath = useAppStore((s) => s.filePreview?.path ?? null);
664 const canvasEnabled = useAppStore((s) => s.desktopFeatureFlags?.canvas === true);
665 const notifications = useAppStore((s) => s.notifications);
666 const setUpdateState = useAppStore((s) => s.setUpdateState);
667 const handleWorkspaceServerExited = useAppStore((s) => s.handleWorkspaceServerExited);
668 const setWorkspaceServerStartupProgress = useAppStore((s) => s.setWorkspaceServerStartupProgress);
669 const seenNotificationIds = useRef(new Set<string>());
670 const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
671 
672 useEffect(
673 () =>
674 onWorkspaceServerStartupProgress((event) => {
675 void writeRendererLog({
676 category: "sidecar",
677 message: "workspace server startup progress",
678 meta: {
679 workspaceId: event.workspaceId,
680 phase: event.progress.phase,
681 version: event.progress.version,
682 percent: event.progress.percent,
683 },
684 }).catch(() => {});
685 setWorkspaceServerStartupProgress(event);
686 }),
687 [setWorkspaceServerStartupProgress],
688 );
689 
690 useEffect(() => {
691 if (windowMode !== "main") return;
692 return onWorkspaceServerExited((event) => {
693 void writeRendererLog({
694 category: "sidecar",
695 message: "workspace server exited",
696 meta: {
697 workspaceId: event.workspaceId,
698 code: event.code,
699 signal: event.signal,
700 },
701 }).catch(() => {});
702 handleWorkspaceServerExited(event);
703 });
704 }, [handleWorkspaceServerExited, windowMode]);
705 
706 useEffect(
707 () =>
708 onWindowCloseRequested((request) => {
709 void (async () => {
710 let canClose = false;
711 try {
712 canClose = await requestCanvasDocumentTransition(null);
713 } catch {
714 canClose = false;
715 } finally {
716 await resolveWindowCloseRequest({
717 requestId: request.requestId,
718 canClose,
719 }).catch(() => {});
720 }
721 })();
722 }),
723 [],
724 );
725 
726 useEffect(() => {
727 const documentElement = document.documentElement;
728 documentElement.dataset.windowMode = windowMode;
729 if (windowMode === "canvas") {
730 documentElement.dataset.canvasSurface = canvasSurfaceKind;
731 }
732 return () => {
733 delete documentElement.dataset.windowMode;
734 delete documentElement.dataset.canvasSurface;
735 };
736 }, [canvasSurfaceKind, windowMode]);
737 
738 useEffect(() => {
739 if (useAppStore.getState().bootstrapPhase !== "idle") return;
740 void init().catch((err) => {
741 console.error(err);
742 });
743 }, [init]);
744 
745 useEffect(() => {
746 let disposed = false;
747 const windowTarget = window;
748 const handleBeforeUnload = () => {
749 if (disposed) {
750 return;
751 }
752 disposed = true;
753 invalidateBootstrap();
754 runJsonRpcShutdownDisposal();
755 };
756 
757 windowTarget.addEventListener("beforeunload", handleBeforeUnload);
758 return () => {
759 windowTarget.removeEventListener("beforeunload", handleBeforeUnload);
760 };
761 }, [invalidateBootstrap]);
762 
763 useEffect(() => {
764 const windowTarget = window;
765 function handleKeyDown(event: KeyboardEvent) {
766 if (event.key === "Escape") {
767 if (event.defaultPrevented || event.isComposing || hasOpenOverlay()) return;
768 if (isEditableEscapeTarget(event.target)) return;
769 const state = useAppStore.getState();
770 if (state.view === "settings") {
771 event.preventDefault();
772 state.closeSettings();
773 }
774 return;
775 }
776 
777 if (isStopTurnShortcut(event)) {
778 if (event.defaultPrevented || event.isComposing || hasOpenOverlay()) return;
779 const state = useAppStore.getState();
780 if (!state.selectedThreadId || !state.threadRuntimeById[state.selectedThreadId]?.busy) {
781 return;
782 }
783 event.preventDefault();
784 state.cancelThread(state.selectedThreadId);
785 }
786 }
787 
788 windowTarget.addEventListener("keydown", handleKeyDown);
789 return () => windowTarget.removeEventListener("keydown", handleKeyDown);
790 }, [hasOpenOverlay]);
791 
792 // Cmd/Ctrl+K opens the command palette. Scoped to the main window so the
793 // popout quick-chat / menu-bar / canvas windows keep their minimal shells.
794 useEffect(() => {
795 if (windowMode !== "main") return;
796 const windowTarget = window;
797 function handlePaletteShortcut(event: KeyboardEvent) {
798 if (
799 (event.metaKey || event.ctrlKey) &&
800 !event.shiftKey &&
801 !event.altKey &&
802 event.key === "k"
803 ) {
804 event.preventDefault();
805 setCommandPaletteOpen((open) => !open);
806 }
807 }
808 windowTarget.addEventListener("keydown", handlePaletteShortcut);
809 return () => windowTarget.removeEventListener("keydown", handlePaletteShortcut);
810 }, [windowMode]);
811 
812 useEffect(() => {
813 function handleMenuCommand(command: DesktopMenuCommand): void {
814 const state = useAppStore.getState();
815 if (command === "newThread") {
816 void state.openNewChatLanding();
817 return;
818 }
819 if (command === "toggleSidebar") {
820 requestDesktopRailCommand("toggle-sidebar");
821 return;
822 }
823 if (command === "openSettings") {
824 state.openSettings();
825 return;
826 }
827 if (command === "openWorkspacesSettings") {
828 state.openSettings("defaults");
829 return;
830 }
831 if (command === "openUpdates") {
832 state.openSettings("updates");
833 void state.checkForUpdates();
834 return;
835 }
836 if (command === "openResearch") {
837 void state.openResearch();
838 return;
839 }
840 if (command === "openSkills") {
841 void state.openSkills();
842 return;
843 }
844 if (command === "openCommandPalette") {
845 setCommandPaletteOpen(true);
846 }
847 }
848 
849 const unsubscribe = onMenuCommand(handleMenuCommand);
850 return unsubscribe;
851 }, []);
852 
853 useEffect(() => {
854 return onPreviewFileChanged((event) => {
855 workspaceFileChangeEvents.publish(event);
856 });
857 }, []);
858 
859 useEffect(() => {
860 return onUpdateStateChanged(setUpdateState);
861 }, [setUpdateState]);
862 
863 useEffect(() => {
864 function applySystemAppearance(appearance: SystemAppearance): void {
865 applySystemAppearanceToDocument(appearance, document, localStorage);
866 syncPlatformChromeCssVars(document);
867 }
868 
869 const unsubscribe = onSystemAppearanceChanged(applySystemAppearance);
870 void getSystemAppearance()
871 .then(applySystemAppearance)
872 .catch(() => {
873 // Keep CSS media-query fallback when system appearance cannot be loaded.
874 });
875 void setWindowAppearance({
876 themeSource: readBootstrappedThemeSource(document.documentElement),
877 }).catch(() => {
878 // Ignore and continue with default system theme behavior.
879 });
880 return unsubscribe;
881 }, []);
882 
883 useEffect(() => {
884 void getPlatformChrome()
885 .then((chrome) => {
886 applyPlatformChromeToDocument(document, chrome);
887 })
888 .catch(() => {
889 // Fallback to defaults if platform chrome cannot be loaded.
890 });
891 }, []);
892 
893 useEffect(() => {
894 if (windowMode !== "main") {
895 return;
896 }
897 for (const notification of notifications) {
898 if (seenNotificationIds.current.has(notification.id)) {
899 continue;
900 }
901 seenNotificationIds.current.add(notification.id);
902 const appIsForegrounded = document.visibilityState === "visible" && document.hasFocus();
903 if (notification.audience !== "background" || appIsForegrounded) {
904 continue;
905 }
906 void showNotification({
907 title: notification.title,
908 body: notification.detail,
909 }).catch(() => {
910 // The in-app notification remains available; OS delivery is best effort.
911 });
912 }
913 }, [notifications, windowMode]);
914 
915 return (
916 <>
917 {windowMode === "quick-chat" ? (
918 <QuickChatShell init={init} ready={ready} startupError={startupError} />
919 ) : windowMode === "utility" ? (
920 <MenuBarUtilityShell init={init} ready={ready} startupError={startupError} />
921 ) : windowMode === "canvas" ? (
922 <div
923 className="relative flex h-full w-full flex-col bg-canvas text-canvas-foreground"
924 data-canvas-surface={canvasSurfaceKind}
925 >
926 <div className="flex-1 min-h-0 min-w-0">
927 <InlineErrorBoundary label="This canvas couldn't be rendered.">
928 <Canvas path={canvasWindowPath} />
929 </InlineErrorBoundary>
930 </div>
931 </div>
932 ) : view === "settings" ? (
933 <div className="app-shell app-shell--settings flex h-full min-h-0 flex-col text-foreground">
934 <div className="app-window-drag-strip" aria-hidden="true" />
935 <div className="min-h-0 flex-1">
936 <SettingsContent init={init} ready={ready} startupError={startupError} />
937 </div>
938 </div>
939 ) : (
940 <ChatShell
941 init={init}
942 ready={ready}
943 startupError={startupError}
944 bootstrapLoading={bootstrapPhase === "loading"}
945 bootstrapStage={bootstrapStage}
946 />
947 )}
948 <LmStudioStartDialog />
949 {windowMode === "main" &&
950 !(canvasEnabled && filePreviewPath && isCanvasSupportedFile(filePreviewPath)) ? (
951 <FilePreviewModal />
952 ) : null}
953 {windowMode === "main" ? (
954 <CommandPalette open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen} />
955 ) : null}
956 {windowMode === "main" ? <DesktopOnboarding /> : null}
957 {windowMode === "main" ? <TranscriptDeliveryRecovery /> : null}
958 {windowMode === "main" ? <InAppToasts /> : null}
959 </>
960 );
961}
962 
963export default function App() {
964 return (
965 <OverlayStackProvider>
966 <AppContent />
967 </OverlayStackProvider>
968 );
969}
970