AGENTS.md@crates/codegen/vtcode-desktop · git:20261103.691275b · 2026-11-03 · sha256 7c85e56a96011d21

AGENTS.md@crates/codegen/vtcode-desktop git:20261103.691275bA

Immutable. This exact content never changes and is served at /api/v1/blob/7c85e56a96011d21.

# vtcode-desktop

[Root AGENTS.md](../AGENTS.md) | macOS-only (Apple silicon) GPUI Kit (`=0.8.1`) app; modules: `view/` (screens), `themes/` (tokens), `preferences/`, `transport.rs`, `localization/`, `app_icon.rs` (artwork + Dock).

## Hard rules

- WCAG AA 4.5:1 for foreground + every accent vs background in all themes (`every_theme_*` tests + `cargo nextest run -p vtcode-ui -E 'test(theme)'`); focus indicators get their own contrast check apart from text.
- All user-facing strings localized (`rust_i18n::t!`, 20 locales, args match `%{...}` placeholders); locale data lives per-surface in `locales/app.<ns>.yml` (one line per key, `_version: 2` first line, no trailing whitespace in values); GUI tests use `#[gpui_kit::test]` with stable `ElementId`s, `lock_scroll_axis()` before `.test_support()`, and `set_reduce_motion(true)` for overlay-click flows (opacity fades stay clickable).
- Every action keyboard-reachable with visible focus; overlays dismiss via `dismiss_overlay` paths and restore focus (`*_return_focus`), new overlays must also bind Escape on the topmost surface and be dropped when `open_settings`/`activate_composer` can mount another route beneath them; rail selections pick `popover` (via `desktop_selected`) vs `secondary` (rails on a `popover` panel, e.g. the license viewer — `popover` on `popover` is invisible); icon-only controls carry an accessible name (+ tooltip when scope needs explaining); never encode meaning by color alone; Buttons for in-app commands (`primary` only for the default commitment, `.danger()` for destructive) with result labels (`Delete`, `Cancel`) and `Delete "Name"?` native confirms (`[Destructive, Cancel]`, `matches!(answer.await, Ok(0))`, untestable headless); Full-access consent instead uses the in-app modal (`view/full_access_confirm.rs`, checkbox-gated Confirm + one-shot `fullAccessConfirmed` attestation, headless-tested); `Switch` for immediately-applied settings.

## Best practice

- Compose from `view/components.rs` (`desktop_button`, `settings_row`, `settings_group_box`, `accessible_group_box`, `centered_empty_state`, `pane_divider`, `nav_group_header`, `card_border_style`, `track_focus`, shimmer labels, turn pending row, `modal_scrim`); same-kind overlay openers toggle via `toggle_palette_kind`/`toggle_settings` (second trigger closes); rem scale (`text_sm`, `gap_2`, `p_4`) and component `Size` APIs over `px()` (pixels only for hairlines, platform insets, hit-test tolerances, and fixed pane/region geometry); `truncate()` long/dynamic text; mono font only for code/ids/shortcuts/aligned numbers; load the `gpui-kit` (coding guides) and `gpui-kit-design-guides` (design guides) skills via `read_skill` before UI work, follow their review checklists, and check `docs/development/desktop-component-review.md` before introducing a component (Applied/Retained/Not-needed decisions stand).
- States visible and honest: distinct hover/focus/selected/disabled/loading; spinners in `muted_foreground`; shimmer labels only while genuinely loading (skeleton viewers were removed in favor of shimmer text); disable submission in flight; errors scoped near the action (`colors.danger`, `text_sm`); keep views free of service/runtime logic.
- Thin response routers: method match arms delegate to `apply_*`/`fail_*` beside the feature's request builders; `fail_pending_navigation()` on async errors; confirm destructive service calls natively and enforce server-side (service validates, client guards).
- Performance: memoize projections per revision/epoch (transcript revision, config catalog epoch, inspector); page long lists (20 rows + `Show more (N remaining)`); virtualize measured rows; remeasure only on typography/theme changes; blocking work via `background_spawn`; no I/O or unbounded loops in render fns. List readers are unsettled before first layout (gate overlay visibility on `transcript_layout_settled`); `on_next_frame` never runs headless (scroll synchronously, assert exactly).

