import { useEffect, useMemo, useState } from 'react'; import { AppShell } from '@renderer/components/AppShell'; import { ActivityPanel } from '@renderer/components/ActivityPanel'; import { ChatPane } from '@renderer/components/ChatPane'; import { NewSessionModal } from '@renderer/components/NewSessionModal'; import { SettingsPanel } from '@renderer/components/SettingsPanel'; import { Sidebar } from '@renderer/components/Sidebar'; import { applySessionEventActivity, pruneSessionActivities, type SessionActivityMap, } from '@renderer/lib/sessionActivity'; import { applySessionEventWorkspace } from '@renderer/lib/sessionWorkspace'; import { WelcomePane } from '@renderer/components/WelcomePane'; import { getElectronApi } from '@renderer/lib/electronApi'; import type { SidecarCapabilities } from '@shared/contracts/sidecar'; import { buildAvailableModelCatalog, findModel, normalizePatternModels, resolveReasoningEffort, } from '@shared/domain/models'; import type { PatternDefinition } from '@shared/domain/pattern'; import { isScratchpadProject } from '@shared/domain/project'; import { applyScratchpadSessionConfig } from '@shared/domain/session'; import type { WorkspaceState } from '@shared/domain/workspace'; import { createId, nowIso } from '@shared/utils/ids'; function createDraftPattern(defaultModelId: string, defaultReasoningEffort: PatternDefinition['agents'][0]['reasoningEffort']): PatternDefinition { const timestamp = nowIso(); return { id: createId('custom-pattern'), name: 'New Pattern', description: '', mode: 'single', availability: 'available', maxIterations: 1, agents: [ { id: createId('agent'), name: 'Primary Agent', description: 'General-purpose assistant.', instructions: 'You are a helpful coding assistant working inside the selected project.', model: defaultModelId, reasoningEffort: defaultReasoningEffort, }, ], createdAt: timestamp, updatedAt: timestamp, }; } export default function App() { const api = getElectronApi(); const [workspace, setWorkspace] = useState(); const [error, setError] = useState(); const [sidecarCapabilities, setSidecarCapabilities] = useState(); const [sessionActivities, setSessionActivities] = useState({}); const [isRefreshingCapabilities, setIsRefreshingCapabilities] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showNewSession, setShowNewSession] = useState(false); // Load workspace on mount useEffect(() => { let disposed = false; void api .loadWorkspace() .then((ws) => !disposed && setWorkspace(ws)) .catch((e) => !disposed && setError(e instanceof Error ? e.message : String(e))); void api .describeSidecarCapabilities() .then((capabilities) => !disposed && setSidecarCapabilities(capabilities)) .catch((e) => { if (!disposed) { console.warn('Failed to load sidecar capabilities', e); } }); const offWorkspace = api.onWorkspaceUpdated((ws) => { setWorkspace(ws); setError(undefined); setSessionActivities((current) => pruneSessionActivities( current, ws.sessions.map((session) => session.id), ), ); }); const offSessionEvent = api.onSessionEvent((event) => { setWorkspace((current) => applySessionEventWorkspace(current, event)); setSessionActivities((current) => applySessionEventActivity(current, event)); }); return () => { disposed = true; offWorkspace(); offSessionEvent(); }; }, [api]); // Derived state const selectedSession = useMemo( () => workspace?.sessions.find((s) => s.id === workspace.selectedSessionId), [workspace?.selectedSessionId, workspace?.sessions], ); const projectForSession = useMemo( () => selectedSession ? workspace?.projects.find((p) => p.id === selectedSession.projectId) : undefined, [selectedSession, workspace?.projects], ); const availableModels = useMemo( () => buildAvailableModelCatalog(sidecarCapabilities?.models), [sidecarCapabilities?.models], ); const patternForSession = useMemo(() => { if (!selectedSession) { return undefined; } const basePattern = workspace?.patterns.find((pattern) => pattern.id === selectedSession.patternId); if (!basePattern) { return undefined; } const patternWithSessionConfig = projectForSession && isScratchpadProject(projectForSession) ? applyScratchpadSessionConfig(basePattern, selectedSession) : basePattern; return normalizePatternModels(patternWithSessionConfig, availableModels); }, [availableModels, projectForSession, selectedSession, workspace?.patterns]); const activityForSession = useMemo( () => (selectedSession ? sessionActivities[selectedSession.id] : undefined), [selectedSession, sessionActivities], ); const hasUserProjects = useMemo( () => (workspace?.projects.some((project) => !isScratchpadProject(project)) ?? false), [workspace?.projects], ); // Loading state if (!workspace) { return (
Loading workspace…
); } const refreshCapabilities = async () => { setIsRefreshingCapabilities(true); try { const capabilities = await api.refreshSidecarCapabilities(); setSidecarCapabilities(capabilities); } finally { setIsRefreshingCapabilities(false); } }; // Determine main content let content: React.ReactNode; let detailPanel: React.ReactNode | undefined; if (error) { content = (

Something went wrong

{error}

); } else if (selectedSession && patternForSession && projectForSession) { content = ( api.sendSessionMessage({ sessionId: selectedSession.id, content: c })} onUpdateScratchpadConfig={(config) => api.updateScratchpadSessionConfig({ sessionId: selectedSession.id, model: config.model, reasoningEffort: config.reasoningEffort, }) } availableModels={availableModels} pattern={patternForSession} project={projectForSession} session={selectedSession} /> ); detailPanel = ( ); } else { content = ( void api.addProject()} onNewSession={() => setShowNewSession(true)} onOpenSettings={() => setShowSettings(true)} /> ); } // Settings overlay const overlay = showSettings ? ( setShowSettings(false)} onDeletePattern={async (id) => { await api.deletePattern(id); }} onNewPattern={() => { const defaultModel = availableModels[0] ?? findModel('gpt-5.4', availableModels) ?? findModel('gpt-5.4'); return createDraftPattern( defaultModel?.id ?? 'gpt-5.4', resolveReasoningEffort(defaultModel, 'high'), ); }} onRefreshCapabilities={refreshCapabilities} onSavePattern={async (pattern) => { await api.savePattern({ pattern }); }} patterns={workspace.patterns} sidecarCapabilities={sidecarCapabilities} /> ) : null; return ( <> void api.addProject()} onNewSession={() => setShowNewSession(true)} onOpenSettings={() => setShowSettings(true)} onProjectSelect={(projectId) => { void api.selectProject(projectId); }} onSessionSelect={(sessionId) => { void api.selectSession(sessionId); }} onRenameSession={(sessionId, title) => { void api.renameSession({ sessionId, title }); }} onDuplicateSession={(sessionId) => { void api.duplicateSession({ sessionId }); }} onSetSessionPinned={(sessionId, isPinned) => { void api.setSessionPinned({ sessionId, isPinned }); }} onSetSessionArchived={(sessionId, isArchived) => { void api.setSessionArchived({ sessionId, isArchived }); }} onRefreshGitContext={(projectId) => { void api.refreshProjectGitContext(projectId); }} workspace={workspace} /> } /> {showNewSession && ( setShowNewSession(false)} onCreate={(projectId, patternId) => { setShowNewSession(false); void api.createSession({ projectId, patternId }); }} onTogglePatternFavorite={(patternId, isFavorite) => { void api.setPatternFavorite({ patternId, isFavorite }); }} patterns={workspace.patterns} projects={workspace.projects} /> )} ); }