Cosmo Docs
Desktop App

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.

ValueTypeDescription
isLoadedbooleanAuth state has been fetched from main
isSignedInbooleanUser is authenticated
tokenstring?Cosmo JWT
userAuthUser?{ id, email, name, avatarUrl }
orgAuthOrg?{ id, name, slug }
rolestring?OWNER / ADMIN / MEMBER
planstring?Active billing plan
onboardingCompletedbooleanWhether onboarding is done
login()fnCalls window.cosmoDesktop.auth.login()
logout()fnCalls window.cosmoDesktop.auth.logout()
getToken()() => stringReturns 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:

ValueTypeDescription
sessionsSessionInfo[]All chat sessions
activeSessionIdstring?Currently open session
messagesDisplayMessage[]Messages for active session
isStreamingbooleanA response is in flight
streamingTextstringAccumulated text so far
streamSegmentsStreamSegment[]Typed segments (thinking, text, tool, advisory)
contextUsage{ usedTokens, maxTokens, percentage }Token meter
activeModelModelInfo?Selected model
availableModelsModelInfo[]All available models
onboardingSessionIdstring?Special onboarding session

Methods:

MethodDescription
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 typeDescription
thinkingExtended thinking content (shown collapsed)
textPlain assistant text (Markdown rendered)
tool_callTool invocation display
tool_resultTool execution result
tool_confirmAwaiting user approval
advisory:next_actionsSuggested next steps
advisory:heads_upWarning or notice card
advisory:connector_requiredPrompt 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:

ValueDescription
theme / presetThe saved mode/preset (what the app shows outside a preview)
draftTheme / draftPresetThe 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
isDirtytrue while the draft differs from the applied/saved appearance
isSavingA save is in flight
saveAppearance()Persists the draft; only on success (confirmed by main) does it become the applied appearance
justSavedtrue once a save is confirmed — drives the success toast
saveErrorMessage 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 ResizeHandle between Chat and Hub lets users drag to adjust the Hub width (default 60% of remaining space, min 280 px).
  • Switching to 'customize' view hides ChatPanel and shows CustomizePage.
  • Listens for cosmo:open-integrations custom 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 DisplayMessage list 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_SELECTOR is set).
  • Stop button during streaming.
  • Filters system role messages from display.
  • 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:

ViewDescription
DashboardStat cards (Open Tasks, Due Today, Completed, Workspaces), focus task list, status breakdown chart
TasksFull task list with filters (All / Mine / Unassigned), show/hide completed
Team HubWorkspaces list, team tasks, member roster
Artifact viewerEmbedded <iframe> rendering the HTML artifact
ConnectorsBrowse 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 than localStorage, 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 an appearance:saved IPC event back to the renderer; only on receiving that event does ThemeContext apply the new appearance, update localStorage (for the next boot's flash-free bootstrap), and flip the "Saved" indicator — the picker never applies optimistically off the save() call's own promise. Migration: a user who picked a theme before this feature existed has a value in localStorage but no durable record yet; on first mount, if appearance:get returns nothing and localStorage holds a real value, ThemeContext seeds 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-level appearance.json used 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 a localStorage that may hold the previous user's pick. Two accounts on the same machine never inherit each other's pick, and the renderer re-fetches on auth: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 / MethodDescription
needsOnboardingtrue 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.

HookReturns
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.