## Typography & accessibility

- Hierarchy via `row_title`/`row_description`, `text_sm` body, `text_xs` metadata; `muted_text` for secondary info, `aa_muted` captions-only; semibold reserved for unread/active/emphasis; verify roles/labels in the test tree (`role()`, `label()`); container aria labels need `role(Group)`; type scales with the user's text-size choice, so verify zoom at several base fonts (wrapping, truncation, pane minima, virtual-list measurement, focus-ring clearance) and keep longer translations from clipping or shifting sibling spines.
- Theme tokens only: no hex/`rgb()`/`hsla()` outside `themes/`; read `cx.theme()` by semantic role (`background`/`foreground`, `muted`, `primary`, `danger`/`warning`/`success` only for their meanings; `chart_*` for categorical segments); floors: secondary copy targets 6:1 where headroom allows, muted text holds 5.0 (AA 4.5 is the absolute floor), chrome 3:1 (`faded_chrome`, `label_foreground`, `tab_foreground`); fills take the weakest separating step with raised keeping a gap above resting while hover takes the strongest readable step; derive via `readable_semantic_color`, `readable_indicator_color`, `accessible_focus_color`, `filled_button_foreground`; verify light + dark, never assume fg/bg polarity; sRGB holds on Display P3 via macOS color management (wide-gamut authoring blocked on kit support; `every_theme_stays_within_srgb_gamut` guards it).

## Gotchas

