Desktop system-tray behavior is opt-in through Settings → General → Keep running in background. Enabling it creates an OS tray with explicit Open CrewCode and Quit CrewCode actions; closing the window hides it without stopping terminals or agents, while tray Quit follows the normal cleanup path. Windows and Linux also restore on tray click (Linux StatusNotifierItem Activate is typically a single click). Disabling the preference removes the tray immediately. macOS retains its Dock icon, and web/Hub/headless runtimes never emulate the tray. See docs/system-tray.md.
Real agent integration is wired through normalized bridges (pi, OpenCode, Claude, Codex, Hermes, CrewCoder, Ollama, and OpenRouter) with PTY panes remaining available for terminals.
The CrewCode app menu includes an About CrewCode card with the running app
version and build commit. Its menu-header version, the About card, and Settings
Updates all share the runtime appBuildInfo source. Electron uses
app.getVersion(); browser clients read the serving Brain/server build info
instead of displaying a renderer placeholder. See docs/about-crewcode.md.
Codex usage reporting prefers the standalone npm CLI at
~/.codex-cli-npm/bin/codex when present, then resolves managed npm
installations through CODEX_MANAGED_PACKAGE_ROOT. It accepts both the legacy
rateLimits response and the newer rateLimitsByLimitId.codex app-server
bucket. The Workspace Dock shows an em dash while usage is unavailable or
fetching instead of presenting missing data as 0%. When a development build
is started from inside a managed CrewCoder Codex turn, its standalone usage
probe removes the parent turn's private Codex runtime identity before launch so
the CLI reads the user's normal Codex account state rather than CrewCoder's
app-server state directory. The dock also recognizes legacy persisted CrewCoder
sessions whose Codex GPT model id predates the current codex:model namespace.
Workspaces, worktrees, git operations, terminals, settings, and crew sessions are all real and persisted (workspaces + tabs to disk, messages to localStorage).
An enrolled desktop can enable Settings → Desktop & Web → Background Brain. The
optional detached Brain becomes authoritative for Brain-authorized workspaces, rich
transcripts, replay/native-resume state, terminals, agents, and the bounded workspace/
chat catalogue used by both Electron and Hub web. First enable seeds missing Electron
state without overwriting existing Brain data and aliases legacy thread: replay into
the shared web: scope. Normal desktop close leaves the Brain and remote availability
running; Stop Brain and Quit and stop Brain explicitly terminate it.
Desktop and web may submit prompts concurrently. Stable bridge starts are coalesced,
and the Brain serializes one conversation FIFO while allowing different conversations
to execute in parallel. Transcript saves merge divergent client snapshots so one
client cannot erase a turn already observed from the other. Catalogue hydration occurs
before app mount and later local edits push bounded per-key patches; live navigation
pull between already-open clients remains a first-release limitation. Files stay on
the enrolled machine, Hub relay traffic remains end-to-end encrypted, and Hub identity
still cannot widen Brain-local roots/scopes. A pre-React startup surface reports Brain
probe, attachment, and hydration phases during the enable-triggered reload instead of
leaving the desktop window blank. Desktop & Web Settings probes the enrolled Hub, shows
its observed canonical browser/passkey origin with an Open Hub action, and states
that enabling Brain does not start the separate Hub service. The enrollment address is
never treated as proof of browser origin or reachability. Foreground and Electron
Background Brain modes share the default owner-only machine credential, while the Hub
keeps a phone signed in across browser and Brain restarts through a revocable 30-day
HttpOnly session cookie whose secret is stored only as a digest. See
docs/desktop-web-continuity.md.
Settings → General stores a workspace-scoped default branch. The selector detects local branches from the active repository. A new solo-chat session captures that setting once, reuses or creates the branch worktree, selects it for that chat surface, then clears the one-shot request so existing chats and later manual branch switches are never moved retroactively. Git Workspace and Git Sidebar also use the live setting as their comparison base without checking it out: committed branch differences remain reviewable alongside local status, while only true working-tree rows expose stage/unstage actions. Delegated threads retain their separate base/worktree contract.
Git Workspace and Git Sidebar keep pull-request creation, browsing, review, management, checks, and merge operations inside CrewCode. Creation is a single-PR Branches → Details → Review flow with measured base-relative ahead/behind/file/conflict evidence. The canonical PR Browser selects a current-branch PR when present, filters its bounded catalogue locally, and shows the observed GitHub author and exact creation time. Its middle pane provides Overview with explicit Description/Problem/What changed/Why it changed/Solution sections, Timeline with chronologically observed open/commit/comment/review evidence, Code changes with an on-demand real per-file PierreDiff, and Checks with on-demand suites, jobs, steps, bounded annotations and logs, rerun controls, and an observed merge-requirements ledger; absent authored rationale is labeled missing, never fabricated. Code changes uses GitHub's combined base-to-head diff rather than its format-patch mail series, removes mail envelopes defensively, and normalizes repeated paths to one canonical file patch before Pierre. It supports exact path/side/commit/line inline-comment drafts, atomic multi-comment review submission, observed review conversations with authorized resolve/reopen, GitHub-backed viewed files with an explicit local-only fallback, next/unreviewed navigation, and files/commits since the viewer's last completed review. The Browser inspector owns title/Markdown editing, reviewer/assignee/label management, overall review submission, update-branch, draft/ready transitions, conflict preparation, confirmed close/reopen, head-pinned merge commit/squash/rebase, auto-merge, and merge queues. Author, label, base, head, request, and decision filters operate on the one bounded catalogue; repository-scoped selection, filters, tab, and file restore when returning. Identity/branch copy controls and observed head-commit/update evidence remain in app. It locks PR selection and navigation during mutations, captures the exact target, revalidates the head before privileged review/check/merge actions, and reloads GitHub evidence before reporting the observed result. Git Sidebar's compact current-branch PR summary opens this same workspace; the redundant standalone review shell is retired. GitHub remains authoritative for repository rules and third-party check capabilities. Brain/web reads stay registered-root and workspace:read scoped while mutations require workspace:write. See docs/github-pull-requests.md.
PR authorship now loads the author's public GitHub profile image through the trusted main/Brain boundary. The host restricts redirect destinations and bounded raster content to GitHub-controlled hosts, caches successful images by username, and gives the renderer a data URL; rejected or unavailable images retain the GitHub-mark fallback. The Overview ends with observed GitHub comments and submitted review summaries after the description sections, including author, state, exact timestamp, and Markdown body.
When the active installed provider is CrewCoder, the desktop model-row reveal offers session-scoped Configured default/general/crewcoder/plugin/extension profiles. Configured default omits the launch flag; a concrete choice adds CrewCoder's distinct --mode, restarts only that bridge before native resume, locks CrewCode's underlying permission policy to Build, and disables Ask/Plan/Build/Full on desktop and phone until Configured default is restored. The concrete crewcoder profile also reveals a session-scoped approval picker for review, always, never, full-access, and sandboxed. Review is the fail-closed default; Always prompts for every non-safe call, Never suppresses prompts but blocks dangerous calls, Full access accepts every call without prompting, and Sandboxed suppresses prompts while applying CrewCoder's sandbox policy to non-dangerous calls where supported. Missing or invalid values fail closed to review, changes restart only an idle bridge, and execution custody includes the native approval value. Full access explicitly bypasses CrewCoder approval requests and dangerous-command blocking, so CrewCode's permission overlay/tripwire cannot interpose. Never pass CrewCode's execution-mode value as the CrewCoder profile, and never let a concrete profile inherit a hidden prior Ask, Plan, or Full Access policy. The crewcoder profile's inspect/clarify/propose/approve-plan gate is CrewCoder-owned: CrewCode renders crewcoder_clarify and crewcoder_propose_plan on the activity overlay and sends /approve-plan as a prompt from the overlay button or slash command. Do not reuse tool-permission Allow/Deny for that card, and do not treat a clarification answer as plan approval.
CrewCoder is a first-class ACP provider implemented separately in crewcoder-bridge.ts; CrewCode is the client and spawns crewcoder acp with the session's normalized native approval value. Keep Hermes untouched. CrewCoder is native-resume, discovers provider:model choices through session/new, maps namespaced usage lastInputTokens to live context occupancy, and reports authoritative compaction lifecycle through _crewcoder/compaction_update (never duplicate it with usage-drop inference). When initialize._meta["crewcoder/sessionCompact"] advertises the exact session/compact method, manual /compact must call it while idle, replace only CrewCode's replay shard with the returned authoritative summary, retain the full display transcript and durable session/bridge, and clear stale context occupancy only when compaction was applied; older agents keep the summary-reset fallback. Idle compaction events must not fabricate a model turn. CrewCoder uses once-only permission choices so remembered agent decisions cannot bypass later composer-mode changes. Its prompt watchdog measures ACP inactivity, not total turn duration, and pauses while Build permission is awaiting user input; a genuine timeout must send session/cancel before CrewCode ends the turn so another prompt cannot overlap live CrewCoder work. A closed CrewCoder ACP child is removed from the bridge registry so the next composer submission follows the existing missing-bridge restart path instead of writing to dead stdin and reporting process not writable. CrewCoder ACP must respect CrewCoder's persisted autoCompact setting; CrewCode must not force compaction or retry context-window failures for CrewCoder, Pi, or other providers. ACP Internal error responses can carry the actionable CrewCoder failure in error.data.message, which the bridge must prefer over the generic envelope text. Local ACP file reads currently use saved disk bytes while SSH reads/writes route through SFTP; do not claim dirty editor-buffer support until a renderer-host route exists. Session-scoped externalDirectories are synchronized after ACP new/load through session/set_external_directories, including [] to revoke stale native-session grants; changing them must restart the bridge. CrewCoder validates and persists the roots, while CrewCode's picker remains unavailable for SSH roots. It is deliberately excluded from disposable editor completion. Every bridge-backed solo, crew-lane, or supervisor dispatch creates a CrewCode-owned activity record whose pending/running/tool-phase/terminal state comes only from observed bridge events; runtime loss becomes interrupted. Optional CrewCoder crew-tasks snapshots on ACP rawOutput enrich that row while active, and newer Task* mutations fold over the current turn's last snapshot. Project-wide TaskList results remain excluded because they mix unrelated CrewCoder sessions. CrewCode does not prompt for or fabricate native activity and does not enable disabled CrewCoder tools. See docs/agent-activity-overlay.md. See docs/crewcoder-provider.md.
Grok Build is a third ACP provider implemented separately in grok-bridge.ts; CrewCode is the client and spawns grok agent stdio. Keep Hermes and CrewCoder untouched. Its defining constraint is that Grok resolves its own permission mode from ~/.grok/config.toml, project .grok/config.toml, and Claude-compatible .claude/settings.json — a user with permission_mode = "always-approve" gets a session that never asks the client and silently runs tools, voiding Ask/Plan/Build. --permission-mode is therefore always passed, is a top-level flag that must precede the agent subcommand (agent stdio accepts none of these flags), and maps ask/plan/read-only to dontAsk, build to default, full to bypassPermissions. Never let Grok's config decide the mode, and never send _meta.full_access on session/new — that field only escalates. The mode is a floor, not the enforcement: dontAsk was measured to still prompt for a client-side write rather than auto-deny, so the real gates are CrewCode's own permission-request refusal and its fs/write_text_file refusal (Grok delegates writes to the client, making that call the last gate before disk), plus a tool-announcement gate that trusts _meta['x.ai/tool'].read_only and falls back to a name check so unknown mutating tools fail closed. Grok's native plan mode must not be used for enforcement — always-approve stays armed underneath it and only edits are blocked, so bash still auto-runs; this mirrors the existing Claude rule. Permission choices are once-only: allow-edits-session is filtered out by ACP kind (never by vendor id spelling) because a session-scoped grant outlives the turn, leaving turn grants to turn-permission-grants.ts. Token usage arrives only on the vendor _x.ai/session_notification channel as response_completed, so a standard-only ACP client renders a dead context meter; per-turn _meta.inputTokens is live context occupancy while _meta.usage.* is cumulative across model calls and must never be read as occupancy. Reasoning effort is Grok's low|medium|high with no off: the picker shows exactly those three using Grok's own wording, and the bridge additionally clamps defensively because an unknown value is rejected for the whole process; effort is spawn-time, so changing it restarts the bridge. Grok numbers its server→client requests from 0, so id presence must be tested with typeof id === 'number', never truthiness. Model discovery reads the catalog off the initialize response and creates no session. Grok todo activity uses the settled todo_write result.TodosUpdated.state.todos map as the full session store; merge arguments and TodosUpdated.todos are a subset and must fold, not replace. Follow-ups queue locally in the bridge (Claude-style) rather than through Grok's own upstream prompt queue, because upstream queueing gives up composer-visible pending items and cancellation while behaving identically otherwise; drainFollowUps() must run after endTurn() since the drain is gated on currentTurnId being cleared, and abort/stop clear the queue. session/prompt returns as soon as the turn opens; the drawer Running shortcut follows runningByScope, not the later turn_end. Terminal capability, slash commands, and --deny rules are deliberately not wired. See docs/grok-provider.md.
Workspace folder actions are state-aware: a project's context menu always offers Create folder, while Move to folder appears only after at least one destination folder exists. The workspace drawer keeps all workspace groups above global Working/Completed/Terminals activity, then renders one Threads section scoped to the selected workspace; never reintroduce per-workspace inline session expansion. Every section has a semantic icon. Local workspace rows abbreviate the exact OS home-directory prefix with ~, retain the absolute path as hover text, and leave SSH roots unchanged. The Running shortcut is live bridge activity keyed by conversation scope in bridge-activity-store (runningByScope); the drawer subscribes to that map itself so ACP providers that acknowledge prompt() before turn_end (Grok, CrewCoder, Hermes) appear while the turn is in flight, not only after it completes. Completed activity is a transient shortcut: hide it one hour after completion without deleting or archiving the underlying session or transcript. Session pins persist on Session and sort stably before unpinned peers inside the existing normal or delegated group; pinning must not erase delegated provenance or make duplicates inherit a pin.
Crew Surface lane model toggles are run-selection gates: use lanes can receive supervisor delegation, shared broadcasts, and direct sends; skip lanes stay visible in the UI but are hidden from supervisor prompts and must not be started by automated fan-out.
Supervisor reporting is incremental, not batched: useCrewSupervisor feeds each worker's reply back to the supervisor as soon as it finishes (feedSupervisor), gated only by a busy flag so it never prompts over an in-flight supervisor turn (replies that land mid-turn buffer and drain on turn_end). A fast worker is reported immediately instead of waiting for the slowest — critical under split distribution where workers finish at very different times. The MAX_SUPERVISOR_ROUNDS budget now counts only fresh delegations, not report drains, so incremental reporting can't exhaust it; the idle watchdog force-feeds (even with no replies) so an abandoned worker still gets reported. Supervisor bridges start with toolPolicy: 'read-only' and Ask-mode behavior; supported bridge providers must block write/exec tools for supervisors even if a normal chat bridge could use them.
Task distribution (session.distribution, default split) is a live header toggle separate from mode. split = each worker gets a distinct sub-task: the supervisor's per-turn run-selection snapshot carries distributionDirective(), and validateDirectivePolicy() hard-blocks "to":"all", targets that resolve to multiple workers, unavailable/skipped targets, and exact duplicate task text before dispatch. No-supervisor shared mode renders one composer per worker in CrewTimeline, and split timeline rounds show each lane's own prompt inside that lane card rather than collapsing to one shared prompt. broadcast = same message to all (handleBroadcast) and the supervisor may use "to":"all". set_distribution is legal at any phase, so it can flip mid-run.
Crew control stops are scoped intentionally: stop all in CrewSurface aborts every runtime, the supervisor composer stop button calls abortSupervisor() only, and lane composer stop buttons call restartLane() for that lane only. A lane stop must release bridge runtimes through useBridgeRegistry.dropBridge() rather than raw stop IPC so the cached (tab, agent) id is removed; lane prompt dispatch also replaces and retries a bridge not found runtime once, re-priming the replacement before sending. The per-lane enabled/paused switch is durable: pausing releases that runtime while retaining the worktree, transcript, and editable nextAction; every assignment seeds the checkpoint, restart recovery preserves it, and supervisor status snapshots include paused lanes without making them delegation targets. The supervisor sidebar width is local UI state in CrewSurface and is resized with the shared Splitter; the supervisor thread also has a scroll-to-bottom affordance. Shared timeline lane groups (crew-lane-group) are locally collapsible with chevrons so dense multi-agent rounds remain scannable. Every direct lane composer lazily supports @ file search scoped to that lane's effective worktree/base path and auto-grows from one line to a bounded 220px before scrolling. Combined-check processes carry durable custody tokens and PID evidence; restart reconciliation reports them as running, exited, or unresolved and gates a replacement verification accordingly. A crash during apply is called applied only after validating the attached base checkout, exact HEAD, clean index/worktree, and absence of unfinished merge state.
Isolated crew review treats clean Git merges as unverified. Cross-lane Diff shows branch/commit ownership, runtime state, exact file overlap, and narrow cross-file contract heuristics; the crew merge sidebar requires explicit acknowledgment when signals exist. Crew sessions persist locally so restart retains lane/worktree provenance while clearing process-local runtime ids. Compare/Merge Git fetches are keyed to a stable ownership fingerprint rather than the once-per-second runtime/usage updates, preventing loaded review evidence from flashing back to a loading state. A merge audit is written before Git starts and recovers a leftover running operation as interrupted. After a clean merge, the sidebar discovers and visibly presents allowlisted package typecheck/test scripts; main runs only those discovered ids with CI=1, and persists pass/fail/interrupted evidence for local and SSH workspaces. See docs/behavioral-merge-review.md.
Delegated threads let a solo-chat agent spawn real, persistent chat sessions through a loopback HTTP API (per-session bearer token, ephemeral port, useDelegatedThreads answering marshalled IPC in the renderer). mode is permissions and isolation is placement — they are independent, and build + shared is the legitimate "run the test suite in this checkout" case; never re-couple them. Depth-1 must stay enforced at three layers, not by withholding credentials alone: canSessionDelegate() (origin !== 'delegated') gates the ChatPane toggle, credential minting in useSessionDelegation, and — authoritatively — every route in useDelegatedThreads.perform() with a 403, so a leaked token cannot read siblings or merge their branches either. The bearer token is the caller's identity, so containsDelegationToken must reject any create prompt or thread message carrying a live token (checked against every registered token, not just the caller's); refuse rather than redact, because a 64-hex string is never legitimate task text. An agent must never be able to archive a chat. POST /v1/threads/:id/close sets Session.delegationClosedAt, which frees a concurrency slot and dims the drawer row; it must never call setArchived, because archived sessions vanish from every live surface and only the user decides they are finished with a thread. Sending a closed thread a message reopens it, focus works on a done thread and refuses an archived one, and closed in the API projection means either state because both free a slot. Reporting is push, not poll, and is modelled directly on the crew Supervisor loop: useDelegationReports subscribes to the bridge registry's subscribeTurnEnd and routes a finished worker's bounded final reply into the parent's running turn as a follow-up, wakes an idle parent with a fresh turn, or buffers it in delegation-inbox-store.ts for the parent's next prompt; every failure falls back to buffering so a report is never dropped. Reports must be coalesced, never delivered one wake per thread — waking costs a full parent turn at that chat's whole context, so a fan-out finishing together gets one wake carrying every report (REPORT_COALESCE_MS), and a delivering flag must be claimed synchronously before the first await (crew's busy) or parallel workers all see an idle parent and fire concurrent prompts at one bridge. Autonomous waking is settings.wakeParentOnDelegatedReport (default on) and is bounded by MAX_AUTONOMOUS_WAKES = 4, which counts recursion, not volume: a wide fan-out from a user-driven turn is free however many threads it contains, while a wake caused by threads spawned during an autonomous turn (Session.delegatedDuringWake, stamped at spawn) costs one. Only a real user message refills the budget (an auto-wake bypasses the send path, so it cannot refill its own), follow-ups into a running turn never spend it, and an exhausted budget must say so in the transcript. A finished thread must be re-read on a settle retry (REPORT_SETTLE_ATTEMPTS × REPORT_SETTLE_INTERVAL_MS, mirroring crew's reportLaneReply) before its reply is extracted — turn_end fires before the renderer flushes stream buffers, so a synchronous read captures the agent's opening line and none of its result. One logical turn must report exactly once: useAgentBridge routes turn_end, error and closed through the same callback, so the guard is a consume-once reported set released only by the child's next turn_start (crew's waiting.delete), never reply-text comparison — the settle retry guarantees an early and a late event see different text, so text dedup silently delivers every report twice. Session.delegationRunId is the cohort stamped at spawn so a batch can report "1 of 3 done — do not summarize as finished yet"; it is unrecoverable after the fact, so never try to infer it at report time. The idle watchdog (shouldAbandonThread, mirroring crew's shouldAbandonRound, fed by subscribeActivity + nextToolsInFlight) reports a thread that is still running with no tool open and no bridge event for 3 minutes — it must report, never close or archive, and must keep the tool-in-flight gate or a single long tool call looks identical to a hang. A wake must revive a bridge reclaimed by the 10-minute idle sweep (ensureBridgeForSession), and both the preamble and the woke report block must tell the agent the user is not present so an auto-started turn never ends in a question. Every report also lands as a visible system row in the parent transcript, is framed as <system> (never user text), is clamped to 1,600 chars, caps at 12 buffered per parent, and drains exactly once at wire-text assembly. See docs/delegated-threads.md.
Chat archiving (Session.archived) is non-destructive: archiving releases the session's bridge but must never delete its transcript, and only explicit Delete calls transcripts:remove. Archived sessions are hidden from every live surface because getSessions() filters them — use getAllSessions() only for the archive list itself. Activation must never land on an archived session, and a tab whose sessions are all archived is treated as empty so ensureTab seeds a fresh thread (with an id that skips archived ones — reusing an id would alias two threads onto one transcript). The archive/rename right-click menu lives on live drawer session rows; archived chats are not shown in the drawer at all and surface only in the archive tab kind, a cross-workspace review page. Session.createdAt is stamped once, while Session.lastUsedAt advances only when work is sent through the chat; opening, metadata edits, restore, and archive do not touch it. The archive page displays lastUsedAt as MM/DD/YYYY and sorts by it, never by archive time; legacy values are recovered from transcript mtimes when available. Archive retention (settings.archiveRetentionDays, 0 | 30 | 60 | 90, default 0) is a classifier, not a scheduler and continues to use the independent archivedAt: it flags expired chats for an explicit confirmed bulk delete and must never gain a background sweep. A session with no archivedAt is never expired, legacy archived sessions are backfilled to first-launch-after-upgrade rather than zero, and a malformed persisted retention value falls back to Never — enabling a window must not retroactively mark unknown-age history deletable. See docs/chat-archiving.md.
Settings include hideVerboseAgentLogs, which filters thinking/toolcall/worklog rows at the shared Messages renderer. Keep final agent replies, user messages, and important system/status meters visible; do not delete verbose messages from storage just because they are hidden in the UI. Work logs and thinking traces use the utilities-only Tailwind renderer integration while retaining their real message/tool mapping, expansion behavior, diagnostics, diffs, and file actions. During execution, tool runs remain visible in stream order; once a later agent response exists, all earlier tool calls in that turn consolidate into one initially expanded work log directly before the latest response. That final log ends with a responsive strip of every unique file changed during the turn, using aggregate add/remove counts across repeated edits. Clicking a changed-file chip opens that exact turn/file in Turn Changes with the agent-summary/list sidebar closed; row-level filenames still open the editor. The drawer and chips share one aggregation path that retains multi-file provider patches, nested edit payloads, and repeated changes to the same file. Multi-file unified patches are split by file, and repeated same-file provider hunks are merged under one canonical header so each drawer selection remains compatible with PierreDiff instead of falling back to raw lines. Keep layouts bounded on mobile and do not replace real data with template/demo content. See docs/tailwind-renderer.md.
Realtime voice is provider-neutral and off by default. GPT Realtime and xAI Voice adapters remain unavailable until their main-process key is configured; permanent keys must never cross preload, only short-lived client secrets may reach the renderer. The voice model is a narrow conversational controller that can send prompts and read agent status, never a coding agent with direct tool authority. Route requests through the existing session send path, keep one microphone owner across panes in voice-session-store.ts, and speak the complete sanitized prose projection in order—never summarize it or read code blocks, diffs, tables, logs, URLs, or long paths. Local replies exceeding the sidecar's 4,000-character per-request limit must be split at sentence/word boundaries and played sequentially, without weakening that security limit. The active transport and original agent target live in module-owned voice-session-runtime.ts, never in a chat-pane lifecycle: navigation and useVoiceSessionController unmounts must detach only the presenter, keep routing pinned to the session where the user started voice, and continue activity tracking and eventual speech. voice-session-store.ts keeps target and presenter scope separate so the next visible chat can adopt the overlay; non-chat pages may temporarily have no host without ending the turn. The orb is one-turn push-to-talk: every transcribed send_prompt_to_agent request must pause at an editable confirmation before routing; mute transport input during review, keep its output side alive for the later spoken result, and let Cancel close without contacting the agent. After Send, release the voice session only after the spoken coding-agent result finishes, then require another click. While the active coding turn has a pending/running tool call, the orb must remain in waiting and display Running tools rather than presenting a late transport event as a voice error; preserve the underlying transport error so it can surface after the coding turn stops. Its active overlay is anchored to the top edge of ChatHeader, extends downward over only its presenting chat pane, and remains visible for the full turn unless the user hides it. The X must hide only the overlay and leave transport, agent tracking, and eventual speech alive; render a compact phase-aware control that can reopen it. The large orb and Escape still stop the session. Composer dictation is a separate speech-to-text-only control after the branch picker: use the selected provider, insert at the current caret, and never invoke the orb, TTS, agent routing, confirmation overlay, or send; it shares only the global microphone lock. fake is development/test-only. local runs Parakeet TDT 0.6B v2 and Kokoro-82M (am_michael default) in the packaged native Python sidecar: main alone owns its random bearer token and authenticated loopback requests, the renderer must never connect to it directly, it must reject non-loopback binds, and app quit must stop the child. Local inference has persisted Automatic/GPU/CPU placement and a Kokoro speech-speed setting (0.5×–2×, default 1×) shared by orb replies and chat read-aloud; normalize the value in main and validate it again in the sidecar. GPU must fail clearly when CUDA is unavailable rather than silently falling back. When Local is selected, launch-time background warmup loads Parakeet only; clicking dictation or the orb also starts transcription warmup immediately, and dictation must never load Kokoro. An orb turn starts Kokoro warmup only after dispatching agent work so it overlaps the coding turn. Keep the lightweight sidecar alive while unloading Kokoro after 5 idle minutes and Parakeet after 15; after model deletion, garbage collection, and CUDA cache clearing, recycle the managed sidecar only if CUDA still reports a material reserved allocation so process termination guarantees VRAM release. Capability warmups must verify their requested model flag rather than treating /v1/health as model readiness; preserve captured child output in request failures so users never receive an unactionable bare fetch failed. See docs/realtime-voice.md.
Solo Chat selection speech is independent text-to-speech: capture a non-empty selection wholly inside the normal Solo Chat thread at context-menu open, offer Read selection aloud, and use the selected Local/GPT/xAI provider. It must never open the orb, acquire the microphone, contact an agent, sanitize beyond trimming, or appear in Crew, Canvas, or Writer chat. Hosted synthesis reuses the main-owned Voice key; Local synthesis remains authenticated through main. Cap selections at 4,000 characters, replace active selection playback on the next request, and keep off/fake disabled. While synthesis is pending, keep a visible loading indicator after the context menu closes and clear it when playback starts or fails. Enabled providers also expose whole-message playback in the completed agent-message hover footer, capped at the same limit; the copy action belongs in the usage strip after token usage. See docs/realtime-voice.md.
Voice orb start/end shortcuts are component-local keybindings exposed in Settings and keys.json. In split layouts, start must target the focused composer rather than fan out; end must only stop the session that currently owns the voice microphone.
Packaged desktop CrewCode polls GitHub Releases through electron-updater.
Available and downloaded versions show on the global notification bar with the
new version so the user does not have to open Settings to find them; the card
stays until dismissed and opens Settings → Updates on click. Channel and
auto-download policy are pushed to main at App launch, not only when the
Settings Updates section mounts. Dev builds and browser sessions do not poll.
See docs/releasing.md and docs/notifications.md.
Completed-turn desktop notifications use the persisted notificationSound setting: system delegates audio to the OS, bell/ding/knock use the renderer's synthesized tones, and none stays silent. Custom tones must send the native toast with silent: true so users never hear both CrewCode and system audio. Coalesced crew completions play exactly one sound. On Linux, toasts go through a detached notify-send child process — never Electron Notification on the main-process hot path, whose synchronous DBus round-trips froze the app 0.5–1.1s per toast. See docs/notifications.md.
The global notification bar consumes chat errors and agent-exit warnings only
from newly appended live messages. L1/L2 transcript hydration, whole-map
continuity reconciliation, app startup, and session switching must never replay
historical chat records as fresh notifications. See docs/notifications.md.
YuHeard is the terminal-only agent-done channel. It listens on a per-user Unix socket at ~/.crewcode/yuheard.sock (overridable via YUHEARD_SOCKET) and accepts newline-delimited JSON reports from any client — a Claude Code hook, the bin/yuheard CLI, or an auto-wrap shell shim that CrewCode installs per pane. Chat bridge turn-end must not call YuHeard; Solo/Crew chat tabs and their sidecar PTYs are ineligible (tabKindAllowsYuHeard). Those chats keep notificationSound and the existing in-app toast only. For terminal panes the renderer plays a knock sound, shows an in-app toast, and (when unfocused) an OS notification titled "Terminal agent finished". CrewCode does not infer turn-complete from a still-running TUI: BEL / OSC 9 / OSC 777 complete immediately on eligible panes; idle-after-output (~2.8s after a large burst) is armed for agent-tagged panes, after a socket running report, or when the user submits a line whose first command is a known agent CLI. Codex TUI never goes idle and its built-in TUI notify defaults to unfocused-only, so spawn/wrap injects -c notify=[python-or-node, per-pane hook] plus tui.notifications=true, tui.notification_method="bel", and tui.notification_condition="always" instead of editing ~/.codex/config.toml. Fish panes redefine agent functions after rc so a function codex cannot skip the shim; bash drops matching aliases/functions via PROMPT_COMMAND. XTermPane reattach sends the same YuHeard spawn flags as the owner ptyCreate. See docs/yuheard.md.
Streaming answer presentation may use Tailwind utilities, but must remain one lightweight text node while live; do not reintroduce per-word animation or template citations/follow-ups that are not backed by real message data. Completed agent-message Markdown fenced code uses the shared safe Shiki CodeBlock; streaming text and inline code remain lightweight. Shiki and semantic Markdown accents (headings, list markers, emphasis, links, inline code) must use --syntax-* CSS-variable references derived from canonical theme tokens in colors_and_type.css, never a fixed bundled palette, so live theme changes recolor existing messages without re-tokenization. Keep body/list text readable, the 80,000-character fallback, and React-node rendering (no innerHTML). See docs/agent-message-markdown.md.
Agent task activity is provider-native and passive: CrewCode visualizes task/plan events but does not force agents to create them. latestTodoActivity() is one fold for every provider: it reads todos/plan/todoList arrays or keyed maps from args, result, and tool_update metadata, folds sparse merges onto the current turn's last full list, and starts a fresh scope on every user message. Claude SDK task_started/task_updated system messages are projected by claude-bridge.ts into one synthetic claude_tasks toolcall per turn, excluding skip_transcript housekeeping and settling before turn_end; if the SDK omits a terminal update, successful turns finalize running tasks as completed and abort/error cancels them. Treat settled result snapshots as authoritative over streaming args. CrewCoder task tools emit a session todos snapshot on ACP rawOutput when autoSyncTodos is on; the fold prefers that snapshot and otherwise reconstructs optional label-identified crew-tasks TaskCreate/TaskUpdate/TaskDelete mutations. Merge matching full rawOutput.task records into the snapshot and preserve stable id, session display number, subject/description, owner/scope, metadata, dependency edges, and timestamps; render active forms, blocked pending tasks, owners, and completion like the CrewCoder TUI. Only an explicit TaskList({ sessionOnly: true }) may replace the session snapshot; project-wide/default lists are excluded. Grok's full session store is result.TodosUpdated.state.todos; the merge todos array is a subset and must not drop unmentioned tasks. Those task rows stay off the work log. crew-tasks stays opt-in on the CrewCoder side (crewcoder task on). The activity selector powers normal chat plus Crew lane/timeline/supervisor overlays. The global Settings → General → Todo activity preference hides only aggregate todo UI; approvals/questions must render regardless of that preference or todo-card dismissal because they can pause the provider. CrewCoder-mode clarify/plan-approval cards are the same: they stay visible with Todo activity off, send /approve-plan instead of a permission response, and clear after the next local user message. Keep delegated task work-log rows distinct from aggregate todo activity; a TaskSummaryItem may carry the same normalized task record without replacing its generic delegated-tool summary form. See docs/agent-activity-overlay.md.
The floating Tweaks panel owns presentation controls for density and workspace dock placement/sizing only. Embedded terminal-column visibility is controlled by chat header and command actions through showTerminal; it must update every embedded ChatPane (normal/Crew, Canvas chat, and Writer chat) without killing PTY panes. Standalone terminal tabs and explicit Canvas terminal panes remain independent. See docs/tweaks-panel.md.
Queued follow-up messages are also session-scoped. There is exactly ONE follow-up behavior: deliver the message into the running turn at the next safe point (mid-stream), not after the turn fully ends. The app-level value is the single literal streamingBehavior: 'followUp' — do NOT reintroduce a 'steer' option in CrewCode's types/UI; 'steer' is pi RPC's own term and lives only inside pi-bridge. This is a deliberate naming inversion against pi: pi's 'steer' = deliver between the running turn's tool calls before the next LLM call (what we want), while pi's 'followUp' = wait until the agent fully stops (append-after, what we do NOT want). So pi-bridge maps our 'followUp' to pi's 'steer'; forwarding pi's 'followUp' verbatim was the "injects after the last message" bug. Claude's Agent SDK physically cannot inject into a live turn, so claude-bridge buffers the follow-up and runs it as a fresh turn the instant the current turn ends (the only thing it can do) via the queue→drainFollowUps path. While a bridge agent is running, sending with non-empty composer text should submit a follow-up instead of forcing a stop. An accepted follow-up's IPC resolves immediately — the renderer must NOT clear the bridge's running flag on that resolution (turn_end owns it), or the composer flips idle mid-turn and the next send gets rejected. Bridges that queue locally (claude) mirror the queue to the renderer via follow_up_queued/follow_up_removed events (reasons: sent/removed/cleared); the composer shows pending items and can cancel one via bridge:removeFollowUp. Providers that queue upstream (pi) report nothing and cannot un-send. Aborting/stopping a turn deliberately clears the whole queue.
Composer execution modes are ask | plan | build | full, where full is displayed as Full Access. A mode has an optional prompt preamble (buildModePreamble) plus a real per-provider permission policy; the preamble is advisory, not enforcement. Settings customizes the shared prompt text, while Session.modePromptsEnabled controls injection per Solo Chat from ChatHeader; it defaults true, copies on duplicate, and locks after startup context is sent because provider context cannot be revoked. Disabling it must never weaken provider-native permission policy, and skills/delegation context must still be delivered. Build approvals may grant allow all (this turn ONLY) only through main-issued {bridgeId, turnId} capability state: approve the current and later permission requests in that turn, clear on every turn/bridge termination boundary, and never use provider-native remembered choices that could outlive the turn. The wire value is 'full' (renamed from 'yolo'), the display token is Mode = 'Full', and the label is MODE_LABEL = 'Full Access'. Because the wire value changed, persisted sessions and defaultMode can still hold 'yolo', so every read of a persisted mode goes through normalizeModeLevel() (yolo -> full, unknown -> build) — an unrecognized level returns undefined from MODE_FROM_SETTINGS, which crashes the composer mode picker and prepends a literal "undefined" to the prompt. Full Access means every tool is pre-approved: Claude uses bypassPermissions, Codex uses workspace-write + never approvals (deliberately NOT danger-full-access — worktree isolation stays), pi/hermes/CrewCoder auto-accept permission requests, and agents must never ask for tool permission. toolPolicy: 'read-only' always overrides the mode, so supervisor and completion bridges stay read-only even in Full Access, and real AskUserQuestion prompts still surface. See docs/execution-modes.md.
Reasoning effort choices are provider-native: Claude exposes off/low/medium/high/xhigh/max; Codex exposes off/low/medium/high/xhigh/max/ultra; other providers expose off/low/medium/high/xhigh. Claude off must explicitly disable SDK thinking, and its named levels map to SDK effort. Codex xhigh/max/ultra must pass through app-server unchanged rather than silently downgrade. Codex Ultra's automatic delegation is upstream provider behavior—CrewCode does not emulate it. See docs/reasoning-effort.md.
Claude SDK turns omit skills and settingSources so Claude CLI defaults apply (user/project/local settings and native skill discovery, including ~/.claude). Do not pass skills: [] — that hides Claude's skill library from the model. Do not switch to full SDK isolation (settingSources: []) unless CrewCode injects equivalent repo guidance itself. CrewCode .crewcode skills remain a separate on-demand injection. See docs/agent-provider-context.md.
Agent conversation replay history is stored per session under userData/conversations/agent-conversations.<stable-session-digest>.json. Each shard must keep the legacy { conversations: { [sessionId]: messages } } shape with a single session key, so it behaves like a sliced-up agent-conversations.json. Do not reintroduce a single monolithic agent-conversations.json live store; that legacy file is migration input only.
Provider switching mid-chat is context handoff, not true provider state migration. Keep provider-native resume IDs keyed by the provider-specific bridge key, but keep CrewCode's local replay transcript keyed by the chat session scope so a new provider can continue from the existing visible thread. A provider switch with existing messages should show a handoff meter and inject a handoff packet on the next prompt, including workspace metadata plus an AI summary generated by a disposable incoming-provider session even when the target provider has an old native resume id. Bound the transcript before disposable summarization so smaller-context providers do not fail on huge sessions. If disposable summarization fails, fall back to bounded transcript context.
Manual /compact generally uses the disposable-summary pattern: generate an AI summary from a bounded transcript in a temporary session, show the summary in chat, replace CrewCode's local replay history with that summary, clear the provider-native resume id, and start fresh on the next prompt. CrewCoder is the exception when it advertises ACP session/compact: CrewCoder rewrites its durable session in place, CrewCode uses the returned summary for local replay, and the native session id and bridge remain intact.
Manual context handoff is available from the Solo Chat header and /handoff. Its card targets either a new chat (with provider/model/effort selection) or any other live chat shown in the current workspace's Sessions list, across that workspace's chat tabs. Used destinations retain their existing provider/model/effort, owning tab, transcript, and selected worktree. Starting either destination type closes the card immediately, reveals the destination through normal session navigation, and leaves progress or failure visible on its handoff meter; a disposable destination-provider session summarizes the source, the packet is appended to destination local replay history, and the destination native resume id is cleared before continuation. Remote browser handoff now follows the same flow through Brain-local web: conversation shards: source history stays on the Brain, the authenticated owner must own the destination bridge, a running destination refuses mutation, and the updated destination history is replayed exactly once on the next native-provider prompt. See docs/provider-context-handoff.md.
Drawer threads can be dragged from ws-session-list onto a Solo Chat
.chat-pane-row or a terminal pane to join the existing window split group.
Same-tab threads cannot share one active session, so a drop that would steal the
visible pane mints a session viewport tab (sessionOwnerTabId + pinnedSessionId)
instead of re-keying the session or mounting the thread into Workbench. Viewport
tabs must not run ensureTab on their own id. Phone layouts do not drag. A split
tab's close control removes that tab only; it must not dissolve the whole group.
See docs/workspace-session-split.md.
Visible chat transcripts persist in two layers (src/renderer/src/stores/chat-messages-store.ts + src/main/transcript-store.ts). L2 — disk is authoritative and unbounded: one file per scope under userData/transcripts/transcript.<scope-digest>.json holding the full rich Message[], written back on the same settle/debounce cadence and via a synchronous IPC batch (transcripts:saveSyncBatch) on window teardown. L1 — crewcode:messagesByTab localStorage is a bounded, synchronous cache for instant paint on launch; it caps each scope's tail and, on QuotaExceededError, evicts the least-recently-touched scopes so the newest conversation always wins. On launch the store hydrates from L2, backfilling anything L1 evicted. Do NOT treat localStorage as the source of truth or let persist() swallow quota errors silently — that was the original "recent messages vanish on restart" data-loss bug. Growing turns stay memory-only: do not serialize L1 or structured-clone L2 for live thinking/agent/tool rows, because those synchronous renderer costs caused the workspace-wide hitch on structural stream events. A settled scope schedules both layers at bounded idle; pagehide/beforeunload/hidden visibility synchronously flushes even live scopes, which is the durability guarantee. User messages are settled and therefore persist before normal agent work, while a crash may lose only the partial in-progress response. Main's transcripts:save is a last-wins async queue (fsp.writeFile), never a sync write on the IPC handler; the teardown sync batch drops queued payloads for scopes it writes so a stale async write can't clobber it. Transcript mtimes are cached in main after the launch scan and updated on writes/removes—never re-read and JSON-parse every shard per transcripts:mtimes request (that blocked Browser main for 1.2–1.3s on Mission Control tool-state refreshes). Explicit session deletion must also call transcripts:remove; the reconciliation prune must NOT delete disk files.
persist() is a hot path — it runs on every structural message, i.e. every tool call. Per-scope serialization is cached in a WeakMap keyed by the scope's Message[] identity (arrays are replaced immutably, so reference equality proves the tail is unchanged): a write touches one scope but the payload needs all of them, and re-stringifying every scope each time cost 20-28ms per tool call. Do not drop that cache or key it by anything weaker than array identity. It must serialize each changed scope exactly once and assemble the payload by string concat under explicit budgets (MAX_PERSISTED_SCOPES, L1_BYTE_BUDGET, PER_SCOPE_BYTE_CAP, and MAX_PERSISTED_MESSAGES_PER_SCOPE = 60 — sized just above the DOM pager's PAGE_SIZE = 50). Never re-stringify the whole map once per evicted scope: quota is the steady state, so the old snapshot-then-retry loop did O(scopes) multi-megabyte serializations and blocked the renderer for ~2s on every tool call. Quota retries must only re-join already-serialized strings. scopeLastTouched must stay a monotonic counter, not Date.now() — same-millisecond ties let eviction shed the newest conversation instead of the coldest. Quota/eviction behavior is covered in chat-messages-store.test.ts; keep it covered.
Renderer re-render isolation: state that changes at high frequency must NOT live in App.tsx, because App rebuilds the whole tab tree (and, on Workbench, every mounted ChatPane) on each render. Two such slices are now isolated into stores that only their consumers subscribe to — stores/terminal-unread-store.ts (background PTY output; a claude/codex agent in a hidden tab used to re-render the shell ~1.4×/s) and stores/composer-draft-store.ts (per-tab composer drafts; every keystroke re-rendered the shell). Do not move either back into App. Live agent state (running / status / queued follow-ups / pending user requests) lives in stores/bridge-activity-store.ts, not in useBridgeRegistry's useState. It used to ride on the bridges prop, which meant ChatPane's Stop button, spinner, follow-up pills, and permission prompts only stayed correct because that bundle got a fresh object identity on every App render — an accident, not a design, and one no type or test protected. Consumers now subscribe to the slice they read: ChatPane uses useIsBridgeRunning / useBridgeStatus / useQueuedFollowUps / useUserRequestsForTab. useBridgeRegistry still subscribes to runningByBridge / runningByScope (Mission Control fans out over every session via isBridgeRunning), and the workspace drawer also subscribes to runningByScope so Running does not depend on App recomputing workingChats. Status and follow-up churn no longer re-render App. Do not reintroduce getBridgeStatus / getQueuedFollowUps / userRequestsByTab onto the bridges bundle. clearBridges() deliberately does NOT drop user requests — an idle-stopped bridge keeps its tab's requests, while dropBridge/releaseTab/resetSession clear them explicitly; bridge-activity-store.test.ts pins that.
Inactive standalone terminal tabs remain mounted for PTY/process continuity, but their full-size visibility:hidden keepalive containers must not keep xterm/WebGL on the renderer hot path. Pass the explicit active-tab signal through TermColumn to XTermPane; inactive panes buffer a bounded output tail without calling term.write(), then refit and replay with a per-frame budget plus xterm callback backpressure on activation. Visible panes use the same batching so PTY bursts do not become one renderer task per chunk. Bridge text/thinking activity phases are folded into the existing 50 ms delta flush rather than scanning the transcript for every raw provider token. See docs/terminal-stream-performance.md.
Idle refreshes follow the same responsiveness contract. The five-second prompt/skill/command scan is App-owned, so unchanged content must preserve the prior library object and slow scans must stay single-flight; local scan reads use asynchronous filesystem APIs rather than blocking Electron main. The automatic GitHub poll must use asynchronous, bounded child processes and preserve the prior App state for an unchanged result. Never add spawnSync, readdirSync, statSync, or readFileSync to an interval-, focus-, or visibility-driven status path. See docs/terminal-stream-performance.md.
Structural message updates (tool calls and new thinking blocks) must stay isolated from the workspace shell. Messages' areRowsEqual compares a work-log anchor's actual source ToolCallMessage identities; never replace that with blanket live-turn invalidation, because a growing thinking/text sibling would rebuild every work log in the turn. Agent rows receive a precomputed showTurnSummary boolean instead of reading the whole transcript. Self-contained rows — thinking above all — re-render only when their own message changes. chronologicalStreamSegments caches splits per chunk string, so a growing block only re-splits its tail instead of every chunk on every flush (that was quadratic in turn length); it also appends units in a loop rather than push(...units), which throws on very long blocks. Solo-chat auto-follow runs in the next animation frame, not useLayoutEffect: reading scrollHeight synchronously during every React commit forced shell-wide layout. Keep .thread-shell layout/paint-contained so transcript reflow cannot invalidate the workspace drawer.
ChatPane is still not React.memo'd, but that is now a cost/benefit call rather than a correctness trap: MessageRow is already memoized and Messages pages at 50 rows, so an App-driven ChatPane re-render is cheap reconciliation. Memoize only against a profile, never against a theory.
Markdown/text editor drafts in chat and Writer Workspace intentionally persist unsaved buffers in renderer localStorage (markdown-draft-storage.ts) so pane refreshes or tab switches do not make untitled/dirty files disappear. This is draft retention only; do not write files to disk until the user saves or accepts the Writer review.
Writer DOCX/PDF support is a conversion workflow, not native binary editing. Persist one source/working/generated link in app user data, reopen the same sibling Markdown working copy on repeated binary clicks, preserve the original, and review local and agent edits through Pierre. Export only approved text to one stable generated DOCX/PDF derivative; replace that derivative only while its stored content hash proves it is still CrewCode-owned, otherwise fork a collision-safe output. Local agent writes use the editor file watcher; SSH Writer files use bounded polling because remote filesystem events are unavailable. Keep an agent's on-disk candidate separate from any dirty local draft, and never imply layout-perfect round-tripping or OCR support. See docs/writer-document-formats.md.
The code editor's active editing surface is CodeMirror 6 (CrewCodeMirrorEditor) wrapped by CodeEditor, which still owns tabs, file-tree UI, save/format, disk-conflict prompts, plugin editor actions, and its Pierre diff review surface. Open saved files reconcile against disk when the editor mounts, use filesystem events as the local fast path, and retain one bounded single-flight active-file check for missed events and SSH; clean agent edits reload, while a dirty buffer is preserved and marked conflicted only when disk diverges from its original baseline. The lazy file tree keeps dependency, generated-output, cache, and workspace-local Python environment folders browsable on demand in local and SSH workspaces; only .git and .DS_Store stay hidden from direct browsing, while broad search/fallback scans retain their generated-tree exclusions. Its context menu stays cursor-anchored but flips and clamps within the viewport rather than rendering off-screen. File-tree Cut / Copy / Paste use a workspace-scoped in-session clipboard (not the OS clipboard; Copy path still does that). Copy pastes through sandboxed fs.copyFile with a copy suffix on collisions; cut pastes through sandboxed fs.move, dims the source until it lands, and refuses the current parent or a folder into itself. Keep Duplicate as a same-directory sibling copy. Clicking a changed file in Git Sidebar opens the active worktree's single-file patch in that editor diff surface; it must not diff the workspace root when a worktree is selected. Keep high-frequency editor document/selection/autocomplete state inside CodeMirror instead of lifting it into App.tsx. Direct @codemirror/* dependencies use local file: links to the independently cloned package repositories under packages/crew-codemirror; rebuild those sources with npm run codemirror:build, and never run the upstream codemirror:install bootstrap over uncommitted package edits because it hard-resets every child repository. Editor themes come from packages/crew-codemirror/theme-library, use the checked IDs in src/shared/editor-theme-types.ts, and must reconfigure through CodeMirror's theme Compartment so switching palettes never destroys document, history, selection, scroll, or LSP state. Editor file/tree icons use the vendored Bearded Icons assets and mapping under src/renderer/src/assets/bearded-icons; preserve its GPL-3.0 license/attribution, prefer exact filename then compound-extension mappings, and never runtime-reference the gitignored .crewcode/ source. The FileTree Outline must derive active-document symbols inside CodeMirror: prefer hierarchical LSP document symbols for TS/JS, use the bounded local fallback for supported non-LSP languages, reject stale responses by document identity, and keep symbol extraction out of App.tsx. TypeScript/JavaScript intelligence uses one shared @codemirror/lsp-client and typescript-language-server process per workspace. Keep JSON-RPC framing and process ownership in the main process, sanitize LSP Markdown, refuse definition/problem paths outside the workspace sandbox, and launch SSH language servers remotely rather than reading remote projects through local-only paths; remote hosts must provide TypeScript and typescript-language-server, and CrewCode must not install them automatically. The Problems and workspace-search indexes must remain bounded. Code Actions currently apply only validated, non-overlapping edits to an unchanged active document; fail closed on commands, stale responses, malformed ranges, and multi-file edits. LSP rename and workspace replace are preview-first multi-file flows: block affected dirty tabs, reject outside-workspace/stale/malformed edits, verify file snapshots immediately before writing, and roll back completed writes after a later failure. Preserve the same guarantees over SSH. Editor AI completions use a dedicated provider/model setting and disposable agent:completion bridges: bounded context, toolPolicy: 'read-only', thinking: 'off', no persisted conversation/resume state, 20-second timeout, cancellation on every stale edit, and built-in providers only (no plugins/Copilot API). Ghost text must never show model reasoning: disabling provider reasoning is not sufficient, because models that inline <think> blocks into the content stream arrive as ordinary text_delta. Every completion route must normalize through the shared src/main/agents/completion-text.ts (strip reasoning blocks, reject unterminated ones, then unwrap one fence) — do not re-inline a per-provider copy of that logic. Completion-only hosted APIs are deliberately distinct from chat providers: OpenCode Go uses its OpenAI-compatible bearer-key endpoint and OpenRouter reuses its API route; both must remain ephemeral and never write completion content to conversation history. See docs/code-editor.md.
Workbench/Canvas Mode is a fresh workspace surface (kind: 'canvas') for workbench-owned chats and terminals. It must NOT mount live panes from existing app chat/terminal tabs; doing so duplicates heavy streaming/xterm work and can freeze the renderer when multiple agents are active. Workbench pane membership persists in crewcode:workbenchPanesByTab:v1; chat transcripts use the existing chat stores, and terminal metadata uses crewcode:terminalSessions:v1. Workbench-owned panes are allowed because they are created explicitly inside Workbench, but future hardening still needs virtualization, explicit lifecycle controls, and measured terminal mount caps. See docs/canvas-mode.md.
The standalone Git Workspace tab (kind: 'git') is a full-page surface backed by the same useGitSidebar state/actions as GitSidebar. Keep Git Sidebar and Git Workspace behavior consistent; both compare the active worktree to Settings' workspace-scoped default branch when configured, include committed and untracked differences, and expose stage controls only for actual working-tree changes. Page-specific changes should live in GitPage/CSS unless the underlying git behavior truly changes. Push authentication uses a one-shot GitAuthModal plus temporary askpass helper; do not persist Git passwords/tokens unless a dedicated secure keychain flow is added. See docs/git-workspace.md and docs/git-authentication.md.
PromptBuilder (prompts tab), Mission Control (mission tab), Canvas Mode (canvas tab), Code Editor, Git Workspace/Sidebar, and Changes by turn are usable on phones and tablets when CrewCode is reached through a browser context — both the standalone crewcode serve and the Hub-relayed ?machine=… entry points share the same renderer. Layout decisions flow from useMobileLayout() (isMobile = innerWidth ≤ 768, isTablet = 769–1024) so JS branches and CSS breakpoints cannot drift. The Code Editor keeps its canvas primary with an off-canvas file tree, Git Sidebar is a dismissible right overlay from chat or editor, Git Workspace uses a bounded single-column overview/change/diff/tools flow, and Changes by turn is a full-screen stacked catalogue/diff surface. The browser context does not affect the Electron renderer; desktop behaviour is unchanged. See docs/mobile-responsive-pages.md.
- Hub mobile home and desktop overview: an authenticated Hub visit at ≤768px redirects to
/app?hub=mobileand mountsMobileDashboardbefore any Brain relay is opened. It reads the owner name and enrolled-machine presence from the Hub's cookie-authenticated, read-only session/machines endpoints; fake agent/worktree counts are forbidden. Both Hub mobile headers use the supplied theme-aware CrewCode logo assets. Selecting an online machine enters/app?hub=mobile&machine=…and opens a disposable end-to-end encrypted tunnel with onlyworkspace:readandagentrequested.MobileMachineOverviewadapts Brain-visible transcript sessions and live executions into Mission agents, then uses the samederiveMissionStatsaggregation asmc-statsfor agents/running/done and distinctprojectId/worktreetotals; denied or incomplete inputs render as—. It lists at most five recent thread summaries. Current Brains return only each summary's opaque scope id, timestamp, and bounded first-user title seed—not transcript bodies. If an already-running older Brain rejectstranscripts.recentas unsupported, the renderer falls back totranscripts.mtimes; saved rows still appear with timestamps and an untitled label, without downloading full histories. A recent row carries a bounded workspace/tab/session descriptor into/app?machine=…; after transcript hydration the App validates ownership, restores the exact session id when the browser has no local catalog entry, and focuses that thread. Open CrewCode enters the same renderer without a thread target. Navigation closes the overview relay before the full runtime opens. Offline machines remain disabled, and/?hub-admin=1is the explicit escape hatch to passkey/device administration. Desktop Hub visits, tablets above 768px, Electron, and directcrewcode servestartup do not mount either mobile surface. - PromptBuilder: above 768px the left rail uses a bounded
.pb-left→.pb-inner→.pb-listflex-height chain so its header, filters, and footer stay fixed while prompt/skill cards scroll independently. Below 768px the 360px left rail and detail pane share alist | detailnavigation state (driven bydata-viewon.pb). The list is edge-to-edge withmin-width: 0enforced through its container chain; do not restore the old centered85%rail because it wastes phone width and lets intrinsic child widths push the page sideways. The Prompts/Skills tabs and compact New action share one bounded row. Category chips are omitted on phones; only the category-management, favorites, and layout tools remain in the compact toolbar. Prompt cards use phone-specific compact padding and type. Cards are non-shrinking children of the scrollable flex list, grow to their wrapped title and description height, and contain text overflow without line clamps. Detail mode removes the Split choice on phones: a stored desktop Split state resolves to Source, and the source textarea fills the remaining body height; users can still explicitly switch between Source and Preview. The composer PromptPicker has separate Prompts and Skills tabs: prompts insert or open variable fill, while skills toggle session-scoped activation in place without inserting their body or closing the picker. On phones the picker becomes a full-bleed bottom sheet with its side fill panel collapsed to a top section, 44px tabs, and 48px list rows. Actionable controls (icon buttons, mode toolbar, save/apply) retain ≥36px touch targets and gain:activestates mirroring the existing:hoverstyles. Keyboard-hint chips (⌘F,⌘P,⌘J,⌘S) are hidden on phones, and text inputs stay at 16px to prevent iOS auto-zoom. - Mission Control:
deriveMissionStatsis the canonical aggregation for the desktopmc-statsstrip, hero totals, and Hub mobile machine overview; status counts and distinctprojectId/worktreeidentity must not be reimplemented per surface. Below 768px the activity feed moves out of the page into a newMobileShellsheet (mission-activity) driven by a header "Activity" pill in the toolbar.StatStripcollapses from 6 to 3 columns (and to 2 below 480px), the agent grid floor drops to 280px so cards fit at 360px viewport width, theBlockingBannerrow reflows into two rows (body on top, reply input + jump button on the bottom), and the menulet already had a 768px full-bleed override.pluginMissionWidgetsand the in-page side feed are hidden on phones and live behind the sheet. The toolbar is a two-row stack: filter segments alone on the top row, all action controls (Activity pill, group-by select, refresh, spawn) on a second row pushed to the right with the desktop spacer collapsed. - Canvas Mode: below 768px the pane grid
grid-auto-rowsdrops tominmax(min(50dvh, 480px), 1fr)so two panes can share a phone screen. The pane-bar action cluster is replaced by a single⋯overflow button (.canvas-mode-pane-more) that opens a popover menu (.canvas-mode-pane-menu-pop) carrying the per-pane actions: mode prompt toggle (chat only), verbose-logs toggle (chat only), and close pane; Add chat / Add terminal live on the page-level FAB. The popover closes on outside click, touch, and Escape. The "Add" controls inside the desktop cluster were promoted from<div>to<button>for keyboard / screen-reader support.
Each page has a sibling mobile-*.test.ts that pins the responsive behaviour against the CSS and JSX so future edits cannot silently regress the layout. New mobile work must add a matching test under the same mobile-*.test.ts convention. See docs/mobile-responsive-pages.md (if present) for the breakpoint map and class conventions.
crewcode hub --local-brain starts the Hub, waits for the owner passkey, enrolls this host only through an in-process Hub grant, and spawns a sibling crewcode brain. Hub identity storage stays in ~/.crewcode/hub; the machine credential stays in ~/.crewcode/brain. No default RPC scopes. Extra machines still enroll then brain against the Hub origin. Omit --local-brain on a relay-only VPS. See docs/web-remote-access.md.