Skip to main content

Native desktop Chat

EXEPERT desktop provides one native Rust/GPUI Chat workspace with three isolated sources. EXEPERT uses the same authenticated Supabase and 9Router boundary as browser Chat. Codex is a visibly Experimental, version-pinned client for one owned loopback App Server and one optional remote WSS profile. DSH owns its separately authenticated bundled sidecar and local session catalog. The desktop does not embed a browser, Electron, React, or a WebView.

This page documents the shared native shell and the established EXEPERT Chat surface. The Codex process/protocol boundary, trust model, task lifecycle, and upgrade policy are recorded separately in Native Codex live client.

Change map

ConcernImplementation
GPUI workspace and lifecycledesktop/exepert-desktop/src/app.rs
Chat transport, authentication, persistence, image processing, and wire typesdesktop/exepert-desktop/src/chat.rs
Chat visual primitives and context indicatordesktop/exepert-desktop/src/ui/chat.rs
Shared adaptive transcript reconciliation, scrolling, bookmarks, middle-button auto-scroll, and user checkpointsdesktop/exepert-desktop/src/ui/transcript.rs
Process-local Find, command-palette ranking, and unified attention reducersdesktop/exepert-desktop/src/desktop_discovery.rs
Strict machine-local navigation-density preferencedesktop/exepert-desktop/src/desktop_ui_settings.rs
Native Chat virtual transcript projectiondesktop/exepert-desktop/src/ui/native_chat_transcript.rs
Experimental Codex runtime, protocol, settings, reducer, and diagnosticsdesktop/exepert-desktop/src/codex/
Codex-specific native presentation primitivesdesktop/exepert-desktop/src/ui/codex.rs
Pinned Codex 0.149.1 stable/experimental schemas and integrity manifestsdesktop/exepert-desktop/schemas/codex-app-server/0.149.1/{stable,experimental}/ (with historical 0.125.0 evidence retained)
Native activity/action iconsdesktop/exepert-desktop/src/ui/activity.rs and assets/activity/*.svg
Conversation Brain classifier, settings, evidence reducer, and activation seamdesktop/exepert-desktop/src/conversation_brain.rs and assets/brain/conversation-atlas-v1.json
Native image/runtime dependenciesdesktop/Cargo.toml, desktop/exepert-desktop/Cargo.toml, and desktop/Cargo.lock (base64, getrandom, image with PNG/JPEG/WebP support, and sha2)
Shared browser-compatible request/catalog typessrc/chat/client.ts and src/chat/models.ts
Edge Function registration and saved-prompt database typessupabase/config.toml and src/data/database.types.ts
Model discovery and explicit image capabilitysupabase/functions/_shared/nine-router.ts and chat-models/index.ts
Multimodal validation, routing, usage, and redactionsupabase/functions/run-chat/**
Authenticated prompt librarysupabase/functions/chat-saved-prompts/index.ts and supabase/migrations/20260822173118_chat_saved_prompts.sql
Contract coverageRust tests under desktop/exepert-desktop/tests and focused Vitest files under src/__tests__

August 2026 native desktop change ledger

The August 28 desktop batch is one presentation, performance, and chrome upgrade layered on the existing Native Chat, Codex, collaboration, emoji, message-action, and Conversation Brain foundations. The table below is the review map for the code shipped by this batch; it intentionally records both user-visible behavior and the implementation seam that owns it.

ChangeShipped behaviorPrimary implementation
Shared Chat presentationOne persistent EXEPERT | ZED selector changes Native Chat and Codex visual hierarchy without changing models, protocols, drafts, tasks, or privacy. A compact menu replaces the tabs below 720 px. Missing, unknown, or malformed presentation values fall back only this preference to EXEPERT instead of discarding the rest of Codex settings.desktop/exepert-desktop/src/app.rs, desktop/exepert-desktop/src/codex/settings.rs, desktop/exepert-desktop/assets/activity/chat-presentation.svg
Shared transcript performanceNative Chat and Codex use stable row identities, revision hashes, changed-range remeasurement, 512 px overdraw, conditional tail follow, unseen-content state, and per-chat or per-task bookmarks. Focus/copy/menu geometry participates in the matching Native row revision, and wheel or scrollbar interaction cancels pending tail follow.desktop/exepert-desktop/src/ui/transcript.rs, desktop/exepert-desktop/src/ui/native_chat_transcript.rs, desktop/exepert-desktop/src/ui/codex_transcript.rs
Truthful streaming and disclosuresNative Chat, Codex, and DSH place a collapsed timing disclosure above agent output when trustworthy source timing exists. Worked is whole-response or whole-turn wall time; Thought is shown only for recorded provider-visible DSH reasoning spans. Codex continues to expose only authorized reasoning summaries and allowlisted structured developer details; raw reasoning and payloads remain excluded. Process-local disclosure and wrapping keys include source, owner, turn or message, item, and metric identity.desktop/exepert-desktop/src/app.rs, desktop/exepert-desktop/src/response_timing.rs, desktop/exepert-desktop/src/ui/*_transcript.rs, desktop/exepert-desktop/src/{codex,dsh}/
Message action geometryUser actions render in a sibling footer below the bubble. The assistant More menu measures its trigger, flips above or below, clamps to the live Chat stage, paints once at deferred priority 100, and closes on outside input or transcript scrolling.desktop/exepert-desktop/src/app.rs, desktop/exepert-desktop/src/ui/chat.rs, desktop/exepert-desktop/tests/message_actions_ux_contract.rs
Confirmation measurementA pending history mutation changes only its matching virtual-row revision. The consequence and equal-growth actions form a content-sized wrapping alert, preventing cached-height overlap with later messages.desktop/exepert-desktop/src/app.rs, desktop/exepert-desktop/src/ui/native_chat_transcript.rs, desktop/exepert-desktop/tests/message_actions_ux_contract.rs
Inspector recoveryThe rail action is idempotent open/restart, the shortcut remains a toggle, and Escape closes an active Inspector before Chat surfaces. Stale detached handles are reconciled before reopening.desktop/exepert-desktop/src/app.rs, desktop/exepert-desktop/src/inspector.rs, desktop/exepert-desktop/tests/ui_contract.rs
Headerless desktop shellThe redundant 46 px workbench header is removed. A 36 px activity rail contains a separate four-mode tab list, while a 24 px full-width status rail owns passive state and interactive controls.desktop/exepert-desktop/src/ui/workbench.rs, desktop/exepert-desktop/src/ui/chrome.rs, desktop/exepert-desktop/src/ui/theme.rs, desktop/exepert-desktop/assets/activity/mode-*.svg
Compile-time build detailsProduct version, desktop crate version, and a twelve-character Git commit are embedded at build time. Cargo watches Git's resolved HEAD and branch-ref paths in normal checkouts and worktrees. The sharp version badge opens its opaque details panel upward from the status rail and becomes static status text in benchmark mode.desktop/exepert-desktop/build.rs, desktop/exepert-desktop/src/build_info.rs, desktop/exepert-desktop/src/app.rs
Collaboration and brand cleanupCollaboration is a sharp 28×20 status action before the version control. The visible EXEPERT brand moves from the title bar to the first passive status slot, and ambient state renders one muted ACTIVITY / plus accented AMBIENT.desktop/exepert-desktop/src/app.rs, desktop/exepert-desktop/src/ui/activity.rs, desktop/exepert-desktop/src/ui/chrome.rs
Third-party glyph provenanceThe collaboration action uses Zed's byte-identical user_group.svg at commit d9ad6aff67e47de43abb270d22de75dd950f1b48. Both the repository GPL text and adjacent icon-directory Lucide ISC notice are preserved and embedded; their applicability to this glyph remains unresolved.desktop/exepert-desktop/assets/third_party/zed/, THIRD_PARTY_NOTICES.md, desktop/exepert-desktop/src/ui/activity.rs
Source-selector identityThe Chat header uses original EXEPERT artwork plus locally embedded Codex and DeepSeek marks. The selected source reveals its label when the stage is wide; all marks remain visible in the icon-only narrow layout. Third-party source URLs, local checksums, adaptation notes, and release-review limits are recorded without claiming affiliation or license clearance.desktop/exepert-desktop/assets/activity/source-exepert.svg, desktop/exepert-desktop/assets/third_party/thesvg/, THIRD_PARTY_NOTICES.md, desktop/exepert-desktop/src/ui/chat.rs

The copied collaboration glyph is a release gate, not a completed licensing determination. Attribution and embedded notices do not establish compatibility with EXEPERT's MIT distribution. Public distribution requires legal review or replacement with an independently created, clearly compatible asset.

September 2026 transcript folding and navigation

The second transcript delivery extends the shared native boundary without changing protocols, settings, storage, paging, or server ownership. Codex commands, tools, developer activity, and authorized reasoning summaries now mount only a bounded one-line summary until explicit expansion. Native Chat, Codex, DSH Chat, and DSH Trajectory share browser-style vertical middle-button auto-scroll and independently virtualized checkpoint rails derived only from loaded user-authored rows. All disclosure, motion, marker, excerpt, and bookmark state remains process-local.

Checkpoint hover and keyboard focus share one compact native preview card. The loaded user query is bright and bounded; a matching resident final-assistant answer appears beneath it in a quieter treatment. Native Chat pairs within the next user boundary, Codex accepts only AgentFinal from the same turn, and DSH prefers the final assistant row with the same turn identity. Missing resident answers are omitted rather than inferred. Card width is clamped to the live viewport and both excerpts are Unicode-safe, wrapped, and line-clamped.

September 2026 native discovery and adaptive navigation

The third desktop delivery replaces Codex's transcript-pushing search strip with one shared native Find in Chat overlay. Ctrl+F or the compact search icon in the active source header opens it without changing transcript layout. Typing searches currently resident EXEPERT and DSH user/assistant rows immediately. Codex searches those same visible message classes locally, then debounces the version-pinned thread/searchOccurrences request. Commands, developer messages, tools, DSH trajectory activity, and hidden reasoning are excluded. Server, task, selection epoch, query revision, and search generation qualify remote responses; stale results cannot replace the current query.

The first 50 Codex occurrences are loaded initially. A count ending in + means the server owns another page. Moving forward from the final loaded match requests that page and selects its first validated result; wrapping begins only after the server reports that paging is exhausted. An unloaded result retains the established inclusive-turn hydration and exact turn/item validation before showing a bounded focused view. The active match has a stronger transcript-row marker and its compact preview highlights the matched Unicode text; other loaded matching rows keep a quieter marker. Enter moves forward, Shift+Enter moves backward, and Escape closes Find and restores the exact previous focus.

Ctrl+K or titlebar-global-search opens the native global command palette. Its independently virtualized groups are Recent chats, Codex tasks, DSH sessions, Quick actions, Navigation, Panels, and Settings. Exact and prefix matches rank before substrings within each group, with recency breaking ties. Codex uses thread/search and DSH uses its session search contract; provider responses remain query-qualified and Codex exposes an explicit next-page control. The palette searches titles, IDs, workspace paths, catalog metadata, and commands only. It never indexes, displays, persists, or logs transcript bodies. Arrow keys, Home, End, Enter, and Escape provide the keyboard path. Find in current chat transfers directly to the Ctrl+F surface.

The title bar's non-drag left cluster is ordered as navigation density, global search, and attention. The native drag region follows, then icon-only System Status and Build utilities, and finally the Windows controls. Both utility clusters remain outside the drag region. Ctrl+B and titlebar-navigation-density share the exact cycle:

  1. Full — activity rail and source/sidebar panel are mounted.
  2. Sidebar only — the source/sidebar remains and the activity rail is not mounted.
  3. Focus — neither navigation region is mounted.

The accessible name reports both the current and next state. Focus moves to the active stage before a navigation owner disappears, and popovers anchored to a disappearing region close. Only navigation_density is written to the strict, versioned %LOCALAPPDATA%\EXEPERT\desktop-ui-settings.json; malformed, oversized, unknown-field, or future-version content safely defaults to Full. Benchmark mode always renders Full and never writes this preference.

September 2026 seamless workbench polish

Navigation and the main title bar share SHELL_SURFACE (#181818), including the idle backgrounds and borders of the 30 px title-bar actions. Their selected, hover, pressed, and keyboard-focus feedback remains distinct. The activity rail has no right border.

The September 3 title-bar follow-up makes the stable workspace-inset meet the bottom of the 34 px title bar and fill the client area's right and bottom edges. The stage and context panel share WORKSPACE_SURFACE (#121214) and are clipped to an 8 px top-left corner with 1 px subtle top/left borders. The other corners are square; there is no outer shadow or top/right/bottom gutter. The left edge stays flush with visible navigation and gains an 8 px gutter only in Focus mode. The shared WORKSPACE_GUTTER token remains 8 px for that left gutter and independent popover margins. The context panel keeps a single internal left divider when side by side and a top divider when stacked.

The responsive side-by-side thresholds are 1,189 logical pixels in Full, 1,153 in Sidebar Only, and 937 in Focus, accounting for the single left border. Brain telemetry remains 84 logical pixels tall at the bottom of its center column; a stacked context panel follows below it. All caption controls use native drawings centered in their actual hit areas, inheriting idle and hover foreground colors independently of font metrics. Nominal 1 px strokes and 10 px icon footprints are rounded consistently in device pixels. Minimize is a 10 by 1 line; maximize is an outlined 10 by 10 square; close is a symmetric cross. When window.is_maximized() is true, maximize becomes two overlapping 8 by 8 restore squares, offset by 2 px within the same footprint. The main window retains its 42 by 34 caption hit areas, and the detached Inspector and Runtime Log reuse the same helper. Native drag, minimize, maximize/restore, and close behavior continues through GPUI's WindowControlArea.

Navigation retains the exact Full -> Sidebar only -> Focus -> Full cycle. Geometry uses a 170 ms ease-out-quint transition, while three original local glyph layers crossfade and scale over 140 ms. Rapid Ctrl+B or pointer input retargets from the currently painted widths; generation checks stop stale frames. The responsive side-by-side decision uses the final target widths, so the breakpoint cannot oscillate during motion. Reduced motion applies the final geometry and glyph immediately, including when the preference changes during an active transition, and deterministic benchmark mode remains Full without changing the saved preference.

Before a navigation region begins disappearing, focus moves to the active stage and anchored popovers close; open Find, Global Search, and Attention overlays keep focus on their own input or panel. The active transcript captures its stable first-visible row, nonzero within-row offset, scope, and interaction revision before the width change. The transcript list holds one measured width during the 170 ms shell motion, then releases it for one final-width reflow. After settlement, restoration proceeds only if no later wheel, checkpoint, jump, auto-scroll, tail-follow, or scope/surface change superseded it. Native scrollbar movement participates through a shared content-free drag revision, and successful restoration cancels active motion. This bookmark, the transition, glyph opacity, and tooltip state are all process-local and never serialized or logged. Only navigation_density continues to enter the strict desktop UI settings file.

The 30 px title-bar actions use one cached structured native tooltip for hover and keyboard focus. Escape dismisses it until both hover and focus leave, and shortcut pills are mirrored into native accessibility shortcut metadata. The navigation copy follows its next action: Hide activity rail, Enter Focus mode, or Restore navigation, each with a Ctrl+B shortcut pill. Global Search shows Search and commands with Ctrl+K; Attention does not advertise an invented shortcut. The accessible navigation name still reports the current and next state. Settled zero-width navigation is unmounted, so refreshed UI Automation output has no hidden rail or sidebar descendants.

Stable IDs added or retained for this shell include workspace-inset, navigation-transition-wrapper, navigation-glyph-transition, navigation-glyph-full, navigation-glyph-sidebar-only, navigation-glyph-focus, titlebar-action-tooltip, and titlebar-action-tooltip-shortcut.

titlebar-attention aggregates process-local, bounded references to pending Codex/DSH approvals, authoritative Codex and collaboration unread counts, updates to inactive EXEPERT/DSH destinations, and recoverable runtime or transport failures. It stores stable IDs, source/kind metadata, a bounded label, and a navigation action—not transcript snippets, request payloads, credentials, or approval authority. Items are deduplicated and ordered Needs action, Unread, then Failures; the badge caps at 99+. Opening unread content acknowledges it. Approval items remain until the source resolves them, and selection never approves a request. Failures remain until recovery or explicit process-local dismissal, and a changed or recurring failure resurfaces.

Find, global search, and attention are mutually exclusive top layers. Escape closes only the active top layer. Their stable accessibility roots are find-in-chat, global-search-dialog, and attention-panel; title-bar roots are titlebar-navigation-density, titlebar-global-search, and titlebar-attention. Result, retry, empty, group, option, badge, and source- qualified toggle IDs are stable descendants. Both virtualized discovery lists retain the existing fewer-than-80 realized-row gate.

September 2026 unified workspace and rail collaboration

The fifth native desktop delivery removed the 24 px bottom status bar. The current workspace also uses the former bottom gutter, as described above. The root, title bar, activity rail, and navigation sidebar use SHELL_SURFACE (#181818). The non-painting workspace-inset-gutter owns the remaining Focus-mode left spacing. The clipped workspace-inset, its center stages, Chat-source stage shells, composer surroundings, and right context panel use WORKSPACE_SURFACE (#121214). Cards, inputs, dividers, and other elevated surfaces keep their existing tokens.

The left title-bar order is Navigation, Search, Attention, System Status, then Build. System Status and Build retain their 30 px icon-only buttons, existing 2 px gaps, and stable titlebar-utility-controls toolbar, now nested after Attention inside titlebar-left-controls. The remaining title-bar space is draggable; the main renderer supplies no trailing utilities. System Status opens the bounded desktop-status-panel, which contains the retained desktop-runtime-status product label and truthful desktop-preset-status activity or preset value. Build retains desktop-version-badge, its compile- time product/crate/commit details, and keyboard focus restoration, but no longer renders a textual badge. Both panels open below the title bar with an 8 px offset and use Anchor::TopLeft, with the existing 8 px window-edge containment. Benchmark mode uses the same no-status-bar geometry and omits these interactive utilities.

The focused toolbar owns Tab and Shift+Tab traversal in visual order. Its EXEPERT_TITLEBAR key context excludes Brain bindings so Enter/Space reach the existing title-bar activation handlers. Brain preset keyboard behavior remains available when focus is outside the toolbar.

Human Collaboration is the first item in activity-global-controls, above the Inspector when available and Settings. collaboration-popup-trigger is a native 28 px popup action with truthful aria-expanded, connected/open styling, focus-visible treatment, a dynamic tooltip and accessible name, and the retained collaboration-unread-badge. The action is mounted only when the collaboration runtime and Full-density rail are available. Ctrl+K exposes the stable global-search-option-quick-open-collaboration quick action in every normal density, and Attention routes collaboration items through the same opener.

In Full density the collaboration panel begins at rail width plus the 8 px workspace gutter. In Sidebar Only and Focus it begins 8 px from the window's left edge. Width, height, and left placement clamp to an 8 px viewport margin, and the panel bottom aligns with the workspace gutter. Opening provenance stays process-local: a rail-origin close restores the mounted trigger, while palette, Attention, guarded remote-control, and message-sharing opens restore the active stage. Cycling away from Full closes a rail popup and transfers focus before navigation motion, so an unmounted rail leaves no hidden UI Automation node.

Find, Global Search, Attention, Human Collaboration, System Status, and Build share one top-level exclusion policy. Opening one closes a conflicting surface; Escape dismisses only the active top layer before returning focus. No new UI preference is serialized: only the existing navigation density preference and existing collaboration preference behavior remain.

September 2026 native response timing and row alignment

All three native Chat sources now use the same typed timing presentation above agent output. A live measurement reads Working for … and freezes as Worked for …. An exact failed or cancelled measurement reads Stopped after …. A second Thinking for … or Thought for … row is present only when DSH records trustworthy provider-visible reasoning boundaries. Missing, malformed, partially paged, or backwards timing data is omitted instead of estimated.

The source rules remain deliberately different:

  • EXEPERT starts a process-local monotonic clock at ChatEvent::Started and replaces it with persisted backend total_latency_ms on successful completion. This is request latency, not private thinking time, so EXEPERT does not synthesize a Thought row.
  • Codex retains per-turn startedAt, completedAt, and durationMs. An explicit durationMs wins; otherwise a valid timestamp difference is used. A local monotonic anchor drives only the provisional live display. Bounded thread/turns/list hydration supplements resident historical turns without changing transcript ownership or the established item/turn/read fallback. Codex 0.149.1 exposes no reasoning duration, so its existing safe reasoning summary remains separate and no Thought timer is inferred.
  • DSH prefers an explicit recorded turn duration, otherwise a matching turn/start to terminal source-timestamp difference. Thought sums merged recorded reasoning block/delta spans across model steps and excludes tool or approval gaps. The already-redacted provider-visible text moves into the bounded Thought card; consumed reasoning rows are hidden only from Chat and remain unchanged in canonical Trajectory.

Each metric is a full-width button with a stable source-qualified ID, native focus styling, keyboard activation, and truthful expanded state. Disclosures start collapsed. Height, opacity, and chevron direction retarget smoothly over 150 ms; reduced-motion mode applies the final state immediately. Expanded content is capped at 360 px with internal scrolling, timing facts are bounded, and reasoning content is capped before rendering. One visible-surface 1 Hz repaint ticker updates running labels without changing row identity or forcing measurement on every tick.

ZED response geometry now routes the active streaming row through the same centered 760 px assistant shell as completed output, including its 20 px content inset. The 720 px response footer is centered inside that shell. Standard EXEPERT presentation retains its existing 720 px alignment. Switching presentation invalidates affected virtual-row measurements while stable keys, bookmarks, and tail-follow preserve the reading position.

Chat sources and isolation

The existing 42 px Chat header exposes accessible EXEPERT, CODEX, and DSH tabs in a native TabList. Each source keeps a locally embedded icon; the selected tab also shows its text label at 720 px or wider, while narrow stages retain icon-only tabs with tooltips and unchanged ARIA names. Codex remains visible when its executable is missing or incompatible and carries an Experimental status label. The shared header dispatches presentation only; it does not merge source state.

EXEPERT retains its NativeChatState, Supabase transport, verified model catalog, image processing, feedback, brain grounding, saved prompts, focus, virtual transcript state, and transcript persistence. Codex owns a different textarea, server-scoped attachment list, task reducer, model/effort choice, approvals, focus, virtual transcript state, diagnostics, an authenticated owned loopback WebSocket runtime, and an optional remote WSS runtime. Switching tabs or tasks preserves those process-local values independently. Codex prompts, responses, approvals, attachment contents, tokens, and protocol payloads never enter ChatPersistence, Supabase, EXEPERT feedback, or affect telemetry.

Only non-content Codex selections are saved: the exact executable path, up to eight attached task IDs, active task, last workspace, active Chat source, and UI preferences. The runtime starts lazily only when Codex Chat is selected; renderer benchmark and probe modes never initialize it. See the architecture record for the exact 0.149.1 protocol, persisted-read firewall, raw-reasoning exclusion, Windows Job ownership, recovery sequence, and no-spend validation boundary.

Presentation selector and shared transcript

The Chat header includes one global EXEPERT | ZED presentation preference. It is stored as CodexUiPreferences.chat_presentation; a missing field defaults to EXEPERT without changing settings schema version 2. The stable controls are chat-presentation-tabs, chat-presentation-exepert, and chat-presentation-zed. Below 720 px of Chat-stage width they collapse into chat-presentation-menu-trigger and an opaque two-option popup.

The selector changes visual hierarchy only. A switch preserves the current Native chat or Codex task, draft, attachments, running request, speech, collaboration, and logical reading position. Message-anchored and composer popups close because their geometry is no longer valid, then only affected rows are remeasured. No model, protocol, privacy, persistence, or network behavior changes.

Native Chat and Codex share an adaptive variable-height ListState engine with stable row keys and revision hashes, prefix/suffix splicing, changed-range remeasurement, 512 px overdraw, a 48 px near-bottom threshold, and a bounded 140 ms glide for non-precision wheel input. Precision input and reduced-motion input remain direct. Bookmarks are scoped by ChatId or CodexTaskKey, so switching conversations restores the prior logical reading position instead of blindly jumping to the newest message.

The same scroll boundary supplies vertical middle-button auto-scroll to Native Chat, Codex, DSH Chat, and DSH Trajectory. An unmodified middle click arms a visible anchor; pointer movement inside a 12 px vertical dead zone does nothing, then speed grows linearly to a 1,800 px/s cap. Frame time is clamped to 50 ms. Reaching an edge pauses movement without disarming the anchor, so moving back across it resumes in the opposite direction. A second middle click, another pointer button, wheel input, Escape, an explicit jump, or a transcript scope change cancels it. Starting auto-scroll pauses tail follow. Reduced-motion mode keeps the functional indicator but omits decorative motion.

When loaded user-authored rows exist, each of those surfaces also renders a compact independent virtual checkpoint rail. Marker identity comes from the stable transcript row key; the accessible label is Jump to user message N and its compact hover/focus card shows the user excerpt in primary text plus an optional resident final-assistant excerpt in secondary text. Each excerpt is normalized to one line and capped independently at 160 Unicode characters; the same paired copy is available in the accessibility description. Native Chat pairs the last assistant message before the next user turn, Codex pairs only an authoritative same-turn final answer, and DSH pairs only a completed same-turn assistant/message record—not stream chunks or model-boundary rows. The ordinal describes only the currently loaded chronological window. Click, Enter, or Space jumps through the existing stable-key path; Up, Down, Home, and End navigate markers. The active marker is the latest loaded user row at or above the first visible transcript row. The rail never requests older data. When a Codex resident page has no user row and an older page can actually be loaded, a visually distinct unnumbered rail affordance jumps to the existing Load Older History row. It is omitted at the 500-item cap and on reduced- fidelity servers without older paging. It never calls the runtime or initiates paging; loading remains an explicit action on that row. Adding or removing a rail remeasures resident transcript rows while the stable-key bookmark keeps the reading anchor.

Native response events continue to drain in 40 ms batches. In ZED presentation, the first visible answer token creates one process-local provisional assistant row; before that token, the stable timing header shows Working for …. The provisional buffer is UTF-8 safe and bounded to 512 KiB. Completion atomically replaces it with the authoritative stored response, while cancellation or failure removes it and retains the existing retry flow. Provisional content is never persisted, sent back as model history, or represented as reasoning.

EXEPERT presentation retains the established cards and disclosures. ZED uses the same optimized engine with compact 8 px user cards, flat assistant output in a centered 760 px reading column, a 20 px response inset, a centered 720 px response footer, quiet timeline guides, borderless message actions, and a 12 px shadowless composer. Codex Thinking may contain only the App Server's authorized ReasoningSummary; it and all other expandable activity begin as a one-line summary. The full projected text and developer Structured details are not mounted until explicit expansion, and collapse unmounts them again. Details come only from the reducer's bounded allowlisted item.details. Raw reasoning, protocol payloads, prompts, credentials, and encrypted fields stay excluded.

There is no bottom status rail. Human Collaboration lives in the activity rail, with Ctrl+K and Attention fallbacks when the rail is hidden. System Status and Build use progressive disclosure in the left title-bar utilities. Closing the collaboration surface never ends an active room, and benchmark mode omits all three interactive utilities without creating dead tab stops.

User experience

Compact composer

The Chat composer is content-driven instead of using a fixed input-strip height. Its shell has a 12 px horizontal inset, no top divider, and an 8 px bottom gap that gives the inner surface visible separation from the window edge. The inner surface uses 12 px horizontal padding, 8 px vertical padding, an 8 px content gap, a subtle outline and shadow, and a Chat-local 20 px radius; the rest of the desktop retains EXEPERT's 2 px visual system. Replacing the former 12 px top-and-bottom inner padding with 8 px on each side recovers the same 8 px used by the shell gap, so normal composer chrome remains 24 px tall and the total footprint does not grow. The outline changes to ACCENT_DIM while the textarea has focus. The textarea has a 40 px minimum height, submits with Enter, inserts a newline with Shift+Enter, and grows with visually wrapped typed or pasted content. Its responsive maximum is 8 rows at logical viewport heights through 720 px, 10 rows from 721–899 px, and 12 rows from 900 px upward; after that cap the textarea scrolls internally. The drag/drop overlay follows the same 20 px composer boundary.

After the first user turn, a normal text-only composer remains approximately 96–112 px tall. It expands only for multiline text, attachment cards, inline notices, or the initial suggestion row. The row buckets are a soft 45% target for text-dominant states; required auxiliary surfaces may temporarily add their own bounded height.

The latest viewport bucket is process-local. EXEPERT applies it immediately while the textarea is empty, including initialization, New Chat, successful Send, manual clearing, and empty Chat re-entry. While text is present the currently applied cap is frozen and the latest bucket remains pending. This is intentional: the pinned GPUI textarea resets its measured row mode when the cap is replaced, so deferral preserves the draft's geometry, selection, undo history, and focus through window resizing. GPUI continues to remeasure normal text edits and width-driven wrapping within the active cap. Programmatic composer replacements from prompt history, Saved prompts, and quick suggestions explicitly enter the same responsive reflow path because the pinned textarea intentionally suppresses change events for set_value.

The bottom row keeps these persistent actions:

  • a 32×32 Tools and attachments trigger at the left;
  • flexible space;
  • a compact verified-model selector immediately before the primary action; and
  • Send or Stop in one stable 36 px slot.

The Tools menu contains only implemented actions: Add photos & files and Saved prompts. Both are direct native menu items rather than a placeholder tool hierarchy. Existing image limits and saved-prompt authentication still control each item's disabled state and explanation.

The three quick suggestions render only before the first user message. A text turn or an image-only turn hides them. New Chat clears the session and makes them available again, while the Prompts library remains available throughout.

Tools and model popovers

The plus control is exposed as Tools and attachments and opens GPUI's native PopupMenu, so assistive technology receives Menu and MenuItem semantics and keyboard users can open the trigger with Enter or Space, then use arrow keys, Enter, and Escape. EXEPERT uses the component's standard labelled rows—the only pinned API that gives the outer selected MenuItem a reliable Windows UIA name. The stable chat-attach-image and chat-prompts-trigger IDs remain as zero-size inspector aliases inside the menu surface because this pinned PopupMenu has no custom row-ID hook; the aliases add no second accessibility role or hit target. When one action is unavailable, its reason is rendered as native menu copy and included in the Tools trigger description. Because the pinned native menu initially selects row zero, EXEPERT keeps the first keyboard row actionable whenever only one tool remains available and independently guards both action handlers. An open menu is rebuilt when processing, pending, or cancellation state changes, so its availability and copy cannot become stale; if both actions become unavailable, the menu closes instead of remaining logically expanded behind a disabled trigger. Escape returns focus to the plus trigger. Pointer dismissal does not explicitly move focus, adding or removing images restores the textarea, and selecting Saved prompts moves focus into its search field. Tools, the model catalog, and the saved-prompt library are mutually exclusive and all close on New Chat or when leaving Chat. If both menu actions are unavailable, the non-focusable trigger tooltip includes both reasons rather than hiding them.

The selected-model control is 32 px high, uses a 120 px minimum and 240 px maximum width, and truncates long names to one line. Its accessible name and tooltip retain the full model, provider, and context state. The 16 px provider logo is supplemental: Claude/Anthropic entries use claude.svg, OpenAI/GPT/Codex entries use openai.svg, and Gemini, combo, Mimo, or unknown families use generic.svg until a dedicated asset is supplied. Picker rows continue to show provider text, so identity never depends on the SVG alone.

The model catalog opens above and right-aligned to the compact trigger with an 8 px gap. Its approximately 400 px independent width is bounded to the Chat surface. Model search, keyboard navigation, explicit selection, capability labels, and routing behavior are unchanged. The model and saved-prompt surfaces prefer conservative 400 px and 360 px maximum heights respectively, then clamp those heights to the live space between the Chat header and composer trigger. Their list bodies flex-shrink and scroll independently, so the whole popup stays inside the required compact viewport instead of merely clipping its list.

Models, Saved prompts, and Tools share an opaque Chat-local popup treatment: solid SURFACE_2, BORDER_STRONG, a restrained 10 px radius, inner highlight, and shadow. The two custom library surfaces defer their paint at priority 100 so they cover transcript and composer content. Tools keeps GPUI's native Popover host and its single native priority-100 deferred layer; EXEPERT does not wrap that menu in a second deferred surface or replace its Menu/MenuItem semantics. The pinned PopupMenu also paints its own opaque nested fill and offers no per-instance background override, so EXEPERT aligns the component theme's process-wide popover background token to SURFACE_2 at startup. The Chat shell still owns the strong outline, 10 px radius, inner highlight, and shadow; no menu geometry or semantics change. The former whole-panel opacity animation remains removed.

This is a frosted-glass visual suggestion, not real backdrop blur: the pinned GPUI revision has no backdrop-filter API, and EXEPERT adds no transparency, platform-composition hook, dependency upgrade, or custom blur render pass. GPUI's .occlude() controls the popup pointer hitbox; the fully opaque fill and deferred paint order—not occlusion—provide visual coverage.

Transcript hierarchy

Assistant responses are flat transcript content: they have no outer card background, border, radius, or padding. The visible EXEPERT label remains, with a 6 px gap before its response body. Feedback and response metadata align to the same left edge as that body.

User messages are compact, right-aligned SURFACE_3 bubbles with 12 px padding, a subtle outline, and a Chat-local 16 px radius. The visual YOU label is removed, while the semantic User message identity remains available to assistive technology. Attachments, failed-response panels, and other actionable or error surfaces keep explicit boundaries.

Conversation history and message actions

EXEPERT Chat stores a versioned set of conversations instead of overwriting one global transcript. Each chat and message has a stable ID, timestamps, its own session key, and bounded safe response metadata. The store retains at most 20 chats, 50 messages per chat, and 8 MiB total. It prunes the oldest inactive chat when needed and reports that event. The active chat, the conversation just left during a switch or New Chat, and both sides of a new fork are protected from size pruning; if that protected set cannot fit, it remains memory-only instead of silently deleting user work. Legacy message-array state migrates deterministically into one active chat. Atomic replacement preserves the existing rule that attachment bytes never enter the saved transcript.

The recent-chat selector switches between retained conversations. New Chat preserves the current conversation and reuses an existing blank chat instead of accumulating duplicates. Fork creates an independent session containing history through the selected message. Switching and branching are unavailable while a request mutation or response edit is pending, and switching stops speech and closes message popups.

Completed Native Chat messages expose role-appropriate 32 px contextual actions. Assistant responses support Copy, local Edit, Regenerate, confirmed Delete, Read aloud, Fork, Markdown export, and reported response details. User messages support Copy, Edit and resend, Fork, and confirmed Delete. The newest assistant action row stays visible; older and user rows remain mounted for pointer and keyboard reveal. Request-backed edit/resend and regeneration stage their replacement and retain the original transcript until the replacement succeeds. Mutation actions are disabled while sending or cancelling, while Copy, export, details, and speech remain available. An image-backed prompt whose process-local image bytes are no longer retained cannot be edited or regenerated; the action explains that the image must be attached again.

Read aloud uses the default Windows system voice from one bounded SAPI worker. Starting another utterance stops the prior one; message removal, chat switching, explicit Stop, and shutdown also stop playback. Only visible plain text reaches the worker. Reasoning, tool payloads, credentials, attachments, and speech content are excluded from logs.

Response details open in a non-modal opaque right-side sheet with a visual scrim, Escape handling, and focus restoration. The sheet shows only values reported by the backend, including safe model, provider, identifiers, timing, usage, and context fields. First-token speed, generation speed, chunks, and tool metrics are marked unavailable when the response does not report them; EXEPERT does not infer them. Missing fallback flags remain unavailable, and estimated context counts are explicitly labelled as server estimates.

Conversation Brain

Both EXEPERT and Codex Chat place an Experimental two-brain monitor at the top of the right panel. It distinguishes HUMAN · PREDICTED ANALOGY from AI · INTERFACE ACTIVITY, labels observed versus inferred evidence, and shows submit/response direction without claiming mind-reading. Selecting the card opens the full BRAIN CONNECTION view; the original SIMULATION view remains separate.

Semantic inference is a bounded, offline English/Malay visible-text heuristic. Empathy mode is opt-in, analyzes an idle draft locally, and exposes its fixed tone/depth suggestion for user override before Send. Native Chat transmits only validated enums that the Edge Function maps to trusted presentation copy; Codex adds a separate visible cue only to a new turn. Derived activity and overrides are process-memory only. See Conversation Brain for the evidence, privacy, atlas, and future activation-provider boundaries.

Header, keyboard, and accessibility

The existing 42 px header now uses icon-first EXEPERT, CODEX, and DSH source tabs while retaining its bottom divider and Close dimensions. The stable IDs remain chat-source-exepert, chat-source-codex, and chat-source-dsh; hover and keyboard focus expose the source name, while the selected source also shows its text label at header widths of 720 px or more. Enter and Space activate a focused inactive tab. EXEPERT keeps the stable chat-new-center action; Codex exposes codex-new-task and requires a native workspace-folder selection. Header controls are transparent at rest, use SURFACE_3 on hover, and show a keyboard-only accent focus ring without changing layout.

While Chat is active, Tab and Shift+Tab use GPUI's native focus traversal. Renderer-preset traversal remains unchanged outside Chat. Every enabled Chat action exposes a focus-visible ring; disabled actions remain outside the tab order.

The native accessibility tree exposes the transcript as a Log, each message as an Article named User message or EXEPERT response, ordinary asynchronous copy as Status, and failures as Alert. Meaningful small labels, notices, save state, feedback text, and response metadata use TEXT_SECONDARY; muted and disabled tones are reserved for redundant or unavailable content. Dynamic response, setup, composer, attachment, feedback, save, and error copy is also included in native accessibility labels instead of relying on visually rendered text alone. Saved-prompt rows are semantic groups with explicit keyboard-accessible Use, Rename, and Delete actions, so interactive controls are never nested inside another focusable action.

Smart response following

The transcript treats content within 48 px of the bottom as already following the conversation. Sending always reveals the new user turn. A completed or failed response auto-scrolls only if the transcript was already within that threshold. Whenever a non-empty transcript is outside that threshold, a keyboard-accessible 32 px circular Scroll to bottom arrow floats 12 px above the transcript's bottom edge without consuming layout height. Its 16 px chevron remains flex-centered, and the existing 140 ms appearance motion grows the control from 28 px to 32 px; reduced-motion mode resolves directly to the final size. The same geometry is used by EXEPERT, Codex, and DSH Chat. An offset accent dot appears only when new content arrived below the current viewport, without covering the compact chevron.

The send path records a short-lived tail-follow intent before the new row is painted, preventing a fast response from racing the first scroll layout. The same guarded intent keeps the transcript bottom-aligned when textarea growth or window reflow begins while the user is already near the bottom. Each intent is generation-qualified so an older delayed layout callback cannot settle a newer send, resize, or input request. Manual wheel interaction or a completed scrollbar interaction cancels that intent. Activating Scroll to bottom, returning near the bottom through those observed scroll paths, a reconciled window resize, sending another turn, or starting a New Chat clears the unseen response state. If bounded transcript retention removes old leading rows while the user is reading history, GPUI compensates the offset by the removed row height. Automatic response updates never move focus away from the textarea.

Contextual action placement

  • New Chat is in the Chat header immediately before Close.
  • Retry appears only in the failed-response panel in the transcript.
  • “Was this response useful?”, Yes/No, structured/model metadata, and local save status render beneath only the latest assistant response.
  • Feedback choices are both disabled while saving. A successful selection keeps its accent state. A failed save shows an inline error and re-enables both choices.

The inspector-facing IDs remain stable, including brain-stage, chat-transcript, chat-composer-shell, chat-composer, chat-send, chat-stop, chat-new-center, chat-close, chat-message-*, feedback, Retry, prompt, model, and attachment IDs. The conditional response-following control uses the internal ID chat-scroll-to-bottom; the Tools trigger adds chat-tools-trigger. Existing chat-prompts-trigger, chat-attach-image, chat-model-anchor, chat-model-trigger, and chat-model-picker IDs remain stable in their new hierarchy.

Codex adds source-local IDs prefixed with codex-, including codex-task-panel, codex-transcript, codex-message-*, codex-composer-shell, codex-composer, codex-send, codex-steer, codex-stop, approval controls, optimistic submission Retry/Discard controls, sanitized diagnostics, and read-only mirror controls. Mirror keeps stable codex-mirror-task-*, codex-stop-mirror, codex-mirror-status, codex-mirror-read-only, and codex-mirror-copy identities. These additions do not rename any EXEPERT inspector contract.

Offline emoji and local reactions

Native Chat and writable Codex composers share one opaque, responsive emoji picker. Its compact catalog is generated at release time from pinned emojibase-data 16.0.2 English and Malay metadata and embedded in the desktop binary; the generator verifies the package version and both input checksums before stamping that provenance. Search, category labels, recents, and skin-tone variants therefore work without a CDN, telemetry call, or runtime network request. Left/Right/Home/End remain native caret keys in search; Down moves into a single-focus results grid, whose accessible label follows the highlighted item. Category tabs use roving focus, and Tab/Shift+Tab remain contained inside the open dialog. GPUI inserts the selected Unicode text through the textarea's atomic replace operation, so the current caret, selection, and undo history remain authoritative. Windows draws the characters with its native emoji font; EXEPERT ships no third-party emoji artwork.

Completed Native assistant responses accept up to six unique local emoji reactions. Each reaction is at most 64 UTF-8 bytes and is saved through the existing atomic version-2, 8 MiB chat store. Forks copy them, local response editing clears them, and truncation removes them with the affected messages. They never enter model context, exports, feedback, telemetry, or collaboration traffic. User messages and non-response rows do not expose reaction controls. The picker keeps at most 24 recent choices and a preferred skin tone only for the current process. The preferred tone is applied consistently to compatible catalog entries, recents, and default quick reactions.

Experimental Codex task experience

The Codex left panel contains only explicitly attached tasks plus a separate stored-task browser. Each attached row reports title, workspace, activity or approval state, and unread count. Takeover is a two-step confirmation that the task is idle in every other Codex client; Fork remains the safe alternative. Detaching an idle task unsubscribes without deleting history. An active task uses a separate Interrupt + Detach confirmation and fails closed if the runtime has not supplied its turn ID. Pending approval in an inactive task also creates a persistent alert above the task list.

Stored-task discovery polls complete recency-sorted pages every second while Codex is foregrounded and every five seconds in the background. It explicitly includes CLI, VS Code, and App Server sources and replaces rows only after the whole epoch succeeds. A failure retains the previous rows and backs off for 30 seconds; Refresh starts a new epoch without clearing the panel. Recovery scopes epochs to the replacement App Server generation, so a prior high epoch cannot hide fresh rows.

Completed discovery pages and normalized mirror presentation snapshots receive process-local SHA-256 fingerprints. After the initial result, an identical poll is accepted for freshness and scheduling but emits no duplicate UI event. Recovery from STALE or UNAVAILABLE is always emitted even when the restored transcript content is identical. Reducer applications classify accepted changes as no-op, metadata-only, or transcript-changing. A no-op notifies nothing; metadata refreshes the workbench; transcript changes reconcile the child and perform one root refresh for shared task/event metadata. Manual Refresh still performs a full network reconciliation; an identical answer simply causes no repaint. Poll cadence, privacy limits, and stale-epoch rejection are unchanged.

Each stored row offers Mirror, Take Over, and Fork. Mirror is one ephemeral, read-only persisted view outside the eight attached tasks. It shows SYNCING, MIRRORING, STALE, or UNAVAILABLE, exposes Copy and Stop Mirror, and replaces the interactive composer. It cannot send turns, approvals, interrupts, attachments, or model changes. Full item pagination falls back to turn pages and then a capped five-second thread read with an explicit reduced- fidelity warning. Mirror selection and content never enter settings. Take Over or Fork keeps the mirror until its matching acknowledgement succeeds. Mirror start/stop uses a lossless last-write-wins lifecycle directive, so Stop also wins while the bounded mutation queue is full or App Server is recovering. Incomplete turn item views and malformed item identity never appear as a current snapshot; large update bursts reload the newest bounded 500-item window.

For controlled tasks, Task creation, Fork, Takeover, and Detach save the outgoing draft and scroll before selecting the next task, then restore the incoming task without moving content between tasks. A mirror has no draft to save and is cleared from memory on Stop, switch, exit, or recovery. Takeover does not select its task until thread/resume succeeds. Active detach waits for a terminal turn after interrupt before unsubscribing, and both idle and active detach retain the row until thread/unsubscribe succeeds. A queue, timeout, or server failure cancels only the transient operation and preserves the existing task state. Pending creation, Fork, and Take Over actions reserve one of the eight controlled slots, with a second runtime-side capacity check before any acknowledgement is accepted.

Each task retains an independent process-local draft, remote-image draft, local and remote attachments, model, supported reasoning effort, transcript scroll, and pending submission. The Codex textarea follows the same responsive 8/10/12-row viewport buckets and populated-draft resize freeze as the EXEPERT textarea. The model list is authoritative for effort choices and input modalities; attached images block Send when the selected model does not report image support. A queued user turn appears immediately, reconciles only with the authoritative userMessage, and remains visible with Retry and Discard after a protocol error, timeout, or interrupted runtime generation.

The Codex transcript is a separate CodexTranscriptView entity rather than a full child tree owned by the workbench. Its top-aligned variable-height ListState retains the 500-row logical limit while realizing only the viewport and 512 px of nearby overdraw. Timeline items, pending submissions, approvals, and the omitted-history notice have stable row keys. Structural reconciliation uses splice_focusable; content-height changes use remeasure_items, so a focused control remains available even when its row is outside the viewport. Wheel animation frames notify this child entity rather than explicitly notifying the root. The transcript wrapper is a full-height flex container, so the child receives the remaining stage height instead of collapsing to its minimum row height. The entity boundary keeps wheel-frame redraws inside the transcript, reuses unchanged row paint, and leaves the surrounding workbench untouched.

Each task's process-local scroll bookmark stores the stable top-row key, a fallback index, the offset inside that row, and whether the view was following the tail. Activity filtering, prefix removal, row remeasurement, and task switches restore that anchor. A missing key falls back to the nearest retained index. New transcript content follows automatically only when the reader was already within 48 px of the bottom; otherwise the currently read text remains anchored. Explicit Send and latest/end actions still jump immediately.

Discrete line-based mouse-wheel input uses a restrained 140 ms ease_out_quint glide. Same-direction steps accumulate up to one viewport; direction reversal replaces the pending target, and boundaries clamp without bounce. Pixel-precise touchpad deltas remain direct. Scrollbar interaction, task/source changes, programmatic jumps, and precise input cancel pending animation. GPUI's reduced-motion setting also makes line input immediate. This is native GPUI interpolation, not PhysX and not a browser compositor or CSS smooth-scroll engine.

The middle-button path is separate from wheel interpolation. It tracks only vertical displacement from its visible anchor, clamps elapsed frame time to 50 ms, and leaves the session armed when a scroll boundary temporarily blocks movement. Pointer buttons, wheel input, Escape, checkpoint activation, programmatic jumps, and transcript scope changes cancel it while preserving the stable transcript bookmark.

Approval cards remain present but disabled after a response is queued and are removed only after serverRequest/resolved. Permission cards include an explicit decline. Multi-question fixed-choice requests collect every answer before submission; secret, free-form, custom-answer, and MCP form prompts expose their sanitized scope but fail closed to decline/cancel rather than presenting an unsafe or incomplete acceptance control. A writer-queue rejection re-enables the matching card instead of leaving it permanently disabled.

Every Codex timeline row owns the transcript width. User and pending-submission bubbles stay content-sized and right-aligned, capped at 760 px. Assistant, reasoning, developer-activity, and approval bodies fill the available row up to the same 760 px cap. Only an inner flexible text region may use a zero minimum width; an otherwise auto-sized body never does. This prevents GPUI from resolving a body to a glyph-wide flex column while still allowing word wrapping at narrow window sizes.

Final agent answers remain primary flat copy. Final and commentary text is rendered by the native Markdown TextView, keyed by its server turn_id and item_id, selectable, and allowed to grow within the transcript rather than creating nested scrolling. Stable keys reuse parsed state across mirror polls; changed streaming source is reparsed. Headings, lists, emphasis, code, and tables therefore receive native rich presentation. Commentary remains lighter progress copy.

Markdown links use GPUI's standard URL handler. Remote Markdown images use its standard image loader and are constrained to the response width. EXEPERT does not add Codex credentials, task identity, workspace data, or custom headers to those media requests. Loading a remote image therefore contacts the host named in the assistant-authored Markdown; users should treat that host as an external privacy boundary. The Copy Visible Transcript action remains verbatim: it copies the original server text, including Markdown source, rather than a rendered reconstruction.

User messages, warnings, assistant commentary, and final answers remain fully mounted. Plans, readable reasoning summaries, commands, output, file changes, MCP, collaboration, web, review, and compaction initially mount only a bounded one-line literal summary. Explicit expansion mounts their complete projected text and allowlisted details; collapse unmounts those bodies while stable-row remeasurement restores the reading anchor. Raw reasoning text is excluded before view state. The context panel reports only App Server-derived operational fields, including model, workspace, effective approval/permission/network settings, writable roots, token usage, rate limits, and sanitized counters. Missing fields say not reported rather than being inferred.

Composer state contract

The readiness matrix below describes the unchanged EXEPERT/Supabase source. Codex uses its independent runtime, model-modality, active-turn steering, and pending-submission gates described above.

Visual styling and request readiness are deliberately separate. The composer evaluates text, current attachments, retained image history, model capability, catalog version, setup state, and request lifecycle before wiring a click handler.

StatePrimary action
Whitespace and no validated imageGrey Send; not focusable or clickable
Trimmed text or validated image-only turnLime/accent Send
Image decoding in progressDisabled with a processing explanation
Invalid or oversized attachmentDisabled with the specific attachment error
Images with an older text-only backendDisabled; image input is unavailable
Images with a text-only selected modelDisabled; select a model marked Vision
Session/model setup or catalog loadingDisabled until verified state is ready
Active responseSend becomes Stop in the same slot
Cancellation settlementDisabled until the turn-scoped cancellation event settles

Stop synchronously queues a turn-scoped local cancellation event. The next desktop event poll settles that event, removes the pending job from the composer, and ignores any late result for that turn; it does not wait for the blocking HTTP worker to return. The desktop HTTP transport and 9Router do not expose a provider-side cancellation protocol, so this behavior must not be described as guaranteed upstream abort or billing cancellation.

Image input

Input paths

The desktop accepts images through:

  • the native multi-file picker;
  • clipboard paste when the clipboard's authoritative entry is an image or external path; and
  • native file drag-and-drop over the composer.

Ordinary clipboard text remains owned by the GPUI textarea. Picker cancellation does not create an error. Adding or removing an image returns focus to the textarea.

Processing and limits

Image work runs on one serial background decoder so four large files cannot multiply peak decoded-memory allocations.

LimitContract
Accepted inputPNG, JPEG, WebP
Images per current turn4
Original file20 MiB maximum
Original pixel count40 megapixels maximum
Processed longest edge1600 px
Processed image2 MiB maximum
Retained/request image bytes6 MiB combined maximum
Encoded run-chat JSON request body9 MiB maximum before JSON parsing

The decoder verifies the real file format rather than trusting the filename, applies image orientation, resizes with bounded allocation, and re-encodes the processed image. Alpha images remain PNG; opaque images use bounded JPEG encoding. A fifth image and any size-budget violation produce inline copy.

The attachment tray shows a decoded thumbnail, processed format, dimensions, processing or error state, and a remove action. Transcript previews reuse a runtime cache rather than decoding or copying the processed bytes every render.

Ephemeral attachment and compatibility contracts

The additive request types are backward-compatible:

interface ChatImageAttachment {
kind: 'image'
mime_type: 'image/png' | 'image/jpeg' | 'image/webp'
data_base64: string
width: number
height: number
}

interface ChatMessage {
role: 'user' | 'assistant'
content: string
attachments?: ChatImageAttachment[]
}

interface RunChatRequest {
// Existing fields remain unchanged.
user_input: string
attachments?: ChatImageAttachment[]
}

Blank user_input is valid only when the current turn contains at least one validated attachment. Metadata-only images restored after a restart cannot be resent or retried until attached again.

Processed bytes use reference-counted memory only in the current desktop process. The live transcript retains no more than one request-sized 6 MiB image budget. Older image allocations are discarded while safe format/dimension metadata remains. New Chat clears the runtime attachments and preview cache.

Transcript JSON never serializes the attachment byte field. After restart, a metadata-only card says Image not retained—attach again instead of showing a broken thumbnail.

Explicit Vision capability

The model catalog advertises:

{
"image_input_version": "image-input-v1",
"models": [
{
"supports_images": true,
"image_capability_source": "provider"
}
]
}

Image support is true only when it comes from one of these sources:

  1. explicit provider modality/capability fields;
  2. a route in the server-side ROUTER_VISION_MODELS JSON array; or
  3. a combo whose known members all support image input.

Unknown or missing provenance is unsupported even if a row says supports_images: true. Route names are never treated as evidence. Users may attach images while a text-only model is selected, but Send remains blocked and the desktop never changes the model automatically.

If a backend does not advertise image-input-v1, a new desktop client remains safely text-only. Older clients ignore the optional catalog and request fields. The browser Chat composer intentionally remains text-only.

run-chat validation and routing

Before calling 9Router, the Edge Function:

  1. reads the JSON body through a bounded stream;
  2. validates required request fields and the image-only rule;
  3. validates attachment keys, per-message count, MIME, strict base64, decoded length, declared dimensions, per-item bytes, and combined bytes;
  4. parses PNG chunks through IEND, JPEG frame/scan markers through EOI, or the complete WebP RIFF chunk layout;
  5. confirms the authenticated user owns the requested project;
  6. resolves the requested route without silent image fallback; and
  7. rejects image content unless the resolved route has explicit capability.

Valid user turns become OpenAI-compatible content parts containing text and image_url data parts. The existing exepert-research-v1 structured response, Journal grounding, brain telemetry block, trace/span persistence, and legacy SSE compatibility remain intact.

Stable public image failures are:

  • unsupported_image_model;
  • invalid_image_attachment; and
  • image_payload_too_large.

The selected model's verified output ceiling now controls both context reservation and the provider max_tokens request, bounded by the structured or legacy response limit.

Context measurement

Text-only requests may use a bounded server token estimate when the selected model has a verified context window. Requests containing images do not invent image-token estimates. Until the provider returns prompt usage, their context measurement is unavailable and the UI says so.

When provider prompt_tokens is returned, the server records it as exact unattributed input usage plus the reserved output segment. This preserves an exact total without falsely assigning provider-counted image tokens to text segments.

Privacy and observability

No Supabase Storage bucket or durable cloud image retention is added. The database and local transcript receive metadata, never image content.

Ephemeral does not mean device-only. When the user sends a turn, the processed image bytes cross the authenticated Supabase Edge Function boundary and are forwarded as OpenAI-compatible image content to the explicitly selected 9Router/provider route. The bytes are used for that request and in-memory follow-up context but are excluded from EXEPERT persistence and observability.

Persistence, diagnostics, public errors, traces, and span attributes redact:

  • data:image/... URLs;
  • data_base64 fields;
  • exact processed attachment encodings; and
  • bearer tokens and existing secret patterns.

Observable attachment records are bounded to source, message index where applicable, MIME, dimensions, decoded byte size, total count, and total bytes. Original filenames are never copied into runtime attachment state or persisted records.

Authentication and saved prompts

Desktop Chat bootstraps through the configured client-safe Supabase URL and publishable key. Router and service-role credentials remain inside Edge Functions. The setup flow supports anonymous Chat plus explicit email or OAuth sign-in without writing tokens into the public configuration store.

The Tools menu's Saved prompts action supports local prompt history throughout a session. Signed-in, non-anonymous users can also list, create, rename/update, and delete a user-wide saved-prompt library through chat-saved-prompts.

The chat_saved_prompts table:

  • bounds titles to 160 characters and content to 8000 characters;
  • indexes prompts by owner and update time;
  • enables RLS;
  • defines owner-scoped SELECT, INSERT, UPDATE, and DELETE policies; and
  • rejects anonymous JWTs even though anonymous users use PostgreSQL's authenticated role.

The Edge Function repeats authentication, non-anonymous, user ownership, and field-bound checks before every library operation.

Configuration and rollout

Server configuration adds:

ROUTER_VISION_MODELS=["provider/explicitly-verified-vision-model"]

This is a server-side allowlist and must never be exposed through VITE_*.

For an image-capable rollout:

  1. apply the saved-prompt migration if the library is being released;
  2. deploy chat-saved-prompts;
  3. configure and verify ROUTER_VISION_MODELS;
  4. deploy chat-models so clients can observe image-input-v1;
  5. deploy run-chat; and
  6. distribute the matching desktop executable.

Deployment and live Vision calls require an explicitly authorized Supabase target. Building the client or passing local tests does not prove a hosted Vision route is configured.

Verification evidence

Automated unit and source-contract coverage includes:

  • composer readiness, compact geometry, transcript hierarchy, focus-visible styling, accessibility roles, smart-follow transitions, footer ordering, native Tools semantics, opaque popup contracts, provider-logo fallback, and stable native IDs;
  • image-only turns, picker cancellation, text-paste fallback, and drag/drop;
  • image formats, orientation, resizing, per-item/combined limits, and a fifth attachment;
  • retained-image follow-ups, text-only model gating, retries, cancellation, New Chat cleanup, and metadata-only restart behavior;
  • malformed or MIME-mismatched server payloads and truncated containers;
  • multimodal provider serialization, capability normalization, combo rules, old-client compatibility, output ceilings, and truthful context usage;
  • transcript, error, response, diagnostic, and observability redaction; and
  • saved-prompt SQL policy predicates, JWT configuration, and Edge Function dispatch contracts;
  • Codex intrinsic user-bubble versus full-width response geometry, the 760 px cap, assistant-only Markdown selection, stable Markdown state identity, hard activity folding, verbatim transcript-copy wiring, virtual-list reconciliation, stable scroll bookmarks, adaptive-scroll boundaries, middle-button velocity/cancellation, virtual checkpoint navigation, and unchanged-snapshot suppression;
  • source-qualified Worked/Thought timing placement, provenance precedence, live-to-terminal transitions, exact stopped durations, invalid-data omission, bounded details, DSH reasoning-span merging/deduplication, keyboard and UIA disclosure state, 150 ms/reduced-motion behavior, and the visible 1 Hz ticker;
  • centered ZED streaming/footer geometry, presentation-change remeasurement, and the shared 32 px scroll-to-bottom control with its centered 16 px icon and clamped 28-to-32 px appearance endpoints;
  • global presentation defaulting and version-2 serialization, responsive selector IDs, ZED visual hierarchy, Native provisional replacement, shared transcript bookmarks, Codex disclosure precedence, allowlisted structured details, the unified #181818 workspace, removed status bar, title-bar utilities, and rail/palette/Attention collaboration placement. These are unit and static contract assertions; a hosted RLS behavioral test still requires an explicitly authorized Supabase target.

The standard local gate is:

pnpm typecheck
pnpm test
pnpm.cmd --dir docs-site exec docusaurus build

cd desktop
cargo +1.96.0-x86_64-pc-windows-msvc fmt --all -- --check
cargo +1.96.0-x86_64-pc-windows-msvc test --workspace --locked --target x86_64-pc-windows-msvc
cargo +1.96.0-x86_64-pc-windows-msvc clippy --workspace --all-targets --locked --target x86_64-pc-windows-msvc -- -D warnings
cargo +1.96.0-x86_64-pc-windows-msvc build -p exepert-desktop --release --locked --target x86_64-pc-windows-msvc

Run the opt-in Codex bounds proof separately and serially because it opens a real native window:

cd desktop
cargo +1.96.0-x86_64-pc-windows-msvc test -p exepert-desktop --test codex_timeline_render --locked --target x86_64-pc-windows-msvc codex_timeline_native_bounds_prevent_glyph_columns -- --ignored --exact --test-threads=1
cargo +1.96.0-x86_64-pc-windows-msvc test -p exepert-desktop --test codex_timeline_render --locked --target x86_64-pc-windows-msvc native_virtual_transcript_realizes_overdraw_in_child_view -- --ignored --exact --test-threads=1
cargo +1.96.0-x86_64-pc-windows-msvc test -p exepert-desktop --lib --locked --target x86_64-pc-windows-msvc ui::transcript::tests::native_auto_scroll_frames_move_the_list_and_paint_the_anchor -- --ignored --exact --test-threads=1

The strict workspace Clippy command above is the release gate and must pass with warnings denied; this feature does not rely on lint allowlists.

git diff --check must also pass. The Rust source-contract tests lock the shared opaque surface, custom priority-100 wrappers, absence of a second Tools wrapper, the footprint-preserving padding exchange, and responsive composer wiring. Pure unit tests prove the 8/10/12-row boundaries, populated-draft deferral, near-bottom geometry, and ephemeral unseen-response transitions. Neither launches a Windows window, validates the exposed UI Automation tree, renders DPI-dependent layouts, or proves pointer, keyboard, scroll, shadow, paint-order, or actual textarea reflow behavior.

The default Codex width tests inspect the GPUI style contract and source wiring. A separate opt-in Windows renderer test opens a production GPUI window, captures first-prepaint bounds, and proves that Hi is wider than its padding, full bodies reach the 760 px cap, and a 320 px rich response wraps vertically. The second opt-in harness lays out 500 variable-height transcript rows and 500 independently virtualized checkpoint markers, proves that each remains below 80 realized entries at the harness viewport, and confirms that a child-only notification reuses cached sibling paint. The third drives production auto-scroll frames against a native list and proves that the offset changes while the anchor paints and motion stays armed. They run explicitly with --ignored --exact --test-threads=1; neither certifies the UIA tree, network-loaded media, shell URL activation, physical wheel feel, or a particular Windows display driver. Those remain native interaction evidence.

Native interaction smoke therefore remains a separate release gate. At 1024×640, 1280×720, 1440×800, and 1920×1009, exercise empty, initial-suggestion, long typed and pasted drafts, explicit newlines, width and height resizing, manual clearing, attachment, warning, Send/Stop, failed/Retry, feedback, and long-transcript states. Verify the 8/10/12-row caps, upward growth, internal scrolling after the cap, stable populated drafts across breakpoints, near-bottom preservation, Tab and Shift+Tab order, keyboard focus rings, Windows UIA roles and accessible names, Tools arrow/Enter/Escape behavior, disabled menu items, model-name truncation, Send/Stop alignment, scroll-to-bottom arrow and unseen-dot behavior, DPI scaling, and unclipped popovers and drop overlay. Confirm visually that no transcript or composer line shows through the Tools, model, or prompt surfaces. Live Supabase and Vision-model requests stay disabled unless the exact hosted target is separately authorized.

For Codex transcript smoke, mirror the existing short Hi and longer rich response tasks at 1024×640, 1280×720, and 1920×1009. Confirm that short user bubbles remain readable, assistant and developer bodies use the available width, headings/lists/code wrap without horizontal clipping, selection and Copy Visible Transcript preserve their distinct rendered/source behavior, and transcript scrolling remains single-layer. Exercise Activity shown/hidden, mouse-wheel glide, middle-button auto-scroll and every cancellation path, precision touchpad input, reduced motion, expansion controls, checkpoint mouse/keyboard navigation, task switching, live mirror updates, and tail following; confirm that collapsed activity never exposes a long body and an unchanged one-second poll creates no visible hitch. Link activation and remote image loading are manual networked checks; use a benign public target and confirm the request carries no Codex credential or task metadata.