- Menu-bar close/Cmd-W share `menu_bar::should_hide` (objc2 AppKit bindings); the status item uses the mono template icon (never theme/app-icon art), `menu_bar::dispatch_status_action` (show-first for window-bound actions), and `MenuBarAction::is_allowed` validation through the rendered tree, with labels rebuilt on locale change; cross-window refresh goes through `reload_views`; settings/browser/review embed in the content slot under the persistent toolbar (`route_page`; `RouteHistory` cursor drives Back/Forward with session history as fallback, Close truncates; transient palette/preview/editors layer above); route close/pop always focuses the composer (deterministic landing spot, kept after embedding); Up/Down alias Left/Right settings nav; shortcut capture suppresses bindings by omitting `DESKTOP`/`SETTINGS` key contexts while the pill holds focus (`capture_suppresses_bindings`); below 900px viewports an open panel collapses the sidebar to its rail and settings stacks its nav (`NARROW_PANEL_BREAKPOINT`); windowed-zen drag strip (`zen-drag-strip`, 40px `ZEN_DRAG_STRIP_HEIGHT`) owns window dragging (`app_owns_titlebar_drag` disables ALL native AppKit drag, so zen needs its own `start_window_move` region or the window cannot move) and the traffic-light clearance, titles align to the content spine (no horizontal dodge); headless `start_window_move` is unimplemented — guard native drag calls with `#[cfg(not(test))]` and drive later nav via dispatched actions; window min-size is 600x560 (`desktop_window_options`); config groups rail sits beside content at >=960px viewport and stacks above it below (arithmetic breakpoint, untested with users); Kit `Button::toggled` is a11y-only; `border`/`input` are soft visible outlines (Codex-style hairlines, never transparent) feeding cards, dividers, Tags, and Kit inputs — give `GroupBox` fills their outline via `content_style(card_border_style())`, since Kit offers no fill+border variant; Kit `Button` centers its built-in icon/label pair (inner content is `justify_center` + `size_full`), so `.justify_start()` cannot left-align entries — rails use a custom icon+label child with an explicit `accessibility_label` (config rail); storage search keywords are plain substrings; storage mutations refetch usage and review protects the current session; shortcuts persist on Save/import and reinstall live (NoAction shadows retire old chords); fixed composer/transcript key contexts (`VTCodeComposer`/`VTCodeTranscript` in `init_keybindings`) reuse Kit input actions for readline parity (undo-preserving) while configurable commands stay in `shortcuts.rs` (overrides colliding with `FIXED_RESERVED_CHORDS` are rejected — keep it in sync with the fixed table); Tab-stop lives on the `FocusHandle` (`.tab_stop(true)` at creation), element `.tab_stop(false)` only opts out, and `.test_support()` must precede `.track_focus()` or headless `focused()` panics; headless `press` does not deliver `ctrl-z` (drive Undo via `cmd-z` press or menu dispatch); transcript follows THREAD_ID ordering with role labels from `t!`; Reasoning items carrying a diagnostic payload project to collapsible severity notices (`TranscriptItem::diagnostic_notice` + `toggle_notice`, rendered by `view/transcript_notice.rs`), payload-less reasoning stays an assistant message, and session restore skips Reasoning rows (archive text owns history); Kit `Radio` owns its focus handle internally (no caller roving — Tab plus shortcuts instead; checked/disabled rows attach no click handler, so radio-card pickers need an explicit commit control) and owns no hover state (cards add their own `.hover()` refinement, gated on enabled + unchecked); Kit `Badge` is count/dot/icon only (text pills are `Tag`); icons outside Kit's 106-icon default bundle must join `ExtraIcons` + `DESKTOP_ICONS` (`desktop/assets.rs`) or they render blank with no headless failure (native-only symptom).
- `secondary` == `group_box`/`sidebar`/`muted` surface fill: selections on those surfaces must use `popover`, never `secondary`, or the highlight is pixel-identical to the background (native-verified 2026-10-31; enforced by `desktop_selected`). Inverse on a `popover` panel (license viewer): select with `secondary` + `.toggled(..)`.
- Session changes while a turn runs route through the running-confirm modal (`view/running_confirm.rs`): UI entries use `*_from_ui`, confirm arms the intent, the running→idle transition drains it; every `running = false` writer must drain or drop the armed wait, and armed/modal state clears in `reset_session_state` plus the Exited/LaunchFailed/disconnect arms.
- Native menus are startup-static (`init_native_menus`): labels cannot track state (Enter/Exit Full Screen is one toggling item); menu actions dispatch through window-level `on_action` (About → `view/about.rs`, topmost modal in `dismiss_overlay`) or app-level handlers (OpenHelp). File transfer is platform-bound: Finder drops arrive as `ExternalPaths` only (promise-only sources are refused by AppKit with the no-drop cursor — the bundle must never declare `NSIsTouchNative`, packaging refuses it, since it would disable the touch-to-mouse emulation); gate drop listeners at attachment time on workspace presence; drag-out via `.on_drag` + `.external_drag_payload::<T>` promotes to a native session at window exit (`files::external_drag_payload` maps paths — the headless platform records promotions but nothing public reaches tests, so test the mapping as a pure fn). `window.is_fullscreen()` is headlessly testable (flag flip); `zoom_window` is not (needs `#[cfg(not(test))]`).
- Cross-env handoff: the slash catalog is service-owned, so desktop-only `/cli` is appended and CLI-only `/desktop` (intercepted natively as an already-here notice — the service arm would spawn a second instance) is filtered in `localize_slash_catalog` (`view/cli_handoff.rs`); new `CommandSkillSpec`s auto-appear in the desktop popup via `visible_commands_for_desktop`, so every addition needs a native route or a documented fallthrough. External launches (editors/terminals) live in `files/launch.rs` behind the fixed-argv boundary; success paths are covered by launch-interception tests, never real spawns. CLI `/desktop` hands off via `--session`/`--workspace` argv parsed once in `main` (`desktop/startup.rs`, unknown args ignored for Finder `-psn`) or, when a bundle claims the scheme, a `vtcode://` URL delivered through `on_open_urls` into a queueing inbox (`UrlInbox`) and dispatched to the `MainWindow` global (weak view + window handle); same-workspace links load directly via the UI entry, cross-workspace links arm pending navigation. The thread loads through pending navigation, and the CLI resume command is `terminal_command_for_session` (single builder shared with Settings → Connections).
- New user-visible strings stay English literals unless full native-quality translations ship for every locale (partial keys break per-locale completeness tests); reuse existing keys where possible.