docs(agent): streamline guidance and skills

Keep always-on instructions concise and route specialized work through focused skills. Split large skills into progressive references and add dedicated change, desktop, sync, and performance guidance.
This commit is contained in:
Bohdan Triapitsyn
2026-07-14 00:45:44 +03:00
parent b36afbf5ee
commit 68f1c1efe3
20 changed files with 1257 additions and 1371 deletions
+4 -51
View File
@@ -1,6 +1,6 @@
---
name: clack-cli-patterns
description: Use when creating or modifying terminal CLI commands, prompts, or output formatting in OpenChamber. Enforces Clack UX standards with strict parity and safety across TTY/non-TTY, --quiet, and --json modes.
description: Use when creating or modifying OpenChamber CLI commands, prompts, terminal output, non-TTY behavior, `--quiet`, or `--json` behavior.
license: MIT
compatibility: opencode
---
@@ -150,56 +150,9 @@ For each command/subcommand, manually verify:
4. non-TTY behavior (e.g. piped)
5. error path in both human and json modes
## Copy/Paste Snippets
## Reusable Snippets
### Prompt Guard
```js
if (canPrompt(options)) {
const value = await select({
message: 'Choose an option',
options: [{ value: 'a', label: 'Option A' }],
});
if (isCancel(value)) {
cancel('Operation cancelled.');
return;
}
}
```
### Non-Interactive Fallback
```js
if (!resolvedValue) {
if (canPrompt(options)) {
// prompt path
} else {
throw new Error('Missing required value. Provide --flag <value>.');
}
}
```
### Spinner Guard
```js
const spin = createSpinner(options);
spin?.start('Running operation...');
// ...work...
spin?.stop('Done');
```
### JSON vs Human Output
```js
if (options.json) {
printJson({ ok: true, data });
return;
}
intro('Operation');
log.success('Completed');
outro('done');
```
Load `references/snippets.md` when implementing prompt guards, non-interactive fallback, spinner lifecycle, or JSON/human output branching.
## Implementation Checklist
@@ -211,6 +164,6 @@ outro('done');
## References
- Policy source: `AGENTS.md` (CLI Parity and Safety Policy)
- This skill is the canonical CLI parity and safety policy.
- Terminal CLI precedent: `packages/web/bin/cli.js`
- Output adapter precedent: `packages/web/bin/cli-output.js`
@@ -0,0 +1,50 @@
# CLI Output Snippets
## Prompt Guard
```js
if (canPrompt(options)) {
const value = await select({
message: 'Choose an option',
options: [{ value: 'a', label: 'Option A' }],
});
if (isCancel(value)) {
cancel('Operation cancelled.');
return;
}
}
```
## Non-Interactive Fallback
```js
if (!resolvedValue) {
if (canPrompt(options)) {
// prompt path
} else {
throw new Error('Missing required value. Provide --flag <value>.');
}
}
```
## Spinner Guard
```js
const spin = createSpinner(options);
spin?.start('Running operation...');
// ...work...
spin?.stop('Done');
```
## JSON vs Human Output
```js
if (options.json) {
printJson({ ok: true, data });
return;
}
intro('Operation');
log.success('Completed');
outro('done');
```
+50
View File
@@ -0,0 +1,50 @@
---
name: desktop-shell
description: Use when changing Electron main/preload code, desktop IPC, native windows, menus, dialogs, notifications, updater behavior, deep links, SSH or tunnels, child processes, packaged startup, or Windows process spawning.
---
# Desktop Shell
## Read First
Read `packages/electron/README.md` and nearby `packages/electron` code before editing.
## Runtime Boundary
- Electron boots `@openchamber/web` in the same Node process and loads the UI over loopback. Do not introduce a sidecar server process.
- Keep OpenCode feature backends and shared domain logic in web/server or runtime APIs.
- Keep Electron focused on inherently native behavior: windows, menus, dialogs, notifications, updater, deep links, runtime host switching, privileged IPC, SSH, and tunnel lifecycle.
- Shared renderer-facing contracts belong in `packages/ui`; shared server behavior belongs in `packages/web`.
- Electron is the desktop release target.
## IPC And Security
1. Add a preload bridge shape only when renderer-facing capability changes.
2. Handle the native operation in `main.mjs`.
3. Gate privileged commands in the main process; renderer checks are not security boundaries.
4. Expose the narrowest payload and never expose filesystem, shell, tokens, or host secrets to remote pages.
5. Do not import Electron from shared UI code.
Remote runtime pages must not gain local desktop privileges. Treat deep links, host imports, stored credentials, and runtime switching as trust-boundary operations.
## Windows Background Processes
Non-user-visible child processes must never flash a console window.
- Spawn the target executable directly with `windowsHide: true`.
- Use `stdio: 'ignore'` for detached/background helpers and call `unref()` when they must outlive Electron.
- Avoid `cmd.exe /c`, batch shims, `taskkill`, `ping` delays, and pipelines that create console grandchildren. `windowsHide` reliably controls only the directly spawned process.
- Prefer native Node/Electron APIs when available.
- For delayed work that must survive app exit, spawn one first-level hidden helper, such as `powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ...`; perform delay and work inside that process with cmdlets.
- Omit hidden-process behavior only for intentionally user-visible terminals or applications.
## Packaging And Lifecycle
- Keep native/external modules configured according to `packages/electron/README.md` and `bundle-main.mjs`.
- Preserve startup, quit, updater, notification, and deep-link behavior across development and packaged builds.
- Ensure cleanup tolerates partial startup and repeated shutdown signals.
- Do not infer readiness from stdout when an in-process callback or returned server handle exists.
## Validation
Run the Electron package type-check/lint commands from `package.json` and focused tests. For startup, preload, routing, or packaging changes, test both HMR development and bundled UI mode. For Windows process work, inspect the complete process tree and verify no console flash; a successful command alone is insufficient.
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: drag-to-reorder
description: Use when implementing drag-to-reorder / sortable lists or chips in OpenChamber with @dnd-kit — covers the correct setup for BOTH desktop and mobile (touch), the variable-width "stretch" fix, the wrapping multi-row strategy choice, and the pitfalls (infinite update loop, offset overlay) we already hit and fixed.
description: Use when implementing or modifying OpenChamber sortable or drag-to-reorder behavior, especially `@dnd-kit`, touch/mobile interactions, variable-width items, or wrapping layouts.
license: MIT
compatibility: opencode
---
@@ -0,0 +1,122 @@
---
name: openchamber-change-discipline
description: Use when implementing, fixing, refactoring, or otherwise modifying OpenChamber source code, dependencies, exports, build configuration, generated assets, package contracts, or module ownership.
---
# OpenChamber Change Discipline
## Core Principle
Make the smallest complete change and validate at the narrowest level that covers the real risk.
Identify existing behavior covered by tests or callers; preserve it unless the requested change explicitly replaces it.
## Before Editing
1. Read the nearest `DOCUMENTATION.md` and package `README.md` when present.
2. Inspect nearby implementation and tests before introducing a pattern.
3. Load every additional project skill whose trigger matches the change.
4. Classify the highest applicable change risk below.
5. Identify affected consumers, runtimes, persisted data, and public exports.
When instructions materially conflict, stop and resolve the conflict instead of silently choosing one.
## Risk Classification
| Risk | Examples | Planning consequence |
|---|---|---|
| Local implementation | Private helper or component behavior in one package | Preserve observable behavior; validate the owning package |
| Module contract | Exported API/type or documented module invariant | Inspect consumers; update contract tests and owning docs |
| Cross-workspace contract | Shared UI/runtime/package shape consumed by multiple workspaces | Trace every actual consumer and runtime; validate across workspaces |
| Persisted or external behavior | Stored settings/data, routes, IDs, files, CLI output | Define compatibility, round-trip, failure, and conversion behavior for existing consumers |
| Platform/runtime behavior | Electron, VS Code, mobile, relay, native or packaged behavior | Run the relevant runtime/build/integration validation |
Apply every matching category. Do not escalate local work into workspace-wide ritual, and do not treat a type-only export as local merely because it emits no JavaScript.
## Mandatory Rules
- Identify existing behavior covered by tests or callers; preserve it unless explicitly replaced.
- Do not add dependencies unless explicitly requested.
- Do not add compatibility paths without a concrete persisted or external consumer.
- Enforce security and correctness in core logic, not only UI controls or prompts.
- Never add, persist, or log secrets, bearer tokens, pairing data, or sensitive user content.
- Make data loss, partial failure, rollback, and fallback behavior explicit.
- Update owning documentation when module ownership, contracts, or invariants change.
- Complete the cumulative validation required by every applicable risk category.
## Engineering Preferences
- Prefer the smallest correct change; avoid drive-by refactors.
- Keep orchestration entrypoints thin and move domain logic to focused modules.
- Prefer explicit dependencies and dependency injection over hidden module coupling.
- Follow local TypeScript types; avoid `any`, blind casts, and guessed payload shapes.
- Prefer early returns and explicit branches over nested conditionals.
## Review Prompts
Before broadening a change, ask:
- Is the new abstraction reused or merely possible to reuse?
- Is the code in the package that owns the behavior?
- Does the change alter shared UI contracts across web, desktop, VS Code, or mobile?
- Does it change persisted data, IDs, routes, exports, generated files, or package entrypoints?
- Can failure leave optimistic state, caches, files, or remote state stranded?
For partial or destructive flows, answer explicitly:
- What remains valid after the first failure?
- What is rolled back or cleaned up?
- What can be retried or resumed safely?
- What does the user observe?
For persisted data, require a migration only when existing stored data needs conversion. Test downgrade compatibility only when older application versions are a concrete supported consumer. "Rollback" means preserving/restoring valid state after a failed write or migration unless a broader contract explicitly says otherwise.
Do not hide a required architectural migration behind a local heuristic. Do not turn a local fix into a speculative rewrite.
## Validation Matrix
Use `package.json` scripts as the command source of truth.
| Change | Minimum validation |
|---|---|
| Executable source | Focused tests plus package-scoped type-check and lint |
| Cross-workspace/shared contract | Workspace-wide type-check and lint plus affected builds/tests |
| Added/deleted/renamed source file, export/type/entrypoint/import shape | `bun run dead-code` in addition to relevant checks |
| Persisted or external contract | Compatibility and round-trip tests; conversion/malformed-old-data tests when old data needs migration; failed-write/migration rollback tests |
| Dependency or lockfile | Workspace-wide checks and affected builds |
| Generated asset | Regeneration check plus consumer build/test |
| Docs-only or isolated config | Narrow syntax/schema/link validation; do not run unrelated full suites |
| Platform/runtime behavior | Relevant runtime build or manual/integration check; static checks are insufficient |
Use a sufficiently long timeout for broad checks. Report exactly what ran and what did not.
Choose affected builds/tests by tracing real consumers and runtime boundaries, not by running everything reflexively.
For type-only shared contracts, validate compile-time consumers. Add runtime serialization tests when the contract crosses a process, persistence, network, or untyped JavaScript boundary.
## Test Design
- Prefer observable contracts, state transitions, failure handling, rollback, and operation counts.
- Test private helpers through public/module behavior when that captures the risk clearly.
- Assert internal map shape, helper calls, or call order only when that structure/order is itself a contract.
- Keep refactor tests resilient to equivalent internal implementations.
- For behavior-preserving refactors, establish the current behavior before changing structure.
## Completion Standard
- Implement the behavior end to end, including rollback and cleanup.
- Run focused regression tests for the changed contract.
- Preserve unrelated changes encountered in shared files.
- Re-read the owning docs and update them when the implementation changed their truth.
- Do not claim runtime, relay, performance, or platform correctness from type-check/lint alone.
## Common Failure Modes
| Failure | Correction |
|---|---|
| Refactoring nearby code while fixing one bug | Keep the diff scoped unless the nearby change is required |
| Adding a helper used once | Keep direct code until reuse or composability is real |
| Swallowing an error for smoother UX | Preserve the failure signal and handle presentation separately |
| Updating a bridge without all runtimes | Load the runtime/API skill and make parity explicit |
| Running only broad checks | Add focused tests that exercise the changed behavior |
| Running only focused checks after a shared-contract change | Add workspace-wide validation |
@@ -0,0 +1,184 @@
---
name: performance-engineering
description: Use when implementing or reviewing code on interaction, render, event, polling, synchronization, list-processing, store-selector, cache, indexing, or high-volume data paths; when users report lag, freezes, jank, high CPU, memory growth, slow startup, or performance regressions; and before accepting memoization or caching as a fix for repeated work.
---
# Performance Engineering
## Overview
Optimize the amount and frequency of work before optimizing individual operations.
**Core principle:** Make expensive work structurally unnecessary. A fast inner function still freezes the app when called millions of times on the main thread.
## Start With A Performance Contract
Define before editing:
| Dimension | Required answer |
|---|---|
| Interaction | Which user action or event must remain responsive? |
| Scale | Realistic and worst-known entity counts |
| Budget | Target latency, frame time, CPU, memory, or operation count |
| Path | Main thread, worker, server, network, disk, or mixed |
| Semantics | Ordering, ownership, freshness, failure, and partial-data invariants |
Do not optimize against a toy fixture when the report provides production scale.
## Workflow
### 1. Reproduce And Measure
- Reproduce the exact interaction, not a nearby helper in isolation.
- Separate scripting, rendering, painting, network, disk, and waiting time.
- Use a profiler to identify total time and self time.
- Add operation counters when timings are noisy: selector calls, normalizations, scans, allocations, sorts, notifications.
- Capture a baseline before changing code.
Do not infer a bottleneck from code appearance when a trace or counter can identify it.
### 2. Write The Cost Equation
Name every multiplying dimension:
```text
consumers × events × projects × sessions × candidate paths
```
For each factor, record:
- cardinality at production scale;
- update frequency;
- whether work happens on the main thread;
- whether multiple consumers independently derive the same result.
Treat hidden fanout as real work. Equality checks may prevent renders while selectors, aggregation, sorting, and allocation still execute.
### 3. Map Sources, Derived State, And Lifetimes
Classify each input:
- authoritative or partial;
- live or historical;
- stable or high-frequency;
- successful empty result or fetch failure;
- globally complete or complete only for one entity.
Define invalidation before adding a cache. Prefer a stronger source of truth over inference.
For destructive consumers, represent completeness explicitly. An incomplete empty bucket means "unknown", not "delete everything".
Track completeness at the smallest destructive scope. One failed project/entity blocks cleanup for itself, not for unrelated complete scopes.
### 4. Remove Work In This Order
1. **Skip:** gate disabled paths and return on no-op updates.
2. **Narrow:** subscribe to the exact entity/field that can affect the result.
3. **Share:** compute identical derived data once for all consumers.
4. **Index:** represent the lookup direction the UI actually needs.
5. **Increment:** update only affected buckets/entities and preserve other references.
6. **Cache:** reuse pure results with explicit keys, invalidation, and memory bounds.
7. **Schedule:** defer, chunk, or move genuinely unavoidable CPU work off the interaction path.
8. **Micro-optimize:** tune regexes, loops, and allocations only after structural multipliers are gone.
Do not jump to a worker to hide avoidable work. Do not add a global store when a local shared index has the correct lifetime.
## Structural Pattern
Replace repeated questions with maintained answers:
```ts
// Bad: every consumer asks every item about every owner.
for (const project of projects) {
const items = sessions.filter((session) => belongsTo(project, session, topology));
}
// Good: resolve ownership once, then read direct buckets.
const sessionsByProject = new Map<string, Session[]>();
for (const session of sessions) {
const projectId = ownership.resolve(session.directory);
if (projectId) append(sessionsByProject, projectId, session);
}
```
Prefer indexes keyed by stable IDs. Keep high-frequency runtime state out of metadata indexes unless it changes membership.
## React And Store Hot Paths
- Subscribe to leaf values, not broad collections.
- Preserve references for unaffected entities and buckets.
- Keep streaming state out of broadly consumed stores.
- Never rely on `React.memo`, `useMemo`, or Zustand equality to prevent selector execution upstream.
- Do not sort structural lists from token/delta-frequency fields.
- Coalesce repeated same-entity events and skip no-op reducer updates.
- Ensure hidden or disabled surfaces perform no ongoing work.
- Preserve scroll position synchronously with `useLayoutEffect`; do not wait visible frames before compensation.
- Distinguish viewport resize from content growth and avoid fighting browser scroll anchoring.
- Avoid textarea auto-size shrink/expand cycles when content only grows.
- Freeze structural ordering during high-frequency updates and reorder at an explicit lifecycle edge.
## Caching Rules
Add a cache only when all are explicit:
- exact key and source identity;
- invalidation events;
- stale-result behavior;
- memory count and byte bounds where values can grow;
- runtime/project/user isolation where identities can collide;
- proof that caching removes enough work to meet the budget.
A cache inside an `O(consumers × entities × candidates)` loop is a mitigation, not automatically a complete fix.
## Verification
Require both correctness and performance guards:
- representative-scale fixture from the report;
- cold and warm paths when caching exists;
- median plus p95/max, not one lucky run;
- deterministic operation-count assertion when possible;
- repeated-event test for streaming/polling paths;
- no-op and unrelated-entity update tests;
- reference-stability test for unaffected buckets;
- failure, partial-data, empty-success, and stale-async-completion tests;
- memory/cache growth check for long-running paths;
- production build or equivalent runtime profile for UI interactions.
State what was not measured. Never claim a freeze is fixed from type-check and unit tests alone.
## Hotfix Policy
Ship a bounded cache-only or local mitigation under deadline pressure only when:
- it measurably meets the user-facing budget at reported scale;
- invalidation and memory behavior are correct;
- semantics are unchanged or explicitly accepted;
- remaining complexity is documented as follow-up work.
If the interaction remains above budget, do not call the mitigation the completed performance fix.
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "The helper is cheap" | Multiply it by events, entities, candidates, and consumers. |
| "No component rerendered" | Selectors and equality comparisons may still burn CPU. |
| "`useMemo` fixes it" | Memoization does not help when dependencies churn or consumers duplicate work. |
| "The cache made it 10× faster" | Compare the result with the interaction budget, not only the baseline. |
| "Projects are few" | Identify the dimension that is large and the dimensions multiplying it. |
| "Move it to a worker" | Moving waste changes responsiveness, not total cost or data correctness. |
| "Empty means nothing exists" | Empty after failure or partial loading is not authoritative absence. |
| "We can optimize later" | Add a scale regression now or the multiplier will return. |
## Exit Checklist
- [ ] Exact interaction and production scale reproduced.
- [ ] Cost equation written and dominant multipliers removed.
- [ ] Sources of truth, completeness, and invalidation explicit.
- [ ] No broad subscription or render-time global scan on a high-frequency path.
- [ ] Unaffected references remain stable.
- [ ] Partial failure cannot trigger destructive cleanup.
- [ ] Representative benchmark meets the stated budget.
- [ ] Operation-count or repeated-event regression test prevents recurrence.
- [ ] Correctness, type, lint, and relevant runtime validations pass.
+13 -1
View File
@@ -1,6 +1,6 @@
---
name: relay-transport
description: Use when adding or changing any WebSocket, SSE, or streaming endpoint (terminal, dictation/voice, event stream, notifications), opening a WebSocket in shared UI, refactoring the runtime transport (runtime-fetch/runtime-url/runtime-switch/runtime-auth), touching anything under packages/ui/src/lib/relay or packages/web/server/lib/relay, or porting a realtime feature. These changes silently break OpenChamber's private relay (mobile→desktop over an E2EE tunnel) in ways that pass local/desktop testing and only fail over the relay on a real device. Load this before such work to know the invariants and the traps already hit.
description: Use when adding or changing OpenChamber WebSocket, SSE, streaming, realtime endpoints, shared UI sockets, runtime transport internals, private relay behavior, or files under the UI/server relay modules.
license: MIT
compatibility: opencode
---
@@ -44,6 +44,18 @@ Adding a new WS endpoint (or porting one, e.g. the planned terminal port) requir
- Relay mode routes through `runtime-switch` (activates the tunnel singleton), `runtime-fetch` (routes runtime requests through it), `runtime-url`/`runtime-socket` (tunnel-backed URLs/sockets), and `runtime-auth` (mints the URL token through the tunnel). When refactoring any of these, preserve the relay branch and the direct-URL/Electron-realtime-proxy branches — they must remain byte-identical in behavior for non-relay runtimes.
- **The host dispatcher never injects credentials.** Tunneled requests carry the client's own token; the server authenticates them. Do not add host-side auth shortcuts, and do not trust loopback source address as authentication (relay traffic arrives at loopback but represents remote clients).
## Reconnect pacing
For indefinite SSE/WebSocket reconnect loops:
- Use exponential backoff based on consecutive failures, not a constant short delay.
- Use the long backoff cap while `navigator.onLine` is false or `document.visibilityState` is hidden.
- Treat permanent 4xx responses as long-backoff failures; keep 408 and 429 retryable.
- Make waits interruptible by `online`, visibility becoming visible, and the pipeline abort signal.
- Reset failure state only after a genuinely healthy connection.
Blind short retries on hidden, offline, unauthorized, or stale-path clients waste battery and flood server logs.
## Testing guidance (a stub that skips auth/origin hides the exact bugs)
- Exercise the real auth and origin gates. An end-to-end test whose stub server accepts any WS upgrade will pass while the real server rejects it — this is precisely how the origin-check bug shipped. When writing a relay integration test, mirror the real gates (`ensureSessionToken` via `oc_url_token`, `isRequestOriginAllowed`) or run against the real server pieces.
+47 -270
View File
@@ -1,291 +1,68 @@
---
name: settings-ui-patterns
description: Use when creating or modifying UI components, styling, or visual elements related to Settings in OpenChamber.
license: MIT
compatibility: opencode
description: Use when creating or modifying OpenChamber Settings pages, dialogs, controls, configuration surfaces, responsive Settings layouts, or Settings search behavior.
---
# Settings UI Patterns Skill
# Settings UI Patterns
## Purpose
This skill provides instructions for creating or redesigning Settings pages, informational panels, and configuration interfaces within the OpenChamber application.
## Required Companion Skills
## Current Canonical Look (2026)
Use this as source of truth for new settings UI work.
- Load `theme-system` for colors, buttons, icons, and visual states.
- Load `locale-ui-patterns` for every visible string, tooltip, placeholder, and accessible label.
- Load `ui-api-decoupling` when a setting reads/writes runtime data or adds a capability.
- **Flat hierarchy first**: Prefer spacing + typography hierarchy over boxed backgrounds.
- **No unnecessary wrappers**: Avoid extra section wrappers that mix unrelated controls.
- **No redundant section titles**: Do not add headers like `Theme Preferences` or `Scaling & Layout` when controls are already self-explanatory.
- **Compact controls**: Option chips and radio rows should be dense, not tall.
- **Left-leading state icon**: Radio/checkbox state icon appears before text.
- **Subtle state contrast**: Inactive radio labels should be visibly dimmer than active labels.
- **Minimal row chrome**: Avoid row hover/background highlighting by default; keep only where explicitly needed.
When examples conflict, shared component/theme and localization contracts win. Stop on unresolved material conflicts.
## Typography Guidelines
Always utilize the standard OpenChamber typography classes defined in `packages/ui/src/lib/typography.ts`.
## Canonical Direction
- **Page Title**: Use `typography-ui-header font-semibold text-foreground` for the top-most title of a settings page/dialog.
- **Section Header**: Use `typography-ui-header font-medium text-foreground` for settings sections (e.g. `Notification Events`, `Session Defaults`).
- **Control Group Header**: Use `typography-ui-header font-medium text-foreground` (or `font-normal` if it reads too loud) for grouped controls inside a section (e.g. `Default Tool Output`, `Diff Layout`).
- **Values / Primary Text**: Use `typography-ui-label text-foreground`. Add `tabular-nums` if displaying numbers or stats to ensure vertical alignment.
- **Option Labels**: Use non-bold label text in compact option controls (`font-normal` when needed to override).
- **Meta / Helper Text**: Use `typography-meta text-muted-foreground` or `typography-small text-muted-foreground` for supplemental text.
- Prefer flat hierarchy built with spacing and typography.
- Avoid unnecessary cards, wrappers, row chrome, and redundant headings.
- Keep controls compact and align related rows consistently.
- Put checkbox/radio state before labels.
- Use subtle, stable selected-state styling without layout shifts.
- Preserve responsive wrapping/stacking and long-text behavior.
## Layout and Spacing Patterns
## Load References By Task
### 1. Main Backgrounds
Main wrappers should generally use `bg-background` or `bg-[var(--surface-background)]`. Ensure adequate padding (e.g., `px-5 py-6` or `p-6`).
| Task | Required reference |
|---|---|
| Page hierarchy, typography, spacing, columns, responsive grids | `references/layout.md` |
| Chips, radios, checkboxes, numeric overrides, inputs, icon actions, pickers | `references/controls.md` |
| Adding/moving controls, pages, availability, anchors, or search entries | `references/search.md` |
### 2. Subsection Grouping
Group related controls with vertical spacing, not mandatory cards.
Load every matching reference before editing.
- Use `space-y-3` between logical subsections.
- Use `p-2` for subsection internal padding.
- Avoid adding `bg-[var(--surface-elevated)]` unless there is a clear reason.
- Avoid extra row decorations (`rounded-md`, hover fills) unless there is explicit UX value.
## Quick Control Selection
### 3. Header-to-Content Hierarchy (critical)
When removing cards/background wrappers, spacing must be rebalanced so header ownership stays clear.
| Need | Shared pattern |
|---|---|
| Short selectable options | `Button variant="chip" size="xs"` + `aria-pressed` |
| Mutually exclusive mode list | `Radio` rows |
| Boolean | `Checkbox` |
| Numeric value/override | `NumberInput` |
| Text/path | `Input` with shared adjacent actions |
| Icon-only action | `Button size="icon"` + sprite `Icon` + localized `aria-label` |
- Keep **section-to-section spacing larger** than **header-to-own-content spacing**.
- Typical pattern:
- header wrapper `mb-1 px-1`
- content wrapper `pt-0 pb-2 px-2`
- outer section spacing `mb-8`
- Do not leave legacy `mb-3` style gaps after flattening a section; it makes headers look detached.
Do not introduce `ButtonSmall`, direct Remixicon components, hardcoded user-facing strings, or one-off color/button systems.
### 4. Headerless Blocks (when context is obvious)
If the page title already provides enough context, remove redundant local headers and place controls directly below the title.
## Settings Search Contract
- Example: project page identity controls can sit directly under project name/path.
- Tighten top gap for this pattern (e.g. top header `mb-4` instead of larger section spacing).
Every stable Settings control addition or move must consider search in the same change:
```tsx
<div className="space-y-3">
<section className="p-2">...</section>
<section className="p-2">...</section>
</div>
```
- explicit registry item in `packages/ui/src/lib/settings/search.ts` when searchable;
- matching `data-settings-item` anchor;
- localized title/description keys;
- availability matching actual render conditions;
- state preparation before highlighting conditional targets.
## Structural Patterns
Dynamic entity rows normally are not indexed. Load `references/search.md` for exact rules.
### 1. Segmented Option Buttons (compact)
Use for short option sets where button-style segmented choice reads best (e.g. Default Tool Output).
## Review Checklist
```tsx
<div className="mt-1 flex flex-wrap items-center gap-1">
<ButtonSmall
variant="outline"
size="xs"
className={cn('!font-normal', isSelected ? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10' : 'text-foreground')}
>
Collapsed
</ButtonSmall>
</div>
```
### 2. Radio Option Lists (compact rows)
Use for mutually exclusive mode/layout settings (e.g. Diff Layout, Diff View Mode).
- Use shared `Radio` component from `@/components/ui/radio`.
- Icon first, label second.
- Row container compact: `py-0.5`.
- Inactive label can use `text-foreground/50`.
```tsx
<div role="radiogroup" aria-label="Diff layout" className="mt-1 space-y-0">
<div className="flex w-full items-center gap-2 py-0.5">
<Radio checked={selected} onChange={onSelect} ariaLabel="Diff layout: Dynamic" />
<span className={cn('typography-ui-label font-normal', selected ? 'text-foreground' : 'text-foreground/50')}>Dynamic</span>
</div>
</div>
```
### 3. Checkbox Setting Rows
Use shared `Checkbox` component from `@/components/ui/checkbox` for boolean toggles.
- Icon first, text immediately after (`gap-2`).
- Typical row spacing for checkbox rows: `py-1.5`.
- Keep row click and keyboard toggle support.
- Prefer checkbox over binary show/hide button pairs for pure boolean state.
```tsx
<div
className="group flex cursor-pointer items-center gap-2 py-1.5"
role="button"
tabIndex={0}
>
<Checkbox checked={value} onChange={setValue} ariaLabel="Show Dotfiles" />
<span className="typography-ui-label text-foreground">Show Dotfiles</span>
</div>
```
### 4. Invisible Two-Column Alignment
Use consistent label/control columns across settings rows so controls align on a shared vertical line.
- Desktop row pattern: `flex items-center gap-8`
- Label column width: `w-56 shrink-0`
- Control cluster: `w-fit`
```tsx
<div className="flex items-center gap-8 py-1.5">
<span className="typography-ui-label text-foreground w-56 shrink-0">Interface Font Size</span>
<div className="flex items-center gap-2 w-fit">...</div>
</div>
```
#### Disabled control rule
If a control is unavailable, disable the control only. Do not dim the label row by default.
#### Width-matching rule
When matching visual widths across different rows, compare full row footprint (control + adjacent action buttons), not just input width.
### 5. Theme Row Composition
For theme controls in Appearance:
- `Color Mode` header on first line; option chips below it.
- `Light Theme` and `Dark Theme` on one row where possible, wrapping on small widths.
- Keep selectors near labels and aligned to existing column rhythm.
- Replace persistent helper text with an info tooltip icon near the related action.
```tsx
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
<div className="flex min-w-0 items-center gap-2">Light Theme ...</div>
<div className="flex min-w-0 items-center gap-2">Dark Theme ...</div>
</div>
```
### 6. Numeric Controls in Settings
Use compact stepper input (`- value +`) plus reset button.
- Prefer shared `NumberInput` stepper style over slider + numeric combo in dense settings pages.
- Keep reset button adjacent to control (`gap-2`).
- Avoid using Tailwind `overflow-hidden` on mobile for controls; `packages/ui/src/styles/mobile.css` forces `.overflow-hidden { overflow-y: auto !important; }`.
Use `overflow-x-hidden overflow-y-hidden` if you truly need clipping.
- Touch devices: `packages/ui/src/styles/mobile.css` enforces `min-height: 36px` on `button`. If you build custom segmented controls with `<button>`, ensure the container height can accommodate that (e.g. `h-9`).
#### Optional numeric overrides
For "override unless empty" fields (e.g. agent Temperature/Top P), keep the value optional and provide a fallback for stepping.
```tsx
<NumberInput
value={temperature}
fallbackValue={0.7}
onValueChange={setTemperature}
onClear={() => setTemperature(undefined)}
min={0}
max={2}
step={0.1}
inputMode="decimal"
emptyLabel="—"
/>
```
```tsx
<div className="flex items-center gap-2 w-fit">
<NumberInput value={fontSize} onValueChange={setFontSize} min={50} max={200} step={5} />
<ButtonSmall variant="ghost" className="h-7 w-7 px-0">...</ButtonSmall>
</div>
```
### 7. Inputs and Select Triggers (settings density)
Keep form controls in settings compact and aligned.
- Prefer `Input` with `className="h-7"` in dense settings rows.
- Prefer default `SelectTrigger` sizing (avoid `size="lg"` in settings).
- For icon-only actions next to inputs, use `ButtonSmall` with `h-7 w-7 p-0`.
```tsx
<div className="flex items-center gap-2">
<Input className="h-7" />
<ButtonSmall variant="outline" size="xs" className="h-7 w-7 p-0" aria-label="Browse">
<RiFolderLine className="h-4 w-4" />
</ButtonSmall>
</div>
```
### 8. Template Grids (text fields)
For template-like settings (title/message pairs), use a simple grid and flat cells.
- Grid: `grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-3`
- Cell: `section p-2`
- Field: `Input className="h-7"`
### 9. Icon/Color Picker Rows
For dense icon/color pickers in settings:
- Place options under the field label when they are a palette/grid choice.
- Use stable selected-state styling (`border`/`ring`/subtle background), avoid transform jumps (`scale-*`).
- Keep chip size compact (`h-7 w-7`) and spacing consistent (`gap-2`).
## Control Selection Rules
- **Use compact option buttons** for short, chip-like selection groups.
- **Use radios** for explicit mode/layout choices where list scanning is better.
- **Use checkboxes** for true/false settings.
- **Avoid show/hide button pairs** when a checkbox maps directly to the boolean.
- **Do not couple unrelated toggles** under one synthetic section header; keep hierarchy clear.
## Settings Search Integration
Every Settings UI addition must preserve item search. The registry is explicit: search does not scrape JSX or infer fields automatically.
### Required Files
- Add or update search items in `packages/ui/src/lib/settings/search.ts`.
- Add matching `data-settings-item="..."` anchors in the rendered Settings UI.
- Reuse existing localized labels/descriptions where possible; otherwise add keys to all `packages/ui/src/lib/i18n/messages/*.settings.ts` files.
- If adding a new top-level Settings page, add metadata in `packages/ui/src/lib/settings/metadata.ts` and at least one searchable item unless the page is purely navigational like `home`.
### What To Index
- Index stable user-facing controls, section headers, and static create/connect actions.
- Use item IDs that match the page and target, for example `appearance.language`, `agents.mode`, `remote-instances.client-auth`.
- Prefer the exact visible label key as `titleKey`; use a concise visible/help text key as `descriptionKey` only when it adds useful context.
- Add `keywords` for common synonyms, acronyms, and words users may type that are not in the label.
### What Not To Index
- Do not generate search items from dynamic entities: individual agents, commands, MCP servers, snippets, plugins, skills, providers, projects, catalog rows, remote hosts, or SSH instances.
- Do not index controls hidden behind selected-entity dialogs unless search selection prepares the required state before highlighting.
- Do not add a registry entry for a conditional control unless its `isAvailable` guard matches actual render visibility.
### Split Page Pattern
For split pages, search should target predictable static surfaces only.
- Index sidebar create/connect actions like `agents.create` or `providers.connect`.
- Index editor fields/sections that exist after the existing search preparation opens a draft.
- If a new create result needs draft setup, update `prepareSettingsSearchTarget` in `SettingsView.tsx` so the target is rendered before highlight runs.
### Availability Guards
- Match runtime/page availability exactly: VS Code, web, desktop, mobile, and local desktop origin when relevant.
- Page-level guards belong in `metadata.ts`; item-specific guards belong in `search.ts`.
- If a target renders only inside desktop shell UI, guard it with `ctx.isDesktop` or `ctx.isDesktopLocalOrigin` as appropriate.
### Highlight Target Rules
- Put `data-settings-item` on the smallest stable container that visually owns the setting.
- Avoid adding layout-only wrappers just for search anchors.
- Highlight styling is intentionally subtle and lives in `packages/ui/src/index.css` under `[data-settings-search-highlight="true"]`; keep it token-based and non-aggressive.
### Audit Checklist
- All registry IDs have matching anchors.
- All `titleKey` and `descriptionKey` values exist in every settings locale file.
- Every non-navigational `SettingsPageSlug` has item coverage.
- Search results respect platform/runtime/mobile visibility.
- Query-empty Settings navigation behavior is unchanged.
## Best Practices
- **Density**: Keep options compact; avoid oversized rows/chips in dense settings pages.
- **Consistency**: Reuse shared controls (`Checkbox`, `Radio`, `ButtonSmall size="xs"`) instead of inline icon logic.
- **Reuse via composition**: Prefer a single settings component with a `visibleSettings` subset (like `OpenChamberVisualSettings`) for multiple tabs (Appearance/Chat) instead of duplicating markup.
- **Hierarchy**: Page title = `font-semibold`; section header = `font-medium`; control group header = `font-medium` (or `font-normal` if needed); option labels = non-bold.
- **Subsection depth**: Nested subgroup headings under a section should usually be one step lighter than parent heading weight.
- **Hierarchy sanity check**: after flattening UI, verify visual grouping by spacing first (not color).
- **Helper blocks**: For small notes/errors under a section, use `mt-1 px-2` with `typography-meta text-muted-foreground/70` (and status token for errors).
- **Truncation**: Always consider long text. Use `min-w-0 flex-1 truncate` on text containers that sit next to buttons or icons to prevent layout breakage.
- **Theme Variables**: *Always* use CSS variables for colors (e.g., `var(--status-success)`) rather than hardcoded hex values or generic Tailwind colors when indicating semantic states.
- **Search compatibility**: When adding or moving a Settings control, update the search registry and anchor in the same change.
- Hierarchy reads through spacing and typography without unnecessary boxes.
- Shared controls are used with localized visible/accessibility text.
- Desktop alignment degrades cleanly on narrow/mobile layouts.
- Disabled state affects the control, not unrelated labels, unless intentional.
- Long labels and adjacent actions do not overflow.
- Search registry, anchor, localization, and availability agree.
- Nearby Settings precedent and relevant tests remain consistent.
@@ -0,0 +1,91 @@
# Settings Controls
Load `theme-system` for button/icon/color contracts and `locale-ui-patterns` for every visible or accessible string.
## Choosing A Control
- Short chip-like option set: shared `Button variant="chip" size="xs"` with `aria-pressed`.
- Explicit mutually exclusive list: shared `Radio`.
- Boolean value: shared `Checkbox`, not paired show/hide buttons.
- Numeric value: shared `NumberInput`.
- Text/path value: shared `Input` plus shared actions.
Do not couple unrelated toggles beneath a synthetic heading.
## Segmented Option
```tsx
<Button variant="chip" size="xs" aria-pressed={isSelected}>
{t(labelKey)}
</Button>
```
## Radio Row
```tsx
<div role="radiogroup" aria-label={t(groupLabelKey)}>
<div className="flex items-center gap-2 py-0.5">
<Radio checked={selected} onChange={onSelect} ariaLabel={t(labelKey)} />
<span className={cn('typography-ui-label', selected ? 'text-foreground' : 'text-foreground/50')}>
{t(labelKey)}
</span>
</div>
</div>
```
## Checkbox Row
```tsx
<div className="flex cursor-pointer items-center gap-2 py-1.5">
<Checkbox checked={value} onChange={setValue} ariaLabel={t(labelKey)} />
<span className="typography-ui-label">{t(labelKey)}</span>
</div>
```
Preserve row click and keyboard behavior when the container is interactive.
## Optional Numeric Override
Empty means “inherit/default.” Provide fallback stepping and explicit clear:
```tsx
<NumberInput
value={temperature}
fallbackValue={0.7}
onValueChange={setTemperature}
onClear={() => setTemperature(undefined)}
min={0}
max={2}
step={0.1}
inputMode="decimal"
emptyLabel="—"
/>
```
Keep reset adjacent. Prefer an info tooltip over persistent helper text when the explanation is secondary.
## Inputs And Icon Actions
```tsx
<div className="flex items-center gap-2">
<Input className="h-7" />
<Button variant="outline" size="icon" aria-label={t(browseLabelKey)}>
<Icon name="folder" className="size-4" />
</Button>
</div>
```
- Prefer compact inputs in dense rows.
- Avoid large select triggers in Settings.
- Use shared `Button` and sprite `Icon`, never wrapper buttons or direct Remixicon imports.
## Mobile Constraints
- `packages/ui/src/styles/mobile.css` may force `.overflow-hidden` to scroll; use explicit x/y clipping only when required.
- Touch CSS enforces minimum button height. Do not put custom segmented buttons in a container too short for them.
## Picker Rows
- Place icon/color palettes beneath their label.
- Keep option dimensions and gaps consistent.
- Use stable border/ring/background selection; avoid scale transforms that shift layout.
@@ -0,0 +1,53 @@
# Settings Layout
## Visual Hierarchy
- Prefer spacing and typography over boxed backgrounds.
- Avoid wrappers that mix unrelated controls.
- Omit redundant headings when page context already names the controls.
- Keep controls compact and row chrome minimal.
- Place checkbox/radio state before its label.
- Dim inactive option labels subtly; do not use transform jumps.
## Typography
Use classes from `packages/ui/src/lib/typography.ts`:
- Page title: `typography-ui-header font-semibold text-foreground`
- Section header: `typography-ui-header font-medium text-foreground`
- Control group: `typography-ui-header font-medium` or `font-normal` when needed
- Values/labels: `typography-ui-label text-foreground`
- Helper/meta: `typography-meta text-muted-foreground` or `typography-small text-muted-foreground`
- Numeric values: add `tabular-nums`
## Spacing
- Keep section-to-section spacing larger than header-to-content spacing.
- Typical flat section: header `mb-1 px-1`, content `pt-0 pb-2 px-2`, outer `mb-8`.
- Group related controls with `space-y-3` and modest internal padding such as `p-2`.
- Avoid elevated backgrounds, rounded rows, and hover fills without explicit UX value.
## Alignment
For consistent desktop columns:
```tsx
<div className="flex items-center gap-8 py-1.5">
<span className="w-56 shrink-0 typography-ui-label">{t(labelKey)}</span>
<div className="flex w-fit items-center gap-2">...</div>
</div>
```
- Let narrow layouts stack or wrap.
- Compare the complete control footprint, including adjacent actions, when matching widths.
- Disable only the unavailable control; do not dim the entire label row by default.
## Responsive Grids
Use a one-column base and introduce columns at a deliberate breakpoint:
```tsx
<div className="grid grid-cols-1 gap-2 md:grid-cols-[14rem_auto] md:gap-x-8" />
```
Template fields commonly use `grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-3` with flat `p-2` cells.
@@ -0,0 +1,42 @@
# Settings Search
Settings search uses an explicit registry; it does not scrape JSX.
## Required Integration
- Add/update items in `packages/ui/src/lib/settings/search.ts`.
- Add a matching `data-settings-item="..."` anchor to the rendered setting.
- Use localized labels/descriptions from every `packages/ui/src/lib/i18n/messages/*.settings.ts` dictionary.
- For a new top-level page, add metadata in `packages/ui/src/lib/settings/metadata.ts` and searchable content unless the page is purely navigational.
## Registry Rules
- Index stable controls, section headers, and static create/connect actions.
- Use IDs matching page and target, such as `appearance.language`.
- Prefer the visible label key as `titleKey`.
- Add `descriptionKey` only when it improves context.
- Add useful synonyms/acronyms as keywords.
- Do not generate items for dynamic entities such as individual agents, providers, projects, skills, hosts, or sessions.
## Conditional Targets
- Do not index a target hidden behind selected-entity state unless search selection prepares that state first.
- Keep item `isAvailable` identical to actual render visibility.
- Put page-level availability in `metadata.ts` and item-specific guards in `search.ts`.
- Distinguish desktop shell from local desktop origin when the feature requires local privileges.
- For split pages, index predictable static surfaces and update `prepareSettingsSearchTarget` when a result must open a draft/editor before highlighting.
## Highlight Anchor
- Put `data-settings-item` on the smallest stable container that visually owns the setting.
- Do not add layout-only wrappers solely for search.
- Keep highlight styling token-based and subtle; it lives under `[data-settings-search-highlight="true"]` in `packages/ui/src/index.css`.
## Audit
- Every registry ID has a matching anchor.
- Every title/description key exists in every Settings locale.
- Every non-navigational page has appropriate coverage.
- Search visibility matches platform/runtime/mobile rendering.
- Conditional state is prepared before highlight.
- Empty-query Settings navigation remains unchanged.
@@ -0,0 +1,109 @@
---
name: sync-state-invariants
description: Use when changing session synchronization, bootstrap or reconnect state, event reducers, polling, optimistic updates, message queues, live activity, ordering/reconciliation, runtime-scoped caches, or directory-dependent session behavior.
---
# Sync State Invariants
## Read First
Read `packages/ui/src/sync/DOCUMENTATION.md` and the nearest owning module documentation before editing.
## Sources Of Truth
Classify every input before deriving state:
| Input | Valid use |
|---|---|
| Directory child store | Live per-directory session/message/status/permission state |
| Global sessions store | Complete global active/archived cache and retention/sidebar coverage |
| Persisted history/cache | Startup continuity and context restoration, never proof of current activity |
| Optimistic shadow state | Temporary UI continuity until authoritative reconciliation |
Prefer deterministic authoritative records over heuristics. Derive live behavior from live channels, not historical anomalies.
## Failure Is Not Empty
Any authoritative loader whose result can replace, delete, or clear state must distinguish failure from successful empty data.
Use an existing pattern:
- Throw when an outer logical block can catch and preserve prior state.
- Return `T | null` when follow-up work must continue and `null` exclusively means fetch failure.
Never swallow an SDK/API error into `[]`, `{}`, or another valid empty success. Verify that callers skip destructive replacement after failure.
Track completeness at the smallest entity/scope. One failed project or directory blocks destructive work for itself, not for unrelated complete scopes.
## Live And Historical State
- Use historical state to restore context, not to infer ongoing execution.
- Scope delayed-live fallbacks to the active entity and clear them when authoritative state arrives.
- Do not let stale persisted data keep a fallback active indefinitely.
- Define field precedence when global and local/live snapshots feed the same view.
- Use one ordering/rank source for all views of the same entities.
## Event Reducers
- Clone only fields the event mutates; preserve every unrelated reference.
- Return no change for semantically identical events.
- Gate scans behind cheap event/entity checks.
- Coalesce repeated same-entity events without violating ordering.
- Reject stale async/event completions using generation or authoritative timestamps.
- Do not widen a narrow fallback to arbitrary historical records.
For streaming-frequency work, also load `performance-engineering`.
## Polling And Bootstrap
- Preserve rich fields when lightweight polling omits them.
- Use cheap change detection before heavy per-directory fetches.
- Treat startup 502/503 as transient with bounded retry/recovery.
- A retry loop requires a real failure signal; swallowed errors disable retries.
- Preserve previous authoritative state during transient bootstrap/reconnect failures.
## Optimistic Updates
- Insert optimistic data into the visible store and a separate shadow tracker.
- Use client-generated IDs accepted and echoed by the server to reconcile in place.
- Remove optimistic data from both visible and shadow state on failure.
- Reconcile deterministically on authoritative fetch/event; do not guess from unrelated events.
- Stabilize callbacks stored in module-level refs to avoid effect loops.
## Session And Queue Consistency
- Capture provider, model, agent, variant, and other send configuration when queueing.
- Do not re-resolve queued configuration from mutable current state at send time.
- Preserve server-backed attachments and convert paths at the transport boundary.
- Pass a directory hint when a newly created session is not indexed yet.
- Read mutable current directory at call time; never cache it in a long-lived closure.
## Cache And Lifecycle
- Match session-store limits to loaded data before events can trigger trimming.
- Invalidate message/prefetch/file caches on mutation and session eviction.
- Key runtime-scoped caches by runtime identity when IDs or paths can collide.
- Clean optimistic and local cache state after partial failures.
## Verification
Cover the relevant lifecycle, not only static state:
- fresh bootstrap and successful empty result;
- fetch failure preserving prior state;
- reconnect/retry and stale completion;
- repeated/no-op/out-of-order events;
- optimistic success, reconciliation, and rollback;
- create, stream, abort, permission, archive/delete, and revisit when session behavior changes;
- partial multi-directory/project failure;
- runtime or worktree switch with dynamic directory resolution.
## Red Flags
- Fetch helper catches and returns `[]`.
- Historical message/session data drives a live spinner.
- One failed entity blocks or clears all entities.
- Light polling overwrites fields it did not fetch.
- Queue reads current model/agent at send time.
- New session lookup assumes SSE already indexed it.
- Optimistic data has no shadow entry or rollback.
+55 -325
View File
@@ -1,350 +1,80 @@
---
name: theme-system
description: Use when creating or modifying UI components, styling, visual elements, or icons in OpenChamber. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes. All icons must use the shared Icon component from the SVG sprite system - never import from @remixicon/react directly.
license: MIT
compatibility: opencode
description: Use when creating or modifying OpenChamber UI components, styling, colors, buttons, visual states, themes, or icons.
---
## Overview
# Theme System
OpenChamber uses a JSON-based theme system. Themes are defined in `packages/ui/src/lib/theme/themes/`. Users can also add custom themes via `~/.config/openchamber/themes/`.
## Core Rules
**Core principle:** UI colors must use theme tokens - never hardcoded hex colors or Tailwind color classes.
- Use semantic OpenChamber theme tokens; never hardcode hex colors or generic Tailwind palette colors.
- Use shared UI primitives before introducing feature-local controls.
- Use the shared `Button`; do not create button wrappers such as `ButtonSmall` or `ButtonLarge`.
- Use the sprite-based `Icon`; never import icons directly from `@remixicon/react`.
- Apply hover tokens only to interactive elements.
- Use status colors only for actual status/feedback.
- Use selection tokens for selected state and primary tokens for primary actions.
## When to Use
## Load References By Task
- Creating or modifying UI components
- Working with colors, backgrounds, borders, or text
- **Working with icons — adding, changing, or creating icon usages**
| Task | Required reference |
|---|---|
| Choosing colors/tokens or reviewing styled examples | `references/tokens-and-examples.md` |
| Adding, converting, storing, or generating icons | `references/icons.md` |
| Adding built-in or custom themes | `references/adding-themes.md` |
## Quick Decision Tree
Load every matching reference before editing. Settings work must also load `settings-ui-patterns`; user-facing or accessible text must load `locale-ui-patterns`.
1. **Code display?**`syntax.*`
2. **Feedback/status?**`status.*`
3. **Primary CTA?**`primary.*`
4. **Interactive/clickable?**`interactive.*`
5. **Background layer?**`surface.*`
6. **Text?**`surface.foreground` or `surface.mutedForeground`
## Token Decision
## Critical Rules
1. Code display -> `syntax.*`
2. Error/warning/success/info -> `status.*`
3. Primary CTA -> `primary.*`
4. Hover/pressed/focus -> `interactive.*`
5. Selected/active state -> `interactive.selection*`
6. Background/text/border layer -> `surface.*` and semantic utility classes
- `surface.elevated` = inputs, cards, panels
- `interactive.hover` = **ONLY on clickable elements**
- `interactive.selection` = active/selected states (not primary!)
- Status colors = **ONLY for actual feedback** (errors, warnings, success)
- Input footers = `bg-transparent` on elevated background
Prefer CSS variables/classes for component styling. Use `useThemeSystem()` only when an API requires resolved color values.
## Button Rules (MANDATORY)
## Button Contract
Use only the shared `Button` component from `packages/ui/src/components/ui/button.tsx`.
Use `Button` from `packages/ui/src/components/ui/button.tsx`.
- Do not create wrapper button components (for example `ButtonLarge`, `ButtonSmall`).
- Do not hardcode button height/padding classes when a `size` variant exists.
- Use semantic button variants consistently; avoid ad-hoc one-off button styling.
| Variant | Use |
|---|---|
| `default` | Primary local action |
| `outline` | Visible secondary action |
| `secondary` | Soft secondary action |
| `ghost` | Quiet row/toolbar action |
| `destructive` | Destructive action |
| `chip` | Compact selectable option with `aria-pressed` |
| `link` | Rare inline text action |
### Allowed Button Variants
| Size | Use |
|---|---|
| `xs` | Dense row/list control |
| `sm` | Compact action |
| `default` | Standard action |
| `lg` | Prominent action |
| `icon` | Icon-only square action |
| Variant | Use for | Token direction |
|-------|-------|-------|
| `default` | Primary action in a local section/dialog | `primary.*` |
| `outline` | Secondary visible action | `surface.elevated` + `interactive.*` |
| `secondary` | Soft secondary action | `interactive.hover` / `interactive.active` |
| `ghost` | Low-emphasis row/toolbar action | transparent + `interactive.hover` |
| `destructive` | Destructive actions (`Delete`, `Revert all`) | `status.error*` |
| `link` | Rare inline text action only | text-link style |
Do not hardcode button height/padding when a size variant exists. Do not recreate selection/destructive styling with ad-hoc classes.
### Allowed Button Sizes
| Size | Use for |
|------|---------|
| `xs` | Dense controls in rows/lists |
| `sm` | Default compact action buttons |
| `default` | Standard form/page actions |
| `lg` | Prominent large actions |
| `icon` | Icon-only square button |
### Button Selection Quick Guide
1. Main CTA in section/dialog -> `default`
2. Side action next to CTA -> `outline`
3. Quiet auxiliary action -> `ghost`
4. Dangerous action -> `destructive`
5. Tiny row action -> keep same variant, set `size="xs"`
### Never Use
- Hardcoded hex colors (`#FF0000`)
- Tailwind colors (`bg-white`, `text-blue-500`, `bg-gray-*`)
- Deprecated: `bg-secondary`, `bg-muted`
## Usage
### Via Hook
```tsx
import { useThemeSystem } from '@/contexts/useThemeSystem';
const { currentTheme } = useThemeSystem();
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
```
### Via CSS Variables
```tsx
<div className="bg-[var(--surface-elevated)] hover:bg-[var(--interactive-hover)]">
```
## Color Tokens
### Surface Colors
| Token | Usage |
|-------|-------|
| `surface.background` | Main app background |
| `surface.elevated` | Inputs, cards, panels, popovers |
| `surface.muted` | Secondary backgrounds, sidebars |
| `surface.foreground` | Primary text |
| `surface.mutedForeground` | Secondary text, hints |
| `surface.subtle` | Subtle dividers |
### Interactive Colors
| Token | Usage |
|-------|-------|
| `interactive.border` | Default borders |
| `interactive.hover` | Hover on **clickable elements only** |
| `interactive.selection` | Active/selected items |
| `interactive.selectionForeground` | Text on selection |
| `interactive.focusRing` | Focus indicators |
### Status Colors
| Token | Usage |
|-------|-------|
| `status.error` | Errors, validation failures |
| `status.warning` | Warnings, cautions |
| `status.success` | Success messages |
| `status.info` | Informational messages |
Each has variants: `*`, `*Foreground`, `*Background`, `*Border`.
### Primary Colors
| Token | Usage |
|-------|-------|
| `primary.base` | Primary CTA buttons |
| `primary.hover` | Hover on primary elements |
| `primary.foreground` | Text on primary background |
**Primary vs Selection:** Primary = "click me" (CTA), Selection = "currently active" (state).
### Syntax Colors
For code display only. Never use for UI elements.
| Token | Usage |
|-------|-------|
| `syntax.base.background` | Code block background |
| `syntax.base.foreground` | Default code text |
| `syntax.base.keyword` | Keywords |
| `syntax.base.string` | Strings |
| `syntax.highlights.diffAdded` | Added lines |
| `syntax.highlights.diffRemoved` | Removed lines |
## Examples
### Input Area
## Icon Contract
```tsx
const { currentTheme } = useThemeSystem();
import { Icon } from '@/components/icon/Icon';
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<textarea className="bg-transparent" />
<div className="bg-transparent">{/* Footer - transparent! */}</div>
</div>
<Icon name="check" className="size-4" />
```
### Active Tab
Use `IconName` for icon values stored in arrays, objects, state, or config. `Icon` has no `size` prop. Run `bun run icons:generate` when introducing a sprite name, and never edit `sprite.ts` manually. Load `references/icons.md` for the complete workflow.
```tsx
<button className={isActive
? 'bg-interactive-selection text-interactive-selection-foreground'
: 'hover:bg-interactive-hover/50'
}>
```
## Verification
### Error Message
```tsx
<div style={{
color: currentTheme.colors.status.error,
backgroundColor: currentTheme.colors.status.errorBackground
}}>
```
### Card
```tsx
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<h3 style={{ color: currentTheme.colors.surface.foreground }}>Title</h3>
<p style={{ color: currentTheme.colors.surface.mutedForeground }}>Description</p>
</div>
```
## Icon System (MANDATORY)
OpenChamber uses an SVG sprite-based icon system. **Never import from `@remixicon/react`.** Always use the shared `Icon` component.
### Import
```tsx
import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
```
### Usage
```tsx
<Icon name="arrow-down-s" className="h-4 w-4" />
<Icon name="loader-4" className="size-4 animate-spin" />
```
### Naming Convention
Convert Remixicon component names to kebab-case sprite names:
1. Strip `Ri` prefix
2. Strip `Line` suffix
3. Convert PascalCase to kebab-case
4. Lowercase everything
| Remixicon | Sprite name |
|-----------|-------------|
| `RiArrowDownSLine` | `arrow-down-s` |
| `RiCheckLine` | `check` |
| `RiLoader4Line` | `loader-4` |
| `RiGithubFill` | `github-fill` |
| `RiBrainAi3Line` | `brain-ai-3` |
### Fill Variants
For filled (solid) icon variants, append `-fill` explicitly. The generator tries `Line` suffix first, then `Fill`, then bare name.
```tsx
<Icon name="github-fill" /> {/* RiGithubFill */}
<Icon name="github" /> {/* RiGithubLine (default) */}
```
### Sizing
The `Icon` component has **no `size` prop**. Use Tailwind classes:
```tsx
<Icon name="check" className="h-4 w-4" /> {/* 16px - most common */}
<Icon name="check" className="size-5" /> {/* 20px */}
<Icon name="check" className="h-3 w-3" /> {/* 12px */}
```
### Adding a New Icon (Workflow)
**In order:**
1. Use the icon in code with the correct kebab-case name:
```tsx
<Icon name="new-icon-name" className="h-4 w-4" />
```
2. If used as a value (not JSX), use `IconName` type:
```tsx
const config = { icon: "new-icon-name" as const };
```
3. Regenerate the sprite:
```bash
bun run icons:generate
```
4. The script scans all source files, reverse-maps to Remixicon names, extracts SVG paths, and regenerates `sprite.ts`.
5. Verify: `bun run type-check`
**Do NOT manually edit `sprite.ts`.** Always regenerate.
### Type Safety for Icon Values
When icons are stored in objects/arrays, change the type from `ComponentType` to `IconName` and render via `<Icon name={value} />`:
```tsx
// ❌ Old: component reference
const items = [{ icon: RiStackLine }];
return <items[0].icon className="h-4 w-4" />;
// ✅ New: IconName string
import type { IconName } from "@/components/icon/icons";
const items: { icon: IconName }[] = [{ icon: "stack" }];
return <Icon name={items[0].icon} className="h-4 w-4" />;
```
## Wrong vs Right
### Wrong
```tsx
// ❌ Importing from @remixicon/react
import { RiArrowDownSLine } from "@remixicon/react";
<RiArrowDownSLine className="h-4 w-4" />
// ❌ Hardcoded colors
<div style={{ backgroundColor: '#F2F0E5' }}>
<button className="bg-blue-500">
// Primary for active tab
<Tab className="bg-primary">Active</Tab>
// Hover on static element
<div className="hover:bg-interactive-hover">Static card</div>
// Colored footer on input
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<textarea />
<div style={{ backgroundColor: currentTheme.colors.surface.muted }}>Footer</div>
</div>
```
### Right
```tsx
// ✅ Using the Icon component
import { Icon } from "@/components/icon/Icon";
<Icon name="arrow-down-s" className="h-4 w-4" />
// Theme tokens
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<button style={{ backgroundColor: currentTheme.colors.primary.base }}>
// Selection for active tab
<Tab style={{ backgroundColor: currentTheme.colors.interactive.selection }}>Active</Tab>
// Hover only on clickable
<button className="hover:bg-[var(--interactive-hover)]">Click</button>
// Transparent footer
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
<textarea className="bg-transparent" />
<div className="bg-transparent">Footer</div>
</div>
```
## References
- **[Adding Themes](references/adding-themes.md)** - Built-in and custom themes
## Key Files
- Theme types: `packages/ui/src/types/theme.ts`
- Theme hook: `packages/ui/src/contexts/useThemeSystem.ts`
- CSS generator: `packages/ui/src/lib/theme/cssGenerator.ts`
- Built-in themes: `packages/ui/src/lib/theme/themes/`
- Icon component: `packages/ui/src/components/icon/Icon.tsx`
- Icon sprite data: `packages/ui/src/components/icon/sprite.ts` (auto-generated)
- Icon types: `packages/ui/src/components/icon/icons.ts`
- Icon sprite generator: `scripts/generate-icon-sprite.mjs`
- Icon docs: `packages/ui/src/components/icon/README.md`
- No hardcoded/palette colors were introduced.
- Buttons use shared variants and sizes.
- Icons use `Icon`/`IconName`, and generated sprite changes are intentional.
- Hover, selection, primary, and status semantics are distinct.
- Light/dark/high-contrast and long-text states remain legible.
- Relevant type-check, visual/runtime validation, and generated-asset checks ran.
@@ -0,0 +1,59 @@
# Icon System
## Contract
Use `Icon` from `@/components/icon/Icon` and `IconName` from `@/components/icon/icons`. Do not import icon components directly from `@remixicon/react`.
```tsx
import { Icon } from '@/components/icon/Icon';
import type { IconName } from '@/components/icon/icons';
<Icon name="arrow-down-s" className="size-4" />
```
`Icon` has no `size` prop. Size it with classes.
## Naming
Convert Remixicon names to sprite names:
1. Remove `Ri`.
2. Remove the `Line` suffix.
3. Convert PascalCase to lowercase kebab-case.
4. Preserve filled variants with explicit `-fill`.
| Remixicon | Sprite name |
|---|---|
| `RiArrowDownSLine` | `arrow-down-s` |
| `RiCheckLine` | `check` |
| `RiLoader4Line` | `loader-4` |
| `RiGithubFill` | `github-fill` |
## Config Values
Store icon names, not component references:
```tsx
const items: Array<{ icon: IconName }> = [{ icon: 'stack' }];
return <Icon name={items[0].icon} className="size-4" />;
```
Use literal inference (`as const`) only when the surrounding type does not already provide `IconName`.
## Adding An Icon
1. Use the correct kebab-case name in source.
2. Type non-JSX values as `IconName`.
3. Run `bun run icons:generate`.
4. Inspect generated changes and run relevant type-check/build validation.
Never edit `packages/ui/src/components/icon/sprite.ts` manually. The generator scans source usages, maps names to Remixicon, and regenerates the sprite.
## Key Files
- Component: `packages/ui/src/components/icon/Icon.tsx`
- Types: `packages/ui/src/components/icon/icons.ts`
- Generated sprite: `packages/ui/src/components/icon/sprite.ts`
- Generator: `scripts/generate-icon-sprite.mjs`
- Documentation: `packages/ui/src/components/icon/README.md`
@@ -0,0 +1,112 @@
# Theme Tokens And Examples
## Token Families
### Surface
| Token | Usage |
|---|---|
| `surface.background` | Main app background |
| `surface.elevated` | Inputs, cards, panels, popovers |
| `surface.muted` | Secondary backgrounds and sidebars |
| `surface.foreground` | Primary text |
| `surface.mutedForeground` | Secondary text and hints |
| `surface.subtle` | Subtle dividers |
### Interactive
| Token | Usage |
|---|---|
| `interactive.border` | Default borders |
| `interactive.hover` | Hover on clickable elements only |
| `interactive.active` | Pressed interaction state |
| `interactive.selection` | Active/selected items |
| `interactive.selectionForeground` | Text on selection |
| `interactive.focusRing` | Focus indicators |
### Status
Use status colors only for actual feedback.
- `status.error`: errors and validation failures
- `status.warning`: cautions
- `status.success`: successful outcomes
- `status.info`: informational feedback
Each family may expose foreground, background, and border variants.
### Primary
- `primary.base`: primary CTA
- `primary.hover`: primary hover
- `primary.foreground`: content on primary
Primary means “act”; selection means “currently active.” Do not use primary to mark ordinary selected tabs or rows.
### Syntax
Use `syntax.*` only for code display: code backgrounds/text, keywords, strings, and diff highlights. Never use syntax colors for ordinary UI chrome.
## Usage
Prefer semantic utility classes when available:
```tsx
<div className="bg-[var(--surface-elevated)] text-foreground" />
<button className="hover:bg-interactive-hover" />
```
Use `useThemeSystem()` when a library/API requires actual color values:
```tsx
const { currentTheme } = useThemeSystem();
<Chart color={currentTheme.colors.status.error} />
```
## Common Patterns
### Input Area
```tsx
<div className="bg-[var(--surface-elevated)]">
<textarea className="bg-transparent" />
<div className="bg-transparent">...</div>
</div>
```
Input footers stay transparent over the elevated input surface.
### Active Item
```tsx
<button className={isActive
? 'bg-interactive-selection text-interactive-selection-foreground'
: 'hover:bg-interactive-hover'
} />
```
### Error Feedback
```tsx
<div className="bg-[var(--status-error-background)] text-[var(--status-error-foreground)]" />
```
### Neutral Card
```tsx
<section className="bg-[var(--surface-elevated)] text-foreground">
<p className="text-muted-foreground">...</p>
</section>
```
## Wrong Patterns
```tsx
<div style={{ backgroundColor: '#F2F0E5' }} />
<button className="bg-blue-500" />
<div className="hover:bg-interactive-hover">Static content</div>
<Tab className="bg-primary">Active</Tab>
```
Use theme tokens, apply hover only to interactive elements, and distinguish selection from primary actions.
+61 -274
View File
@@ -1,306 +1,93 @@
---
name: ui-api-decoupling
description: Use when creating or modifying OpenChamber UI data access, RuntimeAPIs, runtimeFetch/runtime-url auth, authenticated browser assets, OpenCode SDK calls, VS Code bridges, Electron runtime switching, or web server API endpoints.
license: MIT
compatibility: opencode
description: Use when creating or modifying OpenChamber shared UI data access, OpenCode SDK calls, `RuntimeAPIs`, runtime fetch/auth/URLs, authenticated browser assets, bridges/proxies, runtime switching, or server API routes.
---
## Overview
# UI API Decoupling
OpenChamber shared UI runs against web, Electron desktop, remote server URLs, and VS Code webviews. API code must preserve that runtime boundary.
## Core Boundary
**Core principle:** official OpenCode API calls go through `@opencode-ai/sdk/v2` via `opencodeClient`; OpenChamber-owned capabilities go through `RuntimeAPIs` or explicit OpenChamber routes; runtime transport preserves SDK-generated requests exactly.
- Official OpenCode API calls use `@opencode-ai/sdk/v2` through `opencodeClient`.
- OpenChamber-owned HTTP capabilities use `RuntimeAPIs` where runtime-specific behavior exists, otherwise explicit OpenChamber routes through `runtimeFetch`.
- Browser/realtime consumers use shared runtime URL/socket helpers.
- Shared UI never hardcodes localhost, ports, API origins, credentials, or one runtime's transport assumptions.
## Scope
Use this skill for changes touching UI data loading, session/message operations, provider/auth/config calls, filesystem/git/terminal/settings APIs, runtime switching, desktop/VS Code bridges, or server routes under `/api/*`.
Do not use this skill for pure visual-only UI work unless the change adds, removes, or reshapes data access.
## First Step
Before editing, classify every endpoint or capability involved:
## Classify First
| Need | Correct path |
|------|--------------|
| Official OpenCode endpoint | `opencodeClient` or `opencodeClient.getSdkClient()` |
| SDK gap to official OpenCode | Central helper in `opencodeClient` using `runtimeFetch`, documented as SDK gap |
| OpenChamber-owned feature route | `RuntimeAPIs` first, otherwise `runtimeFetch` to explicit OC route |
| Native/runtime capability | Extend `RuntimeAPIs`, implement per runtime, consume via hook/registry |
| Browser/realtime URL that cannot send headers (iframe, download/open link, SSE, WebSocket, preview subresource) | `getRuntimeUrlResolver()` helpers plus `oc_url_token` allowlist, not hardcoded URLs |
| UI-controlled authenticated asset fetch (small icons/thumbnails where JS can fetch) | `runtimeFetch` with `Authorization`, then `URL.createObjectURL(blob)` |
|---|---|
| Official OpenCode endpoint | `opencodeClient` or its SDK client |
| SDK gap for official OpenCode | Narrow documented wrapper in `opencodeClient` preserving request fidelity |
| OpenChamber HTTP route | `runtimeFetch('/api/...')` |
| Runtime-owned capability | Extend `RuntimeAPIs` and implement each applicable runtime |
| Browser-owned authenticated URL | Runtime URL resolver and scoped URL auth |
| SSE/WebSocket | Owning realtime transport; also load `relay-transport` |
## Load References By Task
| Task | Required reference |
|---|---|
| Iframes, downloads, raw images, object URLs, URL tokens, preview proxy/subresources | `references/browser-assets-and-auth.md` |
| Adding runtime capabilities, VS Code behavior, Electron privilege/security, unsupported runtime behavior | `references/runtime-parity.md` |
| Locating implementations, route registration, runtime switching, or focused tests | `references/implementation-map.md` |
Load every matching reference before editing.
## Mandatory Rules
1. **Never bypass the SDK for official OpenCode APIs**
- Do not add raw `fetch` or direct `runtimeFetch` from feature UI to official endpoints such as `/api/session`, `/api/permission`, `/api/question`, `/api/auth`, `/api/provider`, `/api/command`, `/api/app`.
- Use `opencodeClient` wrappers or `opencodeClient.getSdkClient()`.
- If the SDK lacks a method, add a narrow wrapper in `packages/ui/src/lib/opencode/client.ts`, mark it as an SDK gap, and add transport coverage when body/method/query/signal matters.
1. **Do not bypass the SDK for official OpenCode APIs.** Preserve SDK-generated method, body, headers, query, auth, and abort signal.
2. **Keep OpenChamber routes explicit.** Register them before the generic OpenCode proxy.
3. **Use runtime APIs for runtime-owned capabilities.** Components consume hooks/providers, not runtime globals.
4. **Resolve runtime state at call time.** Do not cache runtime base URLs, resolver output, credentials, or SDK clients across endpoint switches.
5. **Let transport own auth.** HTTP uses runtime bearer handling; browser/realtime URLs use scoped short-lived URL auth where headers are impossible.
6. **Never put long-lived client credentials in URLs.** Do not manually append URL tokens.
7. **Define runtime parity explicitly.** Shared UI needs deliberate web, Electron, VS Code, hosted-mobile, and Capacitor behavior or stable unsupported responses.
8. **Authoritative fetches must signal failure.** Do not convert failure into a valid empty value that callers use to clear state.
9. **Keep privileges at the native/runtime boundary.** UI visibility and prompts are not authorization.
10. **Confirm trust-boundary mutations.** Host imports, credential writes, privileged deep links, and runtime switching require explicit user intent.
2. **Preserve SDK request fidelity**
- Runtime transport must preserve `Request` method, body, headers, query string, auth, and abort signal.
- Do not rebuild a request from only `url` and `init`.
- Regression tests belong near `packages/ui/src/lib/runtime-fetch.test.ts`, `packages/vscode/webview/api/bridge.test.ts`, and proxy tests when transport changes.
## HTTP Decision Rules
3. **Use `RuntimeAPIs` for runtime-owned capabilities**
- Files, git, terminal, settings, notifications, GitHub helpers, client auth, editor/VS Code actions, and tools belong in `RuntimeAPIs` when shared UI needs runtime-specific behavior.
- React components use `useRuntimeAPIs()` or `useRuntimeAPI()`.
- Non-React modules use `getRegisteredRuntimeAPIs()` only when a hook cannot be used.
- Direct `window.__OPENCHAMBER_RUNTIME_APIS__` reads are entrypoint/legacy escape hatches, not a new feature pattern.
4. **Keep OpenChamber routes explicit**
- Direct `runtimeFetch` is acceptable for OpenChamber-only routes such as `/api/config/settings`, `/api/config/skills`, `/api/config/commands`, `/api/fs`, `/api/git`, `/api/terminal`, `/api/preview`, `/api/magic-prompts`, `/api/tts`, and `/api/openchamber/tunnel`.
- Register OpenChamber routes before the generic OpenCode proxy, or the proxy will steal the path.
- Shared UI depending on an OC route requires web and VS Code parity, or an explicit deterministic unsupported response.
5. **Do not hardcode local runtime URLs**
- Do not infer `localhost`, server ports, or `/api` origins in shared UI.
- Use `getRuntimeUrlResolver()` at call time.
- Do not use the exported `runtimeUrl` singleton for new code because it can capture stale resolver state.
6. **Treat runtime auth as transport state**
- HTTP auth is owned by `runtime-auth` and `runtimeFetch`; callers pass route paths and let transport attach `Authorization` only for the active runtime service URL.
- Browser/realtime transports that cannot set headers use `runtime-url` helpers and short-lived `oc_url_token` query auth.
- Never put long-lived client bearer tokens in URLs. `oc_client_token` should appear only in legacy stripping/rejection paths, tests, or migration compatibility code.
- Do not manually append `oc_url_token`; use resolver helpers and add server-side allowlist coverage when a new browser-consumed route needs URL auth.
7. **Runtime switch must reset stale state**
- Runtime base URL, runtime key, bearer token, SDK clients, terminal transports, session memory, and UI runtime-scoped state must not be cached blindly.
- Use `switchRuntimeEndpoint`, `subscribeRuntimeEndpointChanged`, `opencodeClient.reconnectToRuntimeBaseUrl()`, and runtime-keyed store state.
8. **Authoritative fetches must signal failure**
- If a caller uses returned data to replace, delete, or clear authoritative state, the method must throw or return `null` on failure.
- Do not swallow errors and return `[]`, `{}`, or `null` when that value is also a valid empty success unless the caller treats it as display-only.
9. **Privileged runtime switching requires explicit user intent**
- Electron connect/deep-link flows that import a remote host, store a client token, change default host, or switch active runtime must show an in-app confirmation before writing config or switching.
- The confirmation may show the label and server URL, but never the token.
- Existing-host imports still require confirmation because they can overwrite the stored token or change the active runtime.
## HTTP Request Decision Rules
For normal HTTP requests to the active OpenChamber runtime, use `runtimeFetch` with the route path. Let `runtimeFetch` resolve the current runtime base URL and auth at call time.
Pass route paths directly to `runtimeFetch`:
```ts
// Good: runtimeFetch owns base URL, runtime auth, and runtime switching.
await runtimeFetch('/health');
await runtimeFetch('/auth/session', { method: 'GET' });
await runtimeFetch('/api/config/settings');
await runtimeFetch('/api/fs/raw', { query: { path: absolutePath } });
// Bad: callers should not prebuild runtime HTTP URLs for fetches.
await fetch(getRuntimeUrlResolver().health());
await runtimeFetch(getRuntimeUrlResolver().api('/api/config/settings'));
await runtimeFetch(getRuntimeUrlResolver().rawFile(absolutePath));
await runtimeFetch('/api/fs/raw', { query: { path } });
```
Use `runtimeFetch(..., { query })` instead of manually appending query strings when the request targets `/api`, `/auth`, or `/health`.
Do not immediately fetch a URL produced by `getRuntimeUrlResolver()`. Use the resolver only when the browser/realtime API itself consumes the URL:
```ts
// Good
await runtimeFetch('/api/git/status', { query: { directory, mode: 'light' } });
// Avoid
await runtimeFetch(`/api/git/status?directory=${encodeURIComponent(directory)}&mode=light`);
```
Use `getRuntimeUrlResolver()` only when the resulting URL is consumed by the browser or a realtime transport, not immediately fetched as HTTP:
```ts
// Good resolver usage: URL is assigned to browser/realtime consumers.
const rawImageSrc = getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', { path });
const iframeSrc = getRuntimeUrlResolver().authenticatedAsset(proxyPath);
const iframeSrc = getRuntimeUrlResolver().authenticatedAsset('/api/preview/frame');
const eventUrl = getRuntimeUrlResolver().sse('/api/event');
const socketUrl = getRuntimeUrlResolver().websocket('/api/terminal/ws');
```
Plain `fetch` is acceptable only for intentional external network requests that do not target the OpenChamber runtime, such as npm registry, models.dev, or a user-provided `https://...` URL.
Plain `fetch` is reserved for intentional external origins that are not the active OpenChamber/OpenCode runtime.
## Authenticated Browser Assets
## Runtime Switch Safety
Authenticated assets need an explicit transport choice. Pick based on who owns the request:
| Asset/request shape | Correct pattern |
|---------------------|-----------------|
| React/UI code can fetch it and the object is small (project icons, small thumbnails, generated previews) | `runtimeFetch('/api/...')` with `Authorization`, read `blob()`, render a `URL.createObjectURL(blob)` |
| Browser must own the URL (iframe `src`, image/download/open-link for large raw files, rewritten preview subresources) | `getRuntimeUrlResolver().authenticatedAsset(...)` so the URL carries short-lived `oc_url_token` |
| Realtime transports | `getRuntimeUrlResolver().sse(...)` or `.websocket(...)`; never generic fetch/proxy paths |
For object-URL assets:
- Key caches by runtime identity (`getRuntimeApiBaseUrl()` or runtime key), entity ID, version/update timestamp, and render-affecting options.
- Cap caches and revoke evicted object URLs with `URL.revokeObjectURL`.
- Render a deterministic fallback while loading or after failure; do not leave empty chrome.
- Keep the fetch display-only unless the caller intentionally treats failure as authoritative.
For URL-auth assets:
- The server route must explicitly allow `oc_url_token` in `packages/web/server/lib/ui-auth/ui-auth.js` and have coverage in `ui-auth.test.js`.
- Scope allowlists narrowly to browser-readable GET routes or specific realtime upgrade paths. Do not allow arbitrary `/api/*`.
- Use short-lived `oc_url_token` only. Do not revive `oc_client_token` in query strings.
Preview iframe/subresource rules:
- Use preview proxy helpers so `oc_preview_token` and `oc_url_token` propagate to rewritten resources and redirects.
- Strip legacy `oc_client_token` before forwarding to dev servers.
- Do not use `postMessage('*')`; target the known preview origin.
- Preserve CSP where possible. If injecting a bridge, prefer a per-response nonce and remove only directives that block framing or the bridge.
## Runtime API Extension Pattern
When adding a native/per-runtime capability:
1. Add or extend the interface in `packages/ui/src/lib/api/types.ts`.
2. Implement web HTTP behavior in `packages/web/src/api/*` and compose it in `packages/web/src/api/index.ts`.
3. Implement VS Code webview API in `packages/vscode/webview/api/*` and compose it in `packages/vscode/webview/api/index.ts`.
4. Add extension-host handlers in `packages/vscode/src/bridge-*-runtime.ts` when filesystem, git, settings, or OpenCode manager access is required.
5. Keep Electron shared through the web runtime unless it needs shell-only IPC in `packages/electron/main.mjs` or `packages/electron/preload.mjs`.
6. Register the runtime APIs through app entrypoints and consume through `RuntimeAPIProvider`.
## VS Code Route Parity
For any shared UI call to `/api/*`, decide the VS Code behavior explicitly:
| Route type | VS Code handling |
|------------|------------------|
| OpenChamber local route | Handle in `packages/vscode/webview/main.tsx` and bridge to extension host when needed |
| Official OpenCode route | Let generic fetch proxy forward to OpenCode via `api:proxy` |
| SSE route | Use `api:sse:start` / stream messages / `api:sse:stop`, never generic proxy |
| Session message POST | Use `api:session:message` special proxy path |
| Unsupported native feature | Return stable 501/unsupported JSON, not silent fallback |
## Electron Security Boundary
Electron exposes API base and shell identity broadly, but privileged local capabilities stay local-only.
- `__OPENCHAMBER_API_BASE_URL__` and `__OPENCHAMBER_LOCAL_ORIGIN__` route requests.
- `__OPENCHAMBER_CLIENT_TOKEN__`, `__OPENCHAMBER_HOME__`, and privileged desktop IPC are local-page gated.
- Do not expose filesystem, shell, or host secrets to remote pages for UI convenience.
- Do not trust arbitrary loopback, `file://`, or `about:blank` origins as local UI. Gate privileged preload/IPC/token access to the packaged UI origin and exact runtime origins.
- Deep-links that add or switch remote runtimes are trust-boundary changes. Confirm before storing tokens or switching hosts.
Review runtime base URL, auth, SDK clients, terminal/realtime transports, stores, session memory, and caches. Key caches by runtime identity where IDs, paths, or URLs can collide. Reset or reconnect affected state through the established runtime-switch flow.
## Common Anti-Patterns
| Anti-pattern | Use instead |
|--------------|-------------|
| `fetch('/api/session/...')` in shared UI | SDK through `opencodeClient` |
| `runtimeFetch('/api/session/...')` from a component | SDK wrapper or documented SDK-gap helper |
| `fetch(getRuntimeUrlResolver().health())` | `runtimeFetch('/health')` |
| `runtimeFetch(getRuntimeUrlResolver().api('/api/foo'))` | `runtimeFetch('/api/foo')` |
| `runtimeFetch(getRuntimeUrlResolver().rawFile(path))` | `runtimeFetch('/api/fs/raw', { query: { path } })` |
| New `/api/foo` only in web server | Web + VS Code route decision |
| Component reads `window.__OPENCHAMBER_RUNTIME_APIS__` | `useRuntimeAPIs()` / `useRuntimeAPI()` |
| Rebuilding `new Request(newUrl)` only | `new Request(newUrl, oldRequest)` plus merged headers |
| Returning `[]` on authoritative SDK failure | Throw or return `null` and preserve state |
| Caching `getRuntimeUrlResolver()` output forever | Read resolver/client at call time or reset on runtime switch |
| Manually appending `oc_client_token` or `oc_url_token` | `runtimeFetch` for HTTP, resolver helpers for browser/realtime URLs |
| Direct `<img src>` to a small authenticated app asset | `runtimeFetch` + `blob()` + object URL with fallback and bounded cache |
| Adding URL-auth access to a route without server allowlist tests | Narrow `oc_url_token` allowlist in `ui-auth.js` plus `ui-auth.test.js` coverage |
| Connect deep-link writes host config before consent | Confirm first, then import/switch |
| Avoid | Use |
|---|---|
| Raw feature `fetch` to official OpenCode | SDK wrapper/client |
| Component reads runtime globals | `useRuntimeAPIs()` / provider |
| Hardcoded runtime URL | `runtimeFetch` or runtime URL resolver |
| Browser URL containing bearer/client token | Scoped URL-auth helper |
| Web-only shared route | Explicit VS Code/mobile decision |
| Returning `[]` after authoritative fetch failure | Throw or distinct failure result |
| Rebuilding SDK `Request` from URL only | Preserve original request body/headers/signal |
## Verification Checklist
## Verification
Before finalizing a UI/API decoupling change:
1. Official OpenCode routes use SDK wrappers or documented SDK-gap helpers.
2. OpenChamber routes are registered before the generic proxy.
3. VS Code has parity, proxy fallback, or explicit unsupported behavior.
4. Runtime transport preserves body, method, headers, query, auth, and abort signal.
5. Runtime auth/token handling uses `runtime-auth` and `runtime-url`.
6. No long-lived client bearer token is placed in a URL; browser/realtime URL auth uses scoped short-lived `oc_url_token` only.
7. Browser-consumed routes that need `oc_url_token` have narrow server allowlist and tests.
8. Runtime switch clears or scopes affected client/store/object-URL state.
9. Authoritative loaders distinguish failure from empty success.
10. Targeted tests cover changed transport, bridge, proxy, auth allowlist, or runtime API behavior.
## Implementation Map
### Shared UI Sources Of Truth
`packages/ui/src/lib/opencode/client.ts` is the central OpenCode SDK wrapper. It creates `@opencode-ai/sdk/v2` clients with `fetch: runtimeFetch`, runtime auth headers, current-directory handling, scoped clients, and convenience wrappers. Add official OpenCode API behavior here unless a feature directly consumes `getSdkClient()` in sync/runtime code.
`packages/ui/src/lib/runtime-fetch.ts` rewrites `/api`, `/auth`, and `/health` through the active runtime URL resolver and injects runtime auth. Its key contract is preserving SDK-created `Request` objects, including method, body, headers, query, and signal. For ordinary HTTP calls, pass route paths directly to `runtimeFetch`; do not pre-resolve them with `getRuntimeUrlResolver()` first.
`packages/ui/src/lib/runtime-url.ts` owns HTTP, auth, health, raw-file, SSE, WebSocket, and authenticated browser URL construction. `getRuntimeUrlResolver()` is the call-time source for browser-consumed URLs like iframe `src`, large/raw image `src`, download/open links, SSE URLs, and WebSocket URLs. `runtimeUrl` is not safe for new code that must survive runtime switches.
`packages/ui/src/lib/runtime-auth.ts` owns bearer-token state and short-lived URL-token minting. `runtimeFetch` merges `Authorization` unless a caller already supplied one. Runtime URL helpers add scoped `oc_url_token` where headers are impossible; they must never expose long-lived client bearer tokens in URLs.
### Runtime API Contract
`packages/ui/src/lib/api/types.ts` defines `RuntimeAPIs` and all per-runtime capability contracts.
`packages/ui/src/contexts/RuntimeAPIProvider.tsx` provides APIs to React and wraps `files` with a content cache that invalidates on write, delete, and rename.
`packages/ui/src/hooks/useRuntimeAPIs.ts` is the React consumption path. `packages/ui/src/contexts/runtimeAPIRegistry.ts` is the non-React escape hatch for modules that cannot use hooks.
`packages/ui/src/App.tsx` and app variants register APIs and reset runtime-scoped stores on `openchamber:runtime-endpoint-changed`.
### Web Runtime
`packages/web/src/runtimeConfig.ts` reads injected globals, configures the runtime URL resolver, sets the runtime bearer token, installs the runtime fetch bridge, and creates web APIs.
`packages/web/src/main.tsx`, `mobile-main.tsx`, and `mini-chat-main.tsx` assign `window.__OPENCHAMBER_RUNTIME_APIS__` before rendering shared UI.
`packages/web/src/api/index.ts` composes web `RuntimeAPIs` from implementations such as `files.ts`, `git.ts`, `terminal.ts`, `settings.ts`, `permissions.ts`, `github.ts`, `clientAuth.ts`, `push.ts`, and `tools.ts`.
Web runtime API implementations are normally HTTP clients for OpenChamber-owned server routes. Use `runtimeFetch` for HTTP requests; use `getRuntimeUrlResolver()` only when producing browser/realtime URLs that will not be immediately fetched by code.
### Server Routes And Proxy
`packages/web/server/index.js` starts the OpenChamber web server. Electron imports this server in-process.
`packages/web/server/lib/opencode/core-routes.js` installs JSON parsing for OpenChamber-owned `/api/*` route families.
`packages/web/server/lib/opencode/feature-routes-runtime.js` registers OpenChamber feature routes before the generic OpenCode proxy: filesystem, git, GitHub, quota, config entities, skills/plugins, magic prompts, session folders, scheduled tasks, and related features.
`packages/web/server/lib/opencode/proxy.js` is the generic `/api/*` proxy to upstream OpenCode. It strips the `/api` prefix, injects OpenCode auth headers, replays parsed bodies for non-GET requests, handles `/api/event` and `/api/global/event` as SSE, applies readiness gating, and canonicalizes directory query parameters.
OpenChamber-owned routes must be explicit and registered before the proxy. If a route is shared UI contract, add VS Code parity or a deterministic unsupported response.
If an OpenChamber route is consumed directly by the browser with `oc_url_token`, update the readable/realtime allowlist in `packages/web/server/lib/ui-auth/ui-auth.js` and add tests in `ui-auth.test.js`. Do not use URL tokens as a blanket `/api/*` auth bypass.
### VS Code Runtime
`packages/vscode/webview/api/index.ts` composes VS Code `RuntimeAPIs`. Terminal is a stub; files, git, settings, permissions, notifications, GitHub, tools, editor, and VS Code actions use the bridge.
`packages/vscode/webview/main.tsx` installs `window.__OPENCHAMBER_RUNTIME_APIS__` and overrides `window.fetch`. It handles OpenChamber local routes, then proxies generic OpenCode `/api/*` calls to the extension host. It has special branches for SSE and session message POST.
`packages/vscode/webview/requestBodyTransport.ts` extracts request bodies from SDK-style `Request` objects and `init.body` without losing bytes.
`packages/vscode/webview/api/bridge.ts` sends bridge messages, supports abort propagation, exposes `proxyApiRequest`, `proxySessionMessageRequest`, and SSE start/stop helpers.
`packages/vscode/src/bridge-proxy-runtime.ts` forwards generic OpenCode proxy requests to the live OpenCode API URL, merges sanitized headers with OpenCode auth, forwards body bytes, and rejects SSE through the generic proxy.
`packages/vscode/src/bridge-config-runtime.ts`, `bridge-fs-runtime.ts`, `bridge-git-runtime.ts`, and related bridge modules implement OpenChamber-owned route behavior in the extension host.
### Electron Runtime
`packages/electron/main.mjs` starts the web server in-process, resolves local/remote runtime target, tracks `apiBaseUrl` and `clientToken`, injects init scripts, confirms remote connect deep-links before storing tokens, and handles host switching.
`packages/electron/preload.mjs` exposes runtime globals. API base and local origin are broadly available for routing. Client token, home directory, and privileged desktop IPC stay local-page gated so remote pages cannot access local host capabilities.
Shared UI should not branch on Electron for backend behavior. Prefer web runtime APIs and the `__OPENCHAMBER_DESKTOP__` bridge only for shell capabilities that already exist in the shared runtime contract.
### Runtime Switch Flow
`packages/ui/src/lib/runtime-switch.ts` updates `__OPENCHAMBER_API_BASE_URL__`, `__OPENCHAMBER_CLIENT_TOKEN__`, runtime URL resolver, bearer token, and dispatches `openchamber:runtime-endpoint-changed`.
`packages/ui/src/App.tsx` reacts by preparing/restoring runtime-keyed session and UI state, reconnecting `opencodeClient`, clearing provider/agent connection state, disposing terminal transports, resetting streaming state, and triggering re-bootstrap.
Any cache keyed only by session ID, directory, or URL should be reviewed when runtime switching is involved. Use runtime keys when local and remote instances can share IDs or paths.
### Tests To Prefer
Use targeted transport/auth tests when changing request forwarding or URL auth: `packages/ui/src/lib/runtime-fetch.test.ts`, `packages/ui/src/lib/runtime-url.test.ts`, `packages/ui/src/lib/runtime-auth.test.ts`, `packages/web/server/lib/ui-auth/ui-auth.test.js`, `packages/vscode/webview/api/bridge.test.ts`, `packages/vscode/src/bridge-proxy-runtime.test.js`, `packages/web/server/opencode-proxy.test.js`, and `packages/web/server/lib/preview/proxy-runtime.test.js`.
Use runtime API tests near the implementation when adding or changing per-runtime behavior, for example web API tests under `packages/web/src/api/*.test.ts`, VS Code bridge tests under `packages/vscode/src/*test.js`, and UI wrapper tests under `packages/ui/src/lib/*test.ts`.
Run `bun run type-check` and `bun run lint` before finalizing code changes unless the user explicitly narrows validation.
## References
- SDK wrapper: `packages/ui/src/lib/opencode/client.ts`
- Runtime fetch/auth/url: `packages/ui/src/lib/runtime-fetch.ts`, `runtime-auth.ts`, `runtime-url.ts`
- Runtime API contract: `packages/ui/src/lib/api/types.ts`
- Web API composition: `packages/web/src/api/index.ts`, `packages/web/src/runtimeConfig.ts`
- VS Code bridge/proxy: `packages/vscode/webview/main.tsx`, `packages/vscode/webview/api/bridge.ts`, `packages/vscode/src/bridge-proxy-runtime.ts`
- Server proxy: `packages/web/server/lib/opencode/proxy.js`, `packages/web/server/lib/opencode/core-routes.js`
- UI auth and URL-token allowlists: `packages/web/server/lib/ui-auth/ui-auth.js`
- Preview proxy and rewritten browser subresources: `packages/web/server/lib/preview/proxy-runtime.js`
- Official calls use SDK paths or documented SDK-gap wrappers.
- OpenChamber routes win before generic proxy fallback.
- Request fidelity, auth, abort, query, and body behavior are tested.
- Browser/realtime auth uses narrow allowlists and scoped tokens.
- Every applicable runtime has implementation or explicit unsupported behavior.
- Runtime switching cannot reuse stale endpoint/auth/cache state.
- Privileged Electron/extension behavior is enforced outside the renderer.
- Focused transport, bridge, proxy, auth, and runtime tests pass; static type/lint checks alone are insufficient.
@@ -0,0 +1,46 @@
# Browser Assets And URL Authentication
## Choose By Request Owner
| Request shape | Correct path |
|---|---|
| UI can fetch a small authenticated asset | `runtimeFetch`, read `blob()`, render an object URL |
| Browser must own a URL (`iframe`, download/open link, large/raw image, rewritten subresource) | `getRuntimeUrlResolver().authenticatedAsset(...)` |
| SSE | `getRuntimeUrlResolver().sse(...)` and owning transport |
| WebSocket | `getRuntimeUrlResolver().websocket(...)` plus `openRuntimeWebSocket` where required |
Do not prebuild a browser URL and then immediately call `runtimeFetch` with it. Ordinary HTTP callers pass route paths to `runtimeFetch`; browser/realtime consumers use resolver URLs.
## Object URLs
- Key caches by runtime identity, entity ID, update/version, and render options.
- Bound caches by count and bytes when values can be large.
- Revoke evicted object URLs with `URL.revokeObjectURL`.
- Render a deterministic fallback while loading or after display-only failure.
## URL Tokens
Browser-owned URLs cannot attach the normal `Authorization` header. Use short-lived scoped `oc_url_token` minted through runtime auth helpers.
- Never manually append `oc_url_token`.
- Never place a long-lived client bearer token in a URL.
- Treat `oc_client_token` query use as legacy stripping/rejection only.
- Add browser-readable GET or realtime paths to the narrow allowlist in `packages/web/server/lib/ui-auth/ui-auth.js`.
- Add allowlist tests; never allow arbitrary `/api/*` URL-token access.
## Preview Iframes And Rewritten Resources
- Use preview proxy helpers so preview and URL tokens propagate to rewritten resources and redirects.
- Strip legacy client-token query parameters before forwarding upstream.
- Do not use `postMessage('*')`; target the known preview origin.
- Preserve CSP where possible. If injecting a bridge, prefer a per-response nonce and remove only directives that block framing or the bridge.
- Re-resolve browser URLs after runtime switches; do not retain URLs minted for an old runtime.
## Security Tests
Prefer focused coverage in:
- `packages/ui/src/lib/runtime-url.test.ts`
- `packages/ui/src/lib/runtime-auth.test.ts`
- `packages/web/server/lib/ui-auth/ui-auth.test.js`
- `packages/web/server/lib/preview/proxy-runtime.test.js`
@@ -0,0 +1,49 @@
# Runtime Implementation Map
## Shared UI
- `packages/ui/src/lib/opencode/client.ts`: OpenCode v2 SDK wrapper, current-directory handling, runtime-aware SDK client.
- `packages/ui/src/lib/runtime-fetch.ts`: runtime HTTP URL resolution and auth while preserving SDK `Request` fidelity.
- `packages/ui/src/lib/runtime-url.ts`: browser/realtime URL construction.
- `packages/ui/src/lib/runtime-auth.ts`: bearer state and short-lived URL-token minting.
- `packages/ui/src/lib/api/types.ts`: shared `RuntimeAPIs` contract.
- `packages/ui/src/contexts/RuntimeAPIProvider.tsx`: React provider and runtime API wrappers.
- `packages/ui/src/hooks/useRuntimeAPIs.ts`: React consumption path.
## Web And Server
- `packages/web/src/runtimeConfig.ts`: initializes runtime URL/auth and web APIs.
- `packages/web/src/api/index.ts`: composes web `RuntimeAPIs`.
- `packages/web/server/lib/opencode/core-routes.js`: installs OpenChamber route families.
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: explicit feature route registration.
- `packages/web/server/lib/opencode/proxy.js`: generic OpenCode proxy fallback.
- `packages/web/server/lib/ui-auth/ui-auth.js`: session and URL-token route gates.
Explicit OpenChamber routes must register before the generic `/api/*` OpenCode proxy.
## VS Code
- `packages/vscode/webview/main.tsx`: webview fetch routing and local-route handling.
- `packages/vscode/webview/api/index.ts`: webview `RuntimeAPIs` composition.
- `packages/vscode/webview/api/bridge.ts`: request, session-message, and SSE bridge helpers.
- `packages/vscode/webview/requestBodyTransport.ts`: byte-preserving request-body extraction.
- `packages/vscode/src/bridge-proxy-runtime.ts`: extension-host OpenCode forwarding.
- `packages/vscode/src/bridge-*-runtime.ts`: owning native/local handlers.
## Runtime Switching
`packages/ui/src/lib/runtime-switch.ts` updates endpoint/auth state and emits the runtime-change event. App roots reconnect SDK clients and reset runtime-scoped stores/transports.
Review every cache keyed only by session ID, directory, URL, or entity ID. Add runtime identity when local and remote runtimes can collide.
## Tests To Prefer
- HTTP/request fidelity: `packages/ui/src/lib/runtime-fetch.test.ts`
- URL/auth: `packages/ui/src/lib/runtime-url.test.ts`, `runtime-auth.test.ts`
- Server auth: `packages/web/server/lib/ui-auth/ui-auth.test.js`
- Generic proxy: `packages/web/server/opencode-proxy.test.js`
- Preview proxy: `packages/web/server/lib/preview/proxy-runtime.test.js`
- VS Code bridge: `packages/vscode/webview/api/bridge.test.ts`
- VS Code proxy: `packages/vscode/src/bridge-proxy-runtime.test.js`
Also run focused tests beside new runtime implementations and validation required by each affected workspace.
@@ -0,0 +1,38 @@
# Runtime API And Parity
## Extending `RuntimeAPIs`
1. Add or extend the shared interface in `packages/ui/src/lib/api/types.ts`.
2. Implement web behavior under `packages/web/src/api/*` and compose it in `packages/web/src/api/index.ts`.
3. Implement VS Code webview behavior under `packages/vscode/webview/api/*`.
4. Add extension-host bridge handlers when filesystem, git, settings, or manager access is required.
5. Keep Electron shared through the web runtime unless behavior is inherently native.
6. Register APIs through app entrypoints and consume via `RuntimeAPIProvider` hooks.
React components use `useRuntimeAPIs()` or `useRuntimeAPI()`. Non-React modules use `getRegisteredRuntimeAPIs()` only when hooks are impossible. Do not introduce direct reads of `window.__OPENCHAMBER_RUNTIME_APIS__` in feature code.
## VS Code Route Decisions
| Route type | VS Code behavior |
|---|---|
| OpenChamber local route | Handle in the webview and bridge to extension host when needed |
| Official OpenCode route | Forward through the generic OpenCode proxy |
| SSE | Use the dedicated SSE bridge, never generic proxy |
| Session message POST | Use the dedicated session-message path |
| Unsupported native feature | Return stable explicit unsupported behavior, normally 501 JSON |
Register explicit OpenChamber handling before generic proxy fallback. Silent empty fallback is not parity.
## Electron Boundary
Electron normally reuses the web runtime/server implementation. Keep privileged shell behavior behind main/preload IPC and local-page gates.
- API base and shell identity may be broadly available for routing.
- Client tokens, home paths, filesystem/shell access, and privileged IPC remain local-page gated.
- Do not trust arbitrary loopback, `file://`, or `about:blank` origins as packaged UI.
- Remote pages and preview iframes must not gain local host privileges.
- Deep links that import hosts, store credentials, or switch runtimes require explicit in-app confirmation before mutation.
## Shared Contract Rule
For every shared capability, decide web, Electron, VS Code, hosted-mobile, and Capacitor behavior explicitly. A stable unsupported response is acceptable; accidental fallthrough is not.
+71 -449
View File
@@ -1,476 +1,98 @@
# OpenChamber - AI Agent Reference
# OpenChamber Agent Guide
## Core purpose
## Purpose
OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an OpenCode server (local auto-start or remote URL). Official OpenCode traffic goes through `@opencode-ai/sdk`; OpenChamber-owned runtime capabilities go through `RuntimeAPIs`, `runtimeFetch`, and browser/realtime URL helpers.
OpenChamber provides shared web, desktop, VS Code, hosted-mobile, and native-mobile UI surfaces for OpenCode.
## Runtime architecture (IMPORTANT)
This file contains only always-on repository rules and routing. Detailed workflows belong to project skills and module documentation.
- `Desktop` (Electron) boots the web server **in the same Node process** as the Electron main, then loads the web UI from `http://127.0.0.1:<port>`. No sidecar subprocess.
- Backend/domain logic lives in `packages/web/server/*` (and `packages/vscode/*` for VS Code bridge/runtime parity). Electron owns the desktop shell/security boundary: windows, menus, dialogs, notifications, updater, deep-links, runtime host switching, local IPC gates, and SSH/tunnel management.
- Do not add OpenCode feature backends to the native shell. Shared UI features should remain server/runtime APIs unless the capability is inherently native.
## Instruction Order
### Desktop Shell
Before editing:
- **Desktop work goes into `packages/electron/`.**
- Desktop-side changes (IPC handlers, native integrations, window/quit/notification behavior) land in `packages/electron/main.mjs` + `packages/electron/preload.mjs`.
- Electron imports the server via `@openchamber/web/server/index.js` (workspace dep) and calls `startWebUiServer({...})`. The returned handle has `getPort()` / `stop()`. Notifications flow via an `onDesktopNotification` callback injected at startup — no stdout-parsing IPC.
- Windows OS integrations must avoid console-window flashes. Any non-user-visible `child_process` call on Windows (system probes, tool discovery, updater/install helpers, SSH/tunnel helpers, cleanup, etc.) should run the target executable directly with `windowsHide: true`; detached/background helpers usually also need `stdio: 'ignore'`. Avoid `cmd.exe /c` pipelines and wrappers that spawn console grandchildren (`taskkill`, `ping`, nested `powershell`, batch shims), because `windowsHide` only reliably applies to the first child. If a delayed/background operation must outlive the app process, use a single hidden first-level helper (for example `powershell.exe -WindowStyle Hidden -EncodedCommand ...`) or a native Node/Electron API. Only omit this for intentionally user-visible shells/apps.
- Build/release: Electron is the desktop release target.
1. Follow this root guide.
2. Load every matching project skill.
3. Read the nearest `DOCUMENTATION.md` and package `README.md` when present.
4. Follow local code and test precedent.
## Tech stack (source of truth: `package.json`, resolved: `bun.lock`)
If these sources materially conflict, stop and resolve the conflict instead of silently choosing one.
- Runtime/tooling: Bun (`package.json` `packageManager`), Node >=22 (`package.json` `engines`)
- UI: React, TypeScript, Vite, Tailwind v4
- State: Zustand stores and sync layer (`packages/ui/src/stores/`, `packages/ui/src/sync/`)
- UI primitives: Base UI (`@base-ui/react`, primary source for dropdown/select/dialog/menu/tooltip/etc. — wrappers live in `packages/ui/src/components/ui/`), Radix UI (`package.json` deps, legacy usages being migrated), HeroUI (`package.json` deps), Remixicon as SVG sprite source only (use shared `Icon`, never direct `@remixicon/react` imports)
- Server: Express (`packages/web/server/index.js`)
- Desktop: Electron 41 (`packages/electron/`)
- VS Code: extension + webview (`packages/vscode/`)
## Runtime Boundaries
## Monorepo layout
- `packages/ui`: shared React UI, state, sync, and runtime contracts.
- `packages/web`: web surfaces, OpenChamber server, managed/external OpenCode lifecycle, and CLI.
- `packages/electron`: native desktop shell and privileged Electron boundary.
- `packages/vscode`: extension host, webview, and runtime bridge.
- `packages/mobile`: Capacitor iOS/Android shell; bundles the mobile web surface and connects to an existing OpenChamber server.
- `packages/docs`: product documentation; not a Bun workspace.
Workspaces are `packages/*` (see `package.json`).
Shared UI calls official OpenCode APIs through `@opencode-ai/sdk/v2`. OpenChamber-owned capabilities use `RuntimeAPIs`, `runtimeFetch`, and shared browser/realtime transport helpers. Server-side upstream integrations may use their owning runtime modules.
- Shared UI: `packages/ui`
- Web app + server + CLI: `packages/web`
- Desktop shell: `packages/electron`
- VS Code extension: `packages/vscode`
Electron starts the OpenChamber backend in-process, never as a sidecar. Development may load loopback/HMR UI; packaged builds load staged assets through `openchamber-ui://` while the loopback server remains the API backend. Keep domain backends in web/runtime modules unless behavior is inherently native.
## Documentation map
Shared contracts must define intentional behavior for every applicable runtime: web, desktop, VS Code, hosted mobile, and Capacitor mobile.
Before changing any mapped module, read its module documentation first.
## Always-On Constraints
### web
- Do not modify `../opencode`; it is a separate repository.
- Do not run git or GitHub commands unless the user explicitly asks.
- Do not add dependencies unless explicitly requested.
- Never add or log secrets, bearer tokens, pairing credentials, or sensitive user data.
- Keep changes minimal and preserve unrelated worktree changes.
- Enforce security and correctness in core/runtime logic, not only UI visibility or prompts.
- Keep entrypoints and bridges thin; place domain logic in focused owning modules.
- Update owning documentation when module ownership, contracts, or invariants change.
Web runtime and server implementation for OpenChamber.
## Correctness Invariants
#### lib
- Prefer authoritative state over heuristics.
- Derive live activity from live channels, not persisted history.
- Scope temporary fallbacks narrowly and clear them when authoritative state arrives.
- Never let fetch failure masquerade as authoritative empty success.
- Make partial results, rollback, cleanup, and stale-data behavior explicit.
- One failed entity must not erase or block unrelated complete entities.
- Runtime-specific differences must be intentional and visible in code.
Server-side integration modules used by API routes and runtime services.
## Documentation Discovery
##### event-stream
Before changing a module, search for the nearest `DOCUMENTATION.md`; before package-level work, read its `README.md`. Discover docs dynamically under `packages/**/DOCUMENTATION.md` rather than relying on a static exhaustive map.
OpenChamber-owned event stream helpers for server-sent runtime events.
High-value anchors:
- Module docs: `packages/web/server/lib/event-stream/DOCUMENTATION.md`
- Sync: `packages/ui/src/sync/DOCUMENTATION.md`
- Stores: `packages/ui/src/stores/DOCUMENTATION.md`
- CLI: `packages/web/bin/lib/DOCUMENTATION.md`
- VS Code runtime: `packages/vscode/src/DOCUMENTATION.md`
- Electron: `packages/electron/README.md`
- Mobile: `packages/mobile/README.md`
##### fs
## Project Skills
Filesystem routes, raw file access, search helpers, and workspace-scoped file operations.
Project skills live under `.agents/skills/*/SKILL.md`. Before editing, load every matching skill; multiple skills may apply. Skills are canonical for their detailed workflows and checklists.
- Module docs: `packages/web/server/lib/fs/DOCUMENTATION.md`
##### quota
Quota provider registry, dispatch, and provider integrations for usage endpoints.
- Module docs: `packages/web/server/lib/quota/DOCUMENTATION.md`
##### git
Git repository operations for the web server runtime.
- Module docs: `packages/web/server/lib/git/DOCUMENTATION.md`
##### github
GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing.
- Module docs: `packages/web/server/lib/github/DOCUMENTATION.md`
##### opencode
OpenCode server integration utilities including config management, provider authentication, and UI authentication.
- Module docs: `packages/web/server/lib/opencode/DOCUMENTATION.md`
##### notifications
Notification message preparation utilities for system notifications, including text truncation and optional summarization.
- Module docs: `packages/web/server/lib/notifications/DOCUMENTATION.md`
##### permission-auto-accept
Persistent server-owned permission auto-accept policy, subagent inheritance, retries, and reconnect reconciliation.
- Module docs: `packages/web/server/lib/permission-auto-accept/DOCUMENTATION.md`
##### scheduled-tasks
Scheduled task persistence, execution, and event fanout for recurring sessions.
- Module docs: `packages/web/server/lib/scheduled-tasks/DOCUMENTATION.md`
##### text
Text processing helpers shared by server-side routes and summarization flows.
- Module docs: `packages/web/server/lib/text/DOCUMENTATION.md`
##### terminal
WebSocket protocol utilities for terminal input handling including message normalization, control frame parsing, and rate limiting.
- Module docs: `packages/web/server/lib/terminal/DOCUMENTATION.md`
##### tts
Server-side text-to-speech services and summarization helpers for `/api/tts/*` endpoints.
- Module docs: `packages/web/server/lib/tts/DOCUMENTATION.md`
##### relay
Host side of the private relay: outbound E2EE tunnel that lets remote clients reach this instance through OpenChamber-hosted relay infrastructure without inbound exposure. Load the `relay-transport` skill before changing it or any WebSocket/streaming endpoint that rides it.
- Module docs: `packages/web/server/lib/relay/DOCUMENTATION.md`
##### tunnels
Tunnel provider setup and runtime helpers for exposing OpenChamber over remote URLs.
- Module docs: `packages/web/server/lib/tunnels/DOCUMENTATION.md`
##### ui-auth
UI session auth, client tokens, URL-token scoping, passkey/reset flows, and route-level auth gates.
- Module docs: `packages/web/server/lib/ui-auth/DOCUMENTATION.md`
##### skills-catalog
Skills catalog management including discovery, installation, and configuration of agent skill packages.
- Module docs: `packages/web/server/lib/skills-catalog/DOCUMENTATION.md`
### ui
Shared React UI, sync layer, runtime API contracts, and stores.
#### sync
Session synchronization, event pipeline, optimistic updates, caches, and live-state stores.
- Module docs: `packages/ui/src/sync/DOCUMENTATION.md`
#### stores
Zustand store ownership, persistence expectations, and store-splitting guidance.
- Module docs: `packages/ui/src/stores/DOCUMENTATION.md`
#### session sidebar
Session sidebar grouping, ordering, virtualization-adjacent behavior, and project/worktree display.
- Module docs: `packages/ui/src/components/session/sidebar/DOCUMENTATION.md`
#### message parts
Chat message part rendering and message-row performance expectations.
- Module docs: `packages/ui/src/components/chat/message/parts/DOCUMENTATION.md`
## Build / dev commands (verified)
All scripts are in `package.json`.
- Validate: `bun run type-check`, `bun run lint`
- Build all: `bun run build`
- Desktop build (Electron — primary): `bun run electron:build`
- Desktop dev (Electron): `bun run electron:dev`
- VS Code build: `bun run vscode:build`
- Release smoke build: `bun run release:test` (shell script: `scripts/test-release-build.sh`)
## Runtime entry points
- Web bootstrap: `packages/web/src/main.tsx`
- Web server: `packages/web/server/index.js`
- Web CLI: `packages/web/bin/cli.js` (package bin: `packages/web/package.json`)
- Desktop: `packages/electron/main.mjs` (boots the web server in-process via `startWebUiServer`, loads web UI over loopback; preload at `packages/electron/preload.mjs` exposes the desktop IPC bridge)
- VS Code extension host: `packages/vscode/src/extension.ts`
- VS Code webview bootstrap: `packages/vscode/webview/main.tsx`
## OpenCode integration
- UI client wrapper: `packages/ui/src/lib/opencode/client.ts` (imports `@opencode-ai/sdk/v2`)
- Sync/event pipeline: app roots mount `SyncProvider` from `packages/ui/src/sync/sync-context.tsx`; OpenCode SSE/WS event handling lives in `packages/ui/src/sync/event-pipeline.ts`
- Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`)
- Web runtime filesystem endpoints: `packages/web/server/lib/fs/routes.js`, registered by `packages/web/server/lib/opencode/feature-routes-runtime.js`
- External server support: Set `OPENCODE_HOST` (full base URL, e.g. `http://hostname:4096`) or `OPENCODE_PORT`, plus `OPENCODE_SKIP_START=true`, to connect to existing OpenCode instance
## Key UI patterns (reference files)
- Settings shell: `packages/ui/src/components/views/SettingsView.tsx`
- Settings shared primitives: `packages/ui/src/components/sections/shared/`
- Settings sections: `packages/ui/src/components/sections/` (incl `skills/`)
- Chat UI: `packages/ui/src/components/chat/` and `packages/ui/src/components/chat/message/`
- Theme + typography: `packages/ui/src/lib/theme/`, `packages/ui/src/lib/typography.ts`
- Terminal UI: `packages/ui/src/components/terminal/` (uses `ghostty-web`)
## External / system integrations (active)
- Runtime API contracts: `packages/ui/src/lib/api/types.ts`; React consumption via `packages/ui/src/hooks/useRuntimeAPIs.ts`
- Runtime transport/auth: `packages/ui/src/lib/runtime-fetch.ts`, `packages/ui/src/lib/runtime-url.ts`, `packages/ui/src/lib/runtime-auth.ts`
- Git: `packages/ui/src/lib/gitApi.ts`, `packages/web/server/lib/git/service.js` (`simple-git`)
- Terminal PTY: `packages/web/server/lib/terminal/runtime.js` (`bun-pty`/`node-pty`)
- Skills catalog: `packages/web/server/lib/skills-catalog/`, UI: `packages/ui/src/components/sections/skills/`
## Agent constraints
- Do not modify `../opencode` (separate repo).
- Do not run git/GitHub commands unless explicitly asked.
- Keep baseline green (run `bun run type-check`, `bun run lint` before finalizing changes).
## Agent code of conduct
- Prefer the smallest correct change.
- Preserve working behavior before improving structure.
- Do not add cleverness where a direct implementation is enough.
- Do not infer critical state from weak signals when a stronger source exists.
- Do not encode policy only in UI; enforce it in core logic.
- Do not hide data loss, partial failure, or fallback behavior. Make it explicit in code.
- Finish work end-to-end: implementation, verification, and cleanup.
## Development rules
- Keep diffs tight; avoid drive-by refactors.
- Follow local precedent; inspect nearby code before introducing new patterns.
- Backend changes: keep web, desktop, and VS Code behavior consistent when they share contracts.
- TypeScript: avoid `any`, blind casts, and shape guessing.
- React: prefer function components + hooks; use classes only when required.
- Control flow: prefer early returns and explicit branching over nested ternaries.
- Styling: Tailwind v4, typography via `packages/ui/src/lib/typography.ts`, theme vars via `packages/ui/src/lib/theme/`.
- Shared UI patterns: reuse shared primitives before introducing feature-local markup patterns.
- Toasts: use the wrapper from `@/components/ui`; do not import `sonner` directly in feature code.
- No new deps unless asked.
- Never add secrets or log sensitive data.
## Architecture patterns
### Thin entrypoints, focused modules
- Keep orchestration entrypoints thin: `index.js`, bridge files, bootstrap files, provider roots.
- Move route, domain, and runtime logic into focused modules with clear ownership.
- Prefer dependency injection over hidden module coupling.
- Add or update module documentation when ownership changes.
### Strong source of truth
- Prefer deterministic state over heuristics.
- Use live server/session state for live activity. Do not let historical anomalies masquerade as current execution.
- If a fallback is necessary, scope it narrowly to the active entity and treat it as temporary.
- Restore derived UI state from authoritative records. Example: restore model or agent from the latest user message, not assistant-side guesses.
### Live state vs historical state
- Derive live UI behavior from live state channels, not persisted history.
- Use historical records to restore context, not to infer that work is still in progress.
- If live state is delayed, use the narrowest possible transient fallback and clear it as soon as authoritative state arrives.
### Cross-runtime parity
- If web defines a route or payload contract that shared UI depends on, keep VS Code and desktop parity where applicable.
- Shared behavior differences must be intentional and visible in code.
- Do not ship a web-only assumption into shared UI.
### Partial-failure-safe flows
- Cross-directory and multi-entity operations must tolerate partial failure.
- Prefer per-item results, rollback paths, or resumable cleanup over all-or-nothing assumptions.
- Never leave optimistic state or local caches stranded after failure.
### Distinguish fetch failure from empty success
Client API methods that feed authoritative state (bootstrap, reconnect resync, retry loops) **must signal fetch failure distinctly from a successful-but-empty server response.** A method that swallows errors and returns `[]`/`{}`/`null` lets the caller delete or overwrite legitimate state on a transient network blip, indistinguishable from "the server says nothing here."
- **Decide which methods are authoritative.** A method is authoritative if any caller uses its result to delete, clear, or replace persisted/sync state. UI-display-only methods (autocomplete, dropdowns, settings pages) can keep silent-empty fallback because the user's next action refreshes them.
- **For authoritative methods, pick one of two patterns** — both already exist in the codebase, do not invent a third:
- **Throw on failure** (e.g. `listPendingPermissions`, `listPendingQuestions`, `listAgents`, the `unwrap()` helper in `packages/ui/src/sync/bootstrap.ts`). Use this when the caller has an outer `try/catch` per logical block — the throw skips the block and preserves prior state.
- **Return `T | null` on failure, where `null` strictly means "fetch failed"** (e.g. `getSessionStatusForDirectory`, the `.catch(() => null)` + early-return-on-null pattern at the per-session reconnect loop in `sync-context.tsx`). Use this when the caller has follow-up work that should still run when one fetch fails.
- **Never swallow inside the method while returning the same type as success.** The SDK's `{data, error}` shape already does this silently — wrap with `if (result.error) throw …` so the failure can't be lost.
- **Verify the caller actually preserves state on failure.** Adding the throw is only half the fix; the consumer must not run the "delete missing" / "overwrite" branch unless it knows the fetch succeeded. The relevant outer `try/catch` is often already there but dormant.
- **Retry loops require a failure signal.** A `for (let attempt = 0; attempt < 3; …)` retry around a method that swallows to `[]` will run exactly once — the loop never sees an error.
This rule is the API-layer counterpart of "Use live server/session state for live activity. Do not let historical anomalies masquerade as current execution." A fetch failure is the same kind of anomaly — don't let it masquerade as authoritative server state.
### Reconnect-loop pacing
The SSE/WebSocket reconnect loop in `packages/ui/src/sync/event-pipeline.ts` retries indefinitely. To avoid burning battery and server load on dead/idle connections, the loop's pacing must respect three signals:
- **`navigator.onLine`**: when the browser reports offline, use the long backoff cap (~60s) instead of the short one (~5s). The expected recovery path is the `online` event, not the next probe.
- **`document.visibilityState`**: when hidden, use the long cap too. A backgrounded PWA shouldn't hammer the network at 1/5s; the browser may also throttle our timers, but state the intent in code rather than relying on it.
- **HTTP status of the last failure**: permanent 4xx errors (401, 403, 404, …) don't recover from blind retry. Jump straight to the long cap instead of running the normal exponential path; otherwise a stale-path or expired-token client would put ~12 reqs/min on the server log forever. 408 (Request Timeout) and 429 (Too Many Requests) are retryable in spirit — let them go through normal backoff.
- **Consecutive failures**: real exponential growth (`base * 2^failures`, clamped), not constant 500ms. A hard-down server should see geometrically fewer probes per minute over time.
The inter-attempt wait must be interruptible by `online`, visibility-becomes-visible, and the pipeline's abort signal — otherwise recovery is delayed by however long the current sleep had left to run.
## CLI Parity and Safety Policy (MANDATORY)
### Principle: policy-first, UX-second
All safety and correctness rules MUST be enforced in core command logic, independent of output mode.
Interactive/pretty UX (`@clack/prompts`) is a presentation layer only.
It must never be the only place where validation or restriction is enforced.
### Required parity across modes
The same functional outcome and safety gates MUST hold for all execution modes:
- Interactive TTY (full Clack UX)
- Non-interactive shells (piped/stdin-less automation)
- `--quiet`
- `--json`
- Fully pre-specified flags (no prompts)
In all modes, invalid operations MUST fail with non-zero exit code and deterministic error semantics.
### Non-negotiable rule
Do not rely on prompts to enforce policy.
- Prompts MAY help users choose valid inputs.
- Core validators MUST run even when prompts are unavailable or skipped.
- `--quiet` suppresses non-essential output only; it does not weaken validation.
- `--json` changes output shape only; it does not weaken validation.
Detailed Clack UX patterns (primitives, prompt gating, and implementation checklist)
are defined in the `clack-cli-patterns` skill and should not be duplicated here.
## Project Skills (MANDATORY)
Project skills live under `.agents/skills/*/SKILL.md`. Before editing, agents **MUST** load every skill whose trigger matches the work; if multiple rows apply, load all of them.
| Work being done | Required skill call |
| Trigger | Required skill |
|---|---|
| Terminal CLI commands, prompts, or output formatting, especially `packages/web/bin/*` | `skill({ name: "clack-cli-patterns" })` |
| Shared UI data access, `RuntimeAPIs`, `runtimeFetch`, `runtime-url`, OpenCode SDK calls, VS Code bridges/proxies, authenticated browser assets, Electron runtime switching, or web server API endpoints | `skill({ name: "ui-api-decoupling" })` |
| UI components, styling, visual elements, colors, buttons, or icons | `skill({ name: "theme-system" })` |
| User-facing UI text: labels, buttons, placeholders, aria labels, empty/error/loading states, toasts, dialogs, settings copy, or navigation labels | `skill({ name: "locale-ui-patterns" })` |
| Settings pages, settings dialogs, configuration UI, or visual/layout changes inside Settings | `skill({ name: "settings-ui-patterns" })` |
| Drag-to-reorder, sortable lists/chips/grids, or `@dnd-kit` behavior including touch/mobile and wrapping variable-width items | `skill({ name: "drag-to-reorder" })` |
| iOS Simulator preview/control for the mobile app, `serve-sim`, simulator taps/typing/gestures/rotation, or headless install/launch workflows outside Xcode | `skill({ name: "serve-sim" })` |
| WebSocket/SSE/streaming endpoints (terminal, dictation/voice, event stream, notifications), opening a WebSocket in shared UI, runtime transport refactors (`runtime-fetch`/`runtime-url`/`runtime-switch`/`runtime-auth`), the private relay tunnel, or anything under `packages/ui/src/lib/relay` or `packages/web/server/lib/relay` | `skill({ name: "relay-transport" })` |
| Any source, dependency, export, build-config, generated-asset, package-contract, or module-ownership change | `openchamber-change-discipline` |
| CLI commands, prompts, terminal output, non-TTY, `--quiet`, or `--json` behavior | `clack-cli-patterns` |
| Shared UI data access, OpenCode SDK, `RuntimeAPIs`, runtime fetch/auth/URLs, bridges/proxies, runtime switching, or server API routes | `ui-api-decoupling` |
| Electron main/preload, IPC, native UI, updater, deep links, SSH/tunnels, packaging, or child processes | `desktop-shell` |
| Session sync, bootstrap/reconnect, reducers, polling, optimistic state, queues, live status, reconciliation, or directory-scoped caches | `sync-state-invariants` |
| Render/store/event hot paths, large lists, caching/indexing, high CPU/memory, lag, jank, freezes, or performance regressions | `performance-engineering` |
| WebSocket, SSE, streaming transport, runtime transport internals, or private relay | `relay-transport` |
| UI components, styling, colors, buttons, or icons | `theme-system` |
| User-facing or accessible UI text, labels, aria, toasts, dialogs, or navigation copy | `locale-ui-patterns` |
| Settings UI, settings dialogs, configuration surfaces, or settings search | `settings-ui-patterns` |
| Sortable or drag-to-reorder behavior, especially `@dnd-kit` and touch/wrapping layouts | `drag-to-reorder` |
| iOS Simulator build, launch, preview, gestures, or `serve-sim` control | `serve-sim` |
Skill docs are the source of truth for detailed patterns. Do not duplicate their full guidance here; load the skill and follow it before making matching changes.
Pure code-reading or explanation does not require implementation skills unless needed to interpret a specialized subsystem.
## Performance rules (MANDATORY)
## Validation
These rules exist because violating them has caused measurable regressions (render cascades, memory bloat, UI jank). They apply to all UI and sync layer work.
### Shared-store render discipline
- **Treat common stores as render fanout boundaries.** An unnecessary reference change in shared state can re-render large parts of the app.
- **Do not put high-frequency state in broadly consumed stores.** Fast-changing state should live in narrow stores with narrow subscribers.
- **Update only the fields that changed.** Preserve references for untouched state branches.
- **Prefer leaf selectors over container selectors.** Subscribe to the smallest stable value that satisfies the component.
- **Isolate hot consumers.** If a value changes often and only a few components need it, move it to a narrower store or consume it in a memoized child.
- **Do not subscribe shell/layout components to broad live collections.** If a shell only needs one field, entity, or derived flag, subscribe to that instead of the whole collection.
- **Treat provider roots as global hot paths.** A top-level provider must not subscribe to high-frequency data unless the feature is actually enabled and the subscription is essential.
### Zustand referential equality
Zustand skips re-renders when a selector returns the same reference (`Object.is`). Every new object/array reference triggers a re-render in every subscriber.
- **Never spread all state fields in an update.** Only create new references for fields that actually changed. A `message.part.delta` event should not clone `session`, `permission`, etc.
- **Select leaf values, not containers.** `useStore((s) => s.permission[sessionID])` is correct. `useStore((s) => s.permission)` subscribes to every permission change across all sessions.
- **Preserve references when merging.** If prepending older messages, keep existing message object references. Only add truly new items. Return the original array if nothing was added.
- **For derived collections, preserve item identity when presentation-relevant fields are unchanged.** Reuse previous item references for unchanged rows/items and move high-frequency live fields to narrow per-item selectors.
### Store splitting
A single store with N properties means every subscriber re-evaluates on every state change. Split stores by change frequency and subscriber set.
- **Group state by how often it changes.** Streaming state (updated 60/sec) must not live with user preferences (updated on click).
- **Group state by who reads it.** If only 2 components need a value, it belongs in a store that only those 2 subscribe to.
- **Cross-store reads use `.getState()`.** Actions in one store that need another store call `useOtherStore.getState()` — imperative, no subscription.
- **Never add unrelated state to an existing store** just because it's convenient. Create a new store.
### Event pipeline and SSE
- **Gate expensive operations on the hot path.** During streaming, `message.part.delta` and `message.part.updated` fire ~60/sec. Any `findIndex`, `filter`, or iteration added to these handlers multiplies across every event. Gate behind a cheap boolean check first (e.g., check `next[0]` before scanning the array).
- **Skip no-op updates.** If an incoming event doesn't change the state (same role, same finish, same timestamps), return `false` from the reducer to avoid creating new references.
- **Coalesce by key.** Same-entity events (e.g., repeated `session.status` for the same session) should replace earlier ones in the queue, not accumulate.
- **Preserve event ordering semantics.** Reducers and queues must not let stale deltas or out-of-order events corrupt the latest state.
- **Do not widen live-activity fallbacks.** A fallback for delayed status should inspect only the current trailing entity, not arbitrary historical records.
### Polling payload fidelity
- **Do not let lightweight polling erase rich fields.** If light mode omits fields (e.g., `diffStats`), preserve previous rich data until a heavy follow-up fetch lands.
- **Use two-phase polling.** Run cheap change detection first; only run heavy status fetches for directories that actually changed.
### Optimistic updates
- **Use the shadow Map pattern.** Insert optimistic data into the store for instant UI, AND register it in a separate tracking Map. Cleanup happens deterministically via `mergeOptimisticPage` on the next data fetch — not via heuristics in the event reducer.
- **Pass client-generated IDs to the server.** Use the same ID format as the server (hex-encoded timestamps). Pass `messageID` to `promptAsync` so the server echoes back the same ID. This prevents duplicates and enables in-place replacement.
- **Rollback on error.** Remove the optimistic entry from both the store and the shadow Map.
- **Stabilize bridge callbacks.** When wiring hook callbacks into module-level refs, use stable ref wrappers so effects do not loop on changing function identities.
### Session/input consistency
- **Capture send config at queue time.** Queue items must include provider/model/agent/variant snapshot; do not re-resolve from mutable live state at send time.
- **Keep server-selected attachments sendable.** Preserve server-backed file selections in queue/submit flows and convert them to proper `file://` URLs before sending.
- **Do not let text input state repaint unrelated chrome.** Typing should not force unrelated controls, menus, indicators, or toolbars to re-render on every keystroke.
- **Extract slow-changing chrome from hot input paths.** If controls do not depend on the current text value, move them behind memoized boundaries with stable callbacks.
### Bootstrap resilience
- **Treat startup 502/503 as transient.** Retry bootstrap/session-list flows with bounded retries/intervals, especially in VS Code where API readiness can lag bridge startup.
- **Use polling recovery when failures are swallowed.** If an async loader resolves without throwing on failure, recover with interval retries gated by loaded-state checks.
### Scroll and DOM
- **Never use `await waitForFrames()` for scroll preservation.** Frames of visible scroll jump are unacceptable. Use `useLayoutEffect` to adjust scroll synchronously after React commits DOM — before the browser paints.
- **Capture scroll state before the state change, restore in layout effect.** The pattern: save `scrollHeight`/`scrollTop` into a ref before triggering the update, consume it in `useLayoutEffect` on the rendered output.
- **Do not let viewport resizes masquerade as content growth.** Viewport-height changes must not trigger the same scroll compensation logic used for actual content growth.
- **Disable or narrow native/browser scroll anchoring when custom scroll logic exists.** Browser anchoring and app-managed pinning/follow logic will fight and produce jiggle.
- **Autosize textareas without transient collapse on growth.** Avoid `height='auto'` shrink/expand cycles on every character when the content only grew; this creates visible layout bounce.
### List ordering and view consistency
- **Do not sort structural lists directly from high-churn live fields.** If live updates are frequent, sorting directly from them causes reorder thrash and wide rerender cascades.
- **If live recency is required, freeze order during high-frequency updates and apply a one-shot reorder only at an intentional lifecycle edge.** Choose the lifecycle edge explicitly instead of letting every intermediate update reshuffle the UI.
- **Use one ordering source for all views of the same data.** Different views of the same entities must derive from the same ranked list or rank map; do not let each surface re-derive ordering independently.
- **Do not mix global snapshots and local live snapshots without an explicit reconciliation policy.** If multiple data sources feed one view, define which fields win and how they merge.
### Component isolation
- **Extract high-frequency hook consumers into separate components.** If a hook re-evaluates 60/sec (e.g., streaming status), wrap its consumer in a `React.memo` child component so the parent doesn't re-render.
- **Use custom `React.memo` comparators for message rows.** Compare render-relevant fields (role, finish, parts count, part IDs) — not object references.
### Caching and memory
- **Cap in-memory caches with both count and byte limits.** Entry count alone doesn't prevent memory bloat from large files. Use dual-constraint LRU (e.g., 40 entries OR 20MB).
- **Set store session limits to match loaded data.** If bootstrap loads N sessions, set `limit >= N`. Otherwise the next SSE event triggers trimming that silently removes sessions.
- **Invalidate caches on mutations.** File content cache must clear entries on write, delete, rename. Prefetch cache must clear on session eviction.
- **Use TTLs to prevent redundant fetches.** If a session was fetched <15s ago, skip re-fetching — SSE events keep it current.
### Directory context
- **Never cache directory strings in closures.** Directory can change at any time (worktree switch). Read it dynamically from `opencodeClient.getDirectory()` at call time.
- **Pass directory hints when the source of truth isn't available yet.** Newly created sessions aren't in the sync store until SSE delivers them. Pass the known directory as a parameter instead of relying on lookup.
## Regression-prevention checklist
- When adding fallback logic, ask: can stale persisted data keep this path active forever?
- When deriving UI state, ask: is this live state, historical state, or inferred state?
- When adding store fields, ask: who reads this, how often does it change, and should it live elsewhere?
- When touching polling or bootstrap, ask: can a lighter payload erase richer existing data?
- When handling optimistic updates, ask: where is rollback, reconciliation, and duplicate prevention?
- When changing shared routes or state contracts, ask: what breaks in web, desktop, and VS Code?
- When fixing a bug with a heuristic, prefer narrowing the heuristic over widening it.
## Validation expectations
- Run type-check/lint validation before finalizing source-code changes that can affect TypeScript, runtime behavior, builds, lint rules, package resolution, or generated assets, and run `bun run dead-code` when the change can add, remove, rename, or reshape files, exports, types, workspace entrypoints, or module imports. Keep validation scoped to the edited workspace by default. Prefer the package-level command for the package you changed (for example the relevant workspace's `type-check`/`lint`) instead of workspace-wide `bun run type-check` / `bun run lint`. Use workspace-wide checks only when the change spans multiple workspaces, shared package contracts, root tooling/config, dependency resolution, generated assets used across packages, or when a narrower command cannot cover the risk. Use a sufficiently long tool timeout for any broad checks (for example 240000ms) so successful package-level results are not lost to a tool timeout. For docs-only or isolated config-only changes, run the narrowest relevant validation instead (for example JSON/schema validation) and do not run full checks unless the change can affect code execution.
- For hot-path changes, verify behavior under streaming or repeated events, not just static render.
- For sync or startup changes, verify fresh load, retry/failure, and restart behavior.
- For session changes, verify create, stream, abort, permission, archive/delete, and revisit flows when relevant.
## Recent changes
- Releases + high-level changes: `CHANGELOG.md`
- Recent commits: `git log --oneline` (latest tags: `v1.11.7`, `v1.11.6`)
- Use `package.json` scripts as the command source of truth.
- Prefer focused tests and package-scoped type-check/lint for executable source changes.
- Use workspace-wide checks for cross-workspace contracts, root tooling, dependencies, or shared generated assets.
- Run `bun run dead-code` when source files are added/deleted/renamed or exports, types, entrypoints, or import shape change; inspect its report because it is non-blocking.
- Do not assume TypeScript/lint covers server JS, CLI JS, Electron helpers, or native behavior; run focused tests, syntax checks, builds, or runtime validation for the touched surface.
- For docs-only or isolated config changes, run the narrowest relevant validation.
- Report exactly what was and was not validated. Static checks alone do not prove runtime, relay, performance, or platform correctness.