Renderer Process
React app structure, contexts, components, and hooks
The renderer is a React 19 single-page application built with Vite and Tailwind. It communicates with the main process exclusively via window.cosmoDesktop (the preload bridge). Source lives in packages/desktop/src/renderer/src/.
Entry Point (main.tsx)
Before React mounts, main.tsx reads localStorage for cosmo-theme and cosmo-preset and applies them to document.documentElement. This prevents a flash of unstyled content on load.
PostHog is initialised here with the app's analytics key.
If VITE_CLERK_PUBLISHABLE_KEY is set, the app renders AuthProvider → App. Otherwise it renders DevApp (development bypass mode).
Contexts
AuthContext (context/AuthContext.tsx)
Manages authentication state for the entire app.
| Value | Type | Description |
|---|---|---|
isLoaded | boolean | Auth state has been fetched from main |
isSignedIn | boolean | User is authenticated |
token | string? | Cosmo JWT |
user | AuthUser? | { id, email, name, avatarUrl } |
org | AuthOrg? | { id, name, slug } |
role | string? | OWNER / ADMIN / MEMBER |
plan | string? | Active billing plan |
onboardingCompleted | boolean | Whether onboarding is done |
login() | fn | Calls window.cosmoDesktop.auth.login() |
logout() | fn | Calls window.cosmoDesktop.auth.logout() |
getToken() | () => string | Returns the current token |
On mount it calls window.cosmoDesktop.auth.get() and subscribes to auth:changed events (fired after deep-link login).
Hook: useDesktopAuth()
ChatContext (context/ChatContext.tsx)
The most complex context — manages sessions, streaming, and model selection.
Session state:
| Value | Type | Description |
|---|---|---|
sessions | SessionInfo[] | All chat sessions |
activeSessionId | string? | Currently open session |
messages | DisplayMessage[] | Messages for active session |
isStreaming | boolean | A response is in flight |
streamingText | string | Accumulated text so far |
streamSegments | StreamSegment[] | Typed segments (thinking, text, tool, advisory) |
contextUsage | { usedTokens, maxTokens, percentage } | Token meter |
activeModel | ModelInfo? | Selected model |
availableModels | ModelInfo[] | All available models |
onboardingSessionId | string? | Special onboarding session |
Methods:
| Method | Description |
|---|---|
createSession(opts?) | Create and switch to a new session |
switchSession(id) | Load a different session |
deleteSession(id) | Delete and switch to another |
sendMessage(content) | Send and stream response |
stopGeneration() | Abort the active stream |
switchModel(modelId) | Change active model |
Stream segments are built incrementally as chat:event IPC events arrive:
| Segment type | Description |
|---|---|
thinking | Extended thinking content (shown collapsed) |
text | Plain assistant text (Markdown rendered) |
tool_call | Tool invocation display |
tool_result | Tool execution result |
tool_confirm | Awaiting user approval |
advisory:next_actions | Suggested next steps |
advisory:heads_up | Warning or notice card |
advisory:connector_required | Prompt to add a connector |
When the final text contains [ONBOARDING_COMPLETE], the context strips the sentinel and calls onboarding.handleComplete(sessionId).
Hook: useChatContext()
AppModeContext (context/AppModeContext.tsx)
Provides { isClerkMode, orgId, userId } throughout the app. Set once at startup from AuthContext.
Hook: useAppMode()
ThemeContext (context/ThemeContext.tsx)
Manages visual theme. A pick only updates a draft — it is previewed while the Appearance page is open, but not persisted or kept until saved:
| Value | Description |
|---|---|
theme / preset | The saved mode/preset (what the app shows outside a preview) |
draftTheme / draftPreset | The in-progress pick on the Appearance page; painted onto the app only while previewing |
setDraftTheme(t) / setDraftPreset(p) | Update the draft only — never persists |
beginPreview() / endPreview() | Called by the Appearance page on mount/unmount. While previewing, the draft is painted onto <html>; ending the preview discards an unsaved draft and repaints the saved appearance. If a save is in flight, ending is deferred until it settles, so leaving the page mid-save never flashes the old theme or loses a pick that then saves successfully |
isDirty | true while the draft differs from the applied/saved appearance |
isSaving | A save is in flight |
saveAppearance() | Persists the draft; only on success (confirmed by main) does it become the applied appearance |
justSaved | true once a save is confirmed — drives the success toast |
saveError | Message from the last failed save, if any |
Applies classes to document.documentElement (the draft while previewing, otherwise the saved theme) and syncs PostHog person properties — for the saved theme only, never a preview.
Appearance is per-user (main keys the save on whoever is signed in), and this context re-fetches on auth:changed so a sign-in/out/switch during the same running session picks up the newly-current identity's appearance without an app restart. See the CustomizePage → Appearance note below for the full persistence design.
Hook: useTheme()
Pages
HomePage (pages/HomePage.tsx)
The main authenticated layout. Three panes:
┌──────────────────────────────────────────────────────┐
│ Titlebar (hamburger + window controls) │
├──────────┬──────────────────────┬───────────────────┤
│ │ │ │
│ Sidebar │ ChatPanel │ HubPanel │
│ (52–256px│ │ (resizable) │
│ toggle) │ │ │
└──────────┴──────────────────────┴───────────────────┘- Sidebar collapses to icon-only mode (52 px) on toggle.
- The
ResizeHandlebetween Chat and Hub lets users drag to adjust the Hub width (default 60% of remaining space, min 280 px). - Switching to
'customize'view hidesChatPaneland showsCustomizePage. - Listens for
cosmo:open-integrationscustom DOM event to jump directly to the Integrations tab.
LoginPage (pages/LoginPage.tsx)
Shown when isSignedIn is false. Shows the Cosmo logo and a Sign in with browser button that calls window.cosmoDesktop.auth.login().
In dev mode (DevApp), shows a mock form for local testing.
Key Components
ChatPanel (components/chat/ChatPanel.tsx)
- Renders
DisplayMessagelist with Markdown support. - Shows thinking blocks (collapsible), tool calls, tool confirmation dialogs, and advisory cards.
- Auto-scrolls to the latest message.
- Textarea with auto-resize (max 160 px height), Shift+Enter for newline, Enter to send.
- Context meter at the bottom shows token usage percentage.
- Model selector (hidden when
VITE_HIDE_MODEL_SELECTORis set). - Stop button during streaming.
- Filters
systemrole messages from display.
Sidebar (components/sidebar/Sidebar.tsx)
- New session button at the top.
- Session search input.
- Sessions grouped by time: Today / Yesterday / Older.
- Delete button on session hover.
- Customize toggle button.
- User menu (bottom): profile, usage modal, manage org (opens web), reset onboarding (dev only), sign out.
- Trial reminder card + Upgrade button if on free or trial plan.
- Update card when a new version is available.
HubPanel (components/hub/HubPanel.tsx)
The right-side panel with multiple views:
| View | Description |
|---|---|
| Dashboard | Stat cards (Open Tasks, Due Today, Completed, Workspaces), focus task list, status breakdown chart |
| Tasks | Full task list with filters (All / Mine / Unassigned), show/hide completed |
| Team Hub | Workspaces list, team tasks, member roster |
| Artifact viewer | Embedded <iframe> rendering the HTML artifact |
| Connectors | Browse and add integrations grouped by category |
Auto-navigates to Artifact view when artifacts:created is received.
CustomizePage (components/customize/CustomizePage.tsx)
Two tabs:
- Integrations — Add/remove MCP connectors, see connection status.
- Appearance — Theme (dark/light) + preset picker (chalk, paper, dusk, ledger, atelier, mono). A pick updates a draft selection, which is previewed across the whole app while the Appearance page is open; it is not kept until the user clicks Save (the Save button sits in a footer below the pickers), and leaving the page without saving restores the saved appearance. Save writes to
userData/appearance.json(main process,appearance-settings.ts) rather thanlocalStorage, so it survives sign-out and restarts. It is not guaranteed to reset on uninstall either — the desktop installer keeps userData by default — so in practice it only goes away if userData itself is deleted. Main confirms the write by sending anappearance:savedIPC event back to the renderer; only on receiving that event doesThemeContextapply the new appearance, updatelocalStorage(for the next boot's flash-free bootstrap), and flip the "Saved" indicator — the picker never applies optimistically off thesave()call's own promise. Migration: a user who picked a theme before this feature existed has a value inlocalStoragebut no durable record yet; on first mount, ifappearance:getreturns nothing andlocalStorageholds a real value,ThemeContextseeds the store from it once, so that pick survives the next sign-out instead of silently reverting to the default. Per-user, not per-device: main keys the save on whichever account is signed in —appearance-<userId>.json, alongside a device-levelappearance.jsonused only while signed out. The device file records who saved it (ownerUserId); a signed-in user with no file of their own adopts it only if it is unowned and no per-user file exists on the machine (a genuine pre-per-user save) — otherwise they get the defaults, returned explicitly so the renderer never seeds them from alocalStoragethat may hold the previous user's pick. Two accounts on the same machine never inherit each other's pick, and the renderer re-fetches onauth:changed(a sign-in, sign-out, or account switch within the same running session), so switching accounts without restarting the app still shows the right identity's appearance.
Hooks
useOnboardingChat (hooks/useOnboardingChat.ts)
Controls the onboarding session lifecycle.
| Value / Method | Description |
|---|---|
needsOnboarding | true if user is incomplete, or admin and org is incomplete |
initOnboarding(api) | Creates onboarding session, sends [SYSTEM: BEGIN_ONBOARDING] trigger |
handleComplete(sessionId) | Extracts memory, marks user/org complete, seeds org data. Org seeding runs regardless of whether the user-profile server write was acknowledged (both are idempotent); an unacknowledged write is re-raised as an OnboardingActionError afterwards, and the resume pointer is kept so a retry continues the same conversation |
skipOnboarding() | Mark complete without memory extraction |
The onboarding session ID is persisted to localStorage so the same session is resumed on restart.
useOnboardingStatus (hooks/useOnboardingStatus.ts)
Loads UserProfile and OrgOnboarding from the main process.
| Hook | Returns |
|---|---|
useUserOnboarding(userId) | { isLoading, isComplete, profile, refetch } |
useOrgOnboarding(orgId) | { isLoading, isComplete, data, refetch } |
Onboarding Prompt (lib/onboardingPrompt.ts)
Builds the AI system prompt used in the onboarding session. The prompt:
- Greets the user by name.
- Steps through: welcome → user background → org info (admin only) → pain points → preferences → wrap-up.
- Uses a different flow for org creators vs. invited members.
- Terminates with
[ONBOARDING_COMPLETE]sentinel when complete. - Allows the user to skip at any point.