Rules here apply everywhere. Before changing code in an area listed under Area conventions, read that area's document; its rules bind like this file. dev-docs/INDEX.md lists every document.
- Reuse the existing token & utility layers before introducing new CSS variables or custom properties. Extend
src/styles/tokens.css/src/styles/utilities.cssif a shared pattern is needed. - Place new styles beside their peers in focused files (≈150 lines or less) under
src/styles/{components|messaging|panels}/and@importthem from the matching aggregate entry file (src/styles/controls.css,messaging.css,panels.css), which holds only imports. Reusable patterns (buttons, selectors, dropdowns) live insrc/styles/components/; never redefine the same utility classes elsewhere. - Dialog, popover and floating-window headers, toolbars, bodies, footers, titles and actions use the shared
.window-*primitives (src/styles/components/window.css). Persistent utility windows and transcript-covering previews putcomponents/window-close-button.tsxat the upper right of their existing top toolbar, never in a dedicated row; transient popups such as transcript filters dismiss on outside interaction and have no close button. - Corners are square unless the user explicitly requests otherwise for a specific change. Round exceptions: Yolo, MCP and plugin activation switches and the lone-setting Settings switch (
settings/settings-switch.tsx), all sharingstyles/components/switches.cssgeometry; floating message scroll buttons; tags and numeric/context/token labels via--chip-radius(alias--pill-radius; register badge variants instyles/components/badges.css, use.badge-shapefor utility-styled labels, never a local radius). Other chrome, including the fixed panel toggles, stays square. - Pick backgrounds by surface role (
styles/tokens.css), never by a colour that merely looks close:--surface-field(the base colour) is reserved for what users type into (composer, inputs, textareas, search rows);--surface-canvasfor reading areas (transcript, editors, diffs, main panel bodies);--surface-overlayfor floating windows, dialogs, popovers, tooltips and every menu or list picker;--surface-inset(also--surface-code) for code, shell, log and diagnostics output and status cards;--surface-controlfor buttons, segmented controls and select triggers; icon controls that rest flush on a canvas (Files panel toolbars, session header buttons) take the canvas they sit on. The three non-field roles follow the title-bar tint (--surface-shade-strong). Utilities.bg-surface-{field,canvas,overlay,inset}mirror them; consumers that need concrete colours (Monaco, MUI, extension frames) resolve roles throughlib/css-color.ts. - Menus share one item treatment (
--menu-item-hover-bg/--menu-item-selected-bg) across Kobalte menus, selectors and native<select>.styles/components/native-select.cssstyles every native select globally: the closed control matches selector triggers (control surface, chevron) and, whereappearance: base-selectand content-box clipping are supported (Electron, WebView2), the open list matches the shared menus and long values stay on one line, clipped before a background-drawn chevron; elsewhere the native select remains. Customizable selects size to the shown value, so auto-width selects need a stable feature width (container units, not percentages of shrink-to-fit parents). Do not style individual selects as fields. - The
.icon-toggleactive state (aria-pressed/aria-expanded/data-active) is one shared rule instyles/utilities.cssfor every palette; never add per-button or per-palette active overrides (exact tint, exception and contrast test indev-docs/UI_CONVENTIONS.md). - SUID buttons never animate on press (
MuiButtonBasedisableTouchRippletheme default inlib/theme.tsx), like the Message content trigger; the keyboard focus ripple remains their focus-visible cue, squared instyles/components/buttons.css. Do not re-enable press ripples per component, nor disable the focus ripple without another focus cue. - Document new conventions where agents will find them: rules that apply everywhere belong in this file; an area's contract belongs in that area's document, under Agent conventions when it fits nowhere else in the body (create the document and add it to the index below when none fits).
- Documentation (this file,
dev-docs/, skills) describes the current state only. Keep no history: removed features and their names, former behavior ("formerly", "no longer", "replaced by"), PR/commit/date provenance, dated validation logs or abandoned designs; Git holds them. Code that reads older data is described as current behavior. Do not add sentences denying something ("X is not Y", "this does not prove…"); when one would counter a statement elsewhere, correct or delete that statement instead. Prohibitions that tell an agent what never to do remain instructions.
- Keep modules narrowly scoped with only the public API callers need (KISS); share helpers through dedicated modules instead of copy/paste (DRY); split files when concerns diverge (state, actions, API, events); prefer composable primitives (signals, hooks, utilities) over deep inheritance or implicit global state; isolate platform integrations (SSE, IPC, SDK) in thin adapters surfacing typed events/actions.
- Memoize action-menu item arrays (
createMemo, or stable per-row arrays) instead of rebuilding them with fresh JSX icons on each evaluation behind<For>or intoActionOverflowMenu. An item whose icon renders in two places at once (inline buttons and the overflow menu, as inFileRowActionsor tool-call headers) creates the icon in a getter or comes from an unmemoized builder such as message-block'sheaderMenuItems: one DOM node cannot be inserted twice. - Follow
dev-docs/CACHE_REFRESH_CONVENTIONS.mdfor display snapshots, coalesced trailing refreshes, stale-response fencing and authoritative mutation reads. Check existing feature semantics before adding another cache or refresh policy. - CodeNomad integrates with an independently installed and updated OpenCode service: compatibility depends only on the API/capability contracts actually consumed; the versions pinned to build or test CodeNomad only make its own build reproducible. Never turn tested versions into a daemon allowlist or block a runtime/feature only because its version is untested; a version-specific restriction needs a demonstrated failing test or an identified breaking change in a consumed contract, cited with why the restriction is necessary. Review upstream changes and affected callers rigorously (private API use needs contract scrutiny) instead of exhaustively retesting unchanged contracts. Missing capabilities must fail safely without destructive or mass-loading fallbacks.
- Never modify OpenCode for Missions autonomy: use and qualify existing native service/plugin mechanisms, and never infer an upstream-extension requirement from the absence of a named scheduling API.
Paths are under packages/ui/src/ or packages/server/src/ unless stated.
- Missions (
missions/,opencode/missions-plugin.ts,server/routes/mission-*,host-lifetime/; UIcomponents/mission-*,components/settings/missions-settings-section.tsx,stores/mission-*,styles/panels/mission-*):dev-docs/MISSIONS.md; recurring schedules and Wayfinder human gates (its "Wayfinder human decision mark" section)dev-docs/MISSIONS_RECURRING.md. - Questions, permissions and pending-request recovery (interruption dock, permission receipts,
question-data.ts,server/routes/pending-requests.ts,workspaces/pending-discovery.ts,stores/pending-recovery-*):dev-docs/NATIVE_INTERRUPTION_UX.md. - Composer (
components/prompt-input*,prompt-input/,PromptContextControls,/btw):dev-docs/COMPOSER.md. - Transcript (message rendering, Message content and presets, token lines, tool renderers,
stores/message-v2/,stores/opencode-data.ts,lib/native-execution-events.ts):dev-docs/TRANSCRIPT.md; long-text boundsdev-docs/TEXT_DISPLAY_BOUNDS.md. - Timeline, transcript scrolling and history search (
virtual-follow-list.tsx,timeline-*,stores/timeline-previews.ts, history search):dev-docs/SESSION_HISTORY_NAVIGATION.md,dev-docs/SESSION_HISTORY_QUERIES.md. - Shell, panels, tabs, session sidebar and native menus (
instance/shell/,tab-scroll.tsx, session list,lib/native/menu-labels*):dev-docs/UI_SHELL.md. - Session state and recovery (
stores/session-state.ts,stores/session-generation-recovery.ts):dev-docs/SESSION_RECOVERY.md. - Dialogs, utility windows, icon toggles, surfaces and scrollbars (
alert-dialog.tsx,dismissible-window.tsx, auth recovery,styles/utilities.css,index.css):dev-docs/UI_CONVENTIONS.md. - Settings and Preferences (settings rows/actions, Preferences window and project context, Providers/Web search, Stats, notifications, bug report):
dev-docs/SETTINGS.md; provider accounts and usagedev-docs/PROVIDER_ACCOUNTS.md; hotkeysdev-docs/HOTKEYS.md; palettesdev-docs/PALETTE_SOURCES.md; plugin controlsdev-docs/PLUGIN_CONTROLS.md; right-panel customization and panel extensionsdev-docs/PANEL_EXTENSIONS.md. - Projects and tabs (closing tabs, temporary workspaces, new-instance launcher, Git-unavailable mode):
dev-docs/PROJECTS.md. - Files panel and Git (Workspace/Changes/Commits, file watches, Git init, whitespace filter):
dev-docs/FILES_PANEL.md. - Worktrees and event ownership (
workspaces/native-worktrees.ts,workspaces/worktree-inventory.ts):dev-docs/WORKTREE_SESSION_PLACEMENT.md. - OpenCode runtime, setup, upgrades and OpenCode settings:
dev-docs/OPENCODE_RUNTIME_SETUP.md; bundled plugin builds, provisioning, presence readiness, the event relay and the Design agent and skills (opencode/design/):dev-docs/OPENCODE_COMPATIBILITY.md; connection budget:dev-docs/CACHE_REFRESH_CONVENTIONS.md; session environment:dev-docs/SESSION_ENVIRONMENT.md; pruning:dev-docs/SESSION_PRUNING_RPC.md. - OpenCode integrations (
opencode/integrations/,integration-types.ts; fixturesscripts/fixtures/fake-integration/,scripts/fixtures/fake-npm-registry.mjs):dev-docs/OPENCODE_INTEGRATIONS.md. - Remote Control and Remote Access settings:
dev-docs/REMOTE_CONTROL.md; SideCars:dev-docs/architecture.md. - Browser previews and automation:
dev-docs/BROWSER_AUTOMATION.md,dev-docs/BROWSER_DEVICE_EMULATION.md. - Desktop hosts: data profiles
dev-docs/DESKTOP_DATA_PROFILES.md; updates and update feeddev-docs/TAURI_UPDATER.md; Tao/Tauri backportspackages/tauri-app/vendor/README.md; bundled Node/npmdev-docs/RUNTIME_MAINTENANCE.md. - Public help (
docs/help/, help entry points):dev-docs/USER_HELP.md.
Never hardcode user-visible strings. The UI uses a small custom i18n layer (packages/ui/src/lib/i18n/index.tsx).
- Use
useI18n()(const { t } = useI18n();) in components andtGlobal(...)in stores/non-component code; never importenMessagesin feature code. - Messages are flat dot-key TypeScript objects (
"flat.dot.keys": "string") inpackages/ui/src/lib/i18n/messages/<locale>/, merged by each locale'sindex.tsviamessages/merge.ts(duplicate keys throw at build time). Add a string to the righten/*.tspart file and the same key to every other locale's matching file: missing keys silently fall back to English, then the key. - Placeholders are simple
{name}replacements (word characters only; never{file-name}); plurals use separate keys such assomething.one/something.other, chosen in code. - A new language needs a
messages/<locale>/folder +index.ts, an entry inSUPPORTED_LOCALES(lib/i18n/index.tsx) and an option incomponents/locale-selector.tsx. - The selected locale is the
localeapp preference, persisted via the server config (default~/.config/codenomad/config.yaml;config.jsonis migration input only).
File size is a refactoring signal, never by itself a reason to refactor. When a change touches a source file over ~500 lines (target limit ~800) or a test file over ~1000, mention its path and approximate line count in your final response; keep new files under these thresholds unless there is a clear reason.
- Modify existing files with the
edittool; usewriteonly to create new files. - Browser rendering regressions live in
packages/ui/tests/browser/(deterministic HTTP fixtures infixtures/) and exercise the real Solid components and native event dispatcher, never reimplemented rendering logic. Runnpm run test:browser --workspace @codenomad/uiafternpx playwright install chromium;CODENOMAD_BROWSER_PATHmay select an existing Chromium executable. - Concurrent Vite browser fixtures use
tests/browser/fixture-cache.tsfor per-server optimized-dependency caches; close the server before disposing its owned temporary cache. Never clear the checkout's shared cache to repair a fixture or make parallel tests share mutable optimizer output. The one sanctioned exception is the runner-owned fixture daemon (dev-docs/BROWSER_TESTING.md): each run spawns one daemon child that owns a single cache dir and serves read-only borrowers over loopback, stopping with an authenticated request when the run ends.
- Native automation instrumentation starts automatically. Do not configure a fixed CDP port or a manual WebView2 profile.
- Rebuild Electron before calling
codenomad.act({ action: "restart" }). For Windows Tauri, stop and relaunch the release executable only when the linker cannot replace it; never stop the shared OpenCode daemon.
- A PR that changes user-visible behavior (labels, controls, steps, defaults, limitations) updates the affected help pages in the same PR: English first, then the nine translations using each locale's UI labels (
dev-docs/USER_HELP.md). Help publishes fromdev, and CI checks only its structure and links. Answer the non-blockinghelp-update-reminder.ymlcomment (English UI messages changed withoutdocs/help) before merging: update the help, or confirm nothing user-facing is documented there.
- Commit messages: a concise conventional-style subject, then body paragraphs explaining why the change exists, the user-visible behavior change, the implementation approach, important edge cases or platform considerations, and the validation or tests that prevent regressions.