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:
@@ -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');
|
||||
```
|
||||
@@ -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,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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user