diff --git a/.agents/skills/changelog-authoring/SKILL.md b/.agents/skills/changelog-authoring/SKILL.md new file mode 100644 index 00000000..bfce1eba --- /dev/null +++ b/.agents/skills/changelog-authoring/SKILL.md @@ -0,0 +1,94 @@ +--- +name: changelog-authoring +description: Use when drafting or updating user-facing CHANGELOG.md entries for the OpenChamber `[Unreleased]` section, including the VS Code extension changelog, summarizing changes since the latest git tag. +license: MIT +compatibility: opencode +--- + +## Overview + +Draft user-facing bullet points for the `## [Unreleased]` section that summarize changes since the latest git tag up to `HEAD`. + +Two files are maintained: + +- `CHANGELOG.md` — main app (Web, Desktop, Mobile/PWA, shared UI). +- `packages/vscode/CHANGELOG.md` — VS Code extension only. + +Only update the `[Unreleased]` bullets. Never add a new release header. + +## Gather Context First + +Read recent release sections for style. Determine the latest tag (or initial commit fallback), then inspect every commit and changed path through `HEAD`: + +```bash +BASE=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD) +git log --oneline "$BASE"..HEAD +git diff --stat "$BASE"..HEAD +``` + +Context gathering is complete when each user-visible change has evidence, platform reach, and contributor identity where available. + +## Squashed PR Merges + +A squashed merge commit often collapses a whole PR into a single terse subject line that omits valuable detail. When a commit looks like a squashed PR merge (subject ending in `(#123)`, or a `Merge pull request #123` commit), inspect the PR itself — its title and description usually carry the real user-facing context. + +Use `gh pr view --json number,title,body,author,mergedAt` for PR evidence. + +- Prefer the PR description over the squashed commit subject when the description explains the user-visible change more accurately. +- Do not copy PR descriptions verbatim; distill them into the changelog style below. +- Use PR author/metadata to attribute contributor credit (see Contributor Credit). +- If `gh` is unavailable or the PR cannot be fetched, fall back to the commit message and diff, and note any uncertainty rather than inventing details. + +## Writing Style + +- Match the tone and level of detail of the existing changelog. +- Write like release notes for real users, not marketing. Be concrete and plain-spoken. +- Avoid generic payoff clauses ("making X faster", "improving reliability", "for a smoother workflow", "so you can...") unless the diff clearly proves that exact user-visible outcome. +- Prefer short direct bullets: what changed, where users see it, and only one obvious consequence. +- Omit internal implementation details; do not replace them with vague benefits. If a technical change has no user-visible effect, omit it or group under a plain reliability bullet. +- Avoid internal component names unless users see them (ex: "VS Code extension", "Desktop app", "Web app"). +- Use area prefixes in the main changelog when they help grouping (e.g., "Chat:", "VSCode:", "Settings:", "Git:", "Terminal:", "Mobile:", "UI:"). +- Do not include commit hashes, file paths, or implementation notes in changelog text. +- Do not mention low-level mechanics ("local refs first", "source of truth", "route", "store", "cache", "payload", "ref resolution"). Translate only when there is a clear user-facing symptom. +- Avoid LinkedIn-style language. Bad: "commit review is faster and branch history is more reliable." Better: "commit history can now show file diffs inline." + +## Highlights and Ordering + +- Sort bullets by user impact, not commit order. Breaking changes first, then significant new capabilities or broad user-visible improvements, then smaller features, fixes, and visual polish. +- Keep the opening highlight block contiguous. Place every bold highlight before the first regular bullet; a regular bullet marks the end of the highlight block. +- Mark only the strongest highlights with a bold area prefix, such as `- **Chat attachments:** ...`. Usually the first 1–3 bullets; fewer when the release lacks substantial changes, more only when clearly justified. +- Treat a change as a highlight only when it introduces a substantial user-facing capability, materially changes a common workflow, or fixes a severe/widespread problem. Do not bold merely because a bullet is first, has a large diff, or was hard to implement. +- Keep related platform bullets together only when that does not push a more important change too far down. +- Rank highlights independently in each changelog. A main-app highlight is not automatically a VS Code highlight. + +## VS Code Changelog Rules + +- Craft entries only for behavior present in the VS Code extension. Exclude Desktop, Web, Mobile/PWA, and main-app-only UI. +- Do not copy shared/main bullets here unless changed files or code paths show the feature exists in the extension. +- Focus on core UI improvements and VS Code integration. +- Do NOT use "VSCode:" or "VS Code:" prefixes in this file. +- When unsure whether a change reaches the extension, leave it out. + +## Contributor Credit + +- Credit contributors inline with "(thanks to @username)" at the end of the bullet. +- Find usernames from commit authors (GitHub username, not email) or PR metadata when available. +- Skip credit when the contributor is `btriapitsyn` (repo owner). + +## Completion Criteria + +- For every bullet: "Could a user point to this in the UI or behavior?" If not, rewrite or drop it. +- For every VS Code bullet: verify the change applies to the extension, not just shared web UI or server code. +- For every bold bullet: "Would a user reasonably call this a headline change?" If not, unbold or move it lower. +- Read the finished list top to bottom; confirm each bullet is no more important than those above it, except where keeping related platform bullets together improves readability. +- Do not bundle unrelated changes to reduce bullet count. Prefer omitting minor internal fixes over vague catch-all sentences. +- Mention mostly-internal refactors only when there is a concrete user-visible fix; otherwise add no bullet. + +The lists are complete when every bullet is supported by inspected evidence, points to user-observable behavior, is ranked by impact, appears only in changelogs whose runtime receives it, and credits eligible contributors. + +## Workflow + +1. Gather repo style and complete git/PR context. +2. Propose the new `[Unreleased]` bullet list for the main `CHANGELOG.md`. +3. Propose the VS Code-specific `[Unreleased]` list for `packages/vscode/CHANGELOG.md`. +4. Edit both files to update their respective `[Unreleased]` sections. diff --git a/.agents/skills/clack-cli-patterns/SKILL.md b/.agents/skills/clack-cli-patterns/SKILL.md index 23e9468e..82c77101 100644 --- a/.agents/skills/clack-cli-patterns/SKILL.md +++ b/.agents/skills/clack-cli-patterns/SKILL.md @@ -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 --- @@ -17,36 +17,19 @@ Use this skill for terminal CLI work only (for example `packages/web/bin/*`). Do not use this skill for web UI or VS Code webview styling work. -## Mandatory Rules +## Mode Contract -1. **Validation first** - - Safety and correctness checks must run in all modes. - - Prompts may help collect input, but cannot be the only guard. +Run safety and correctness validation before presentation in every mode. Prompts collect missing input; they never enforce policy alone. -2. **Mode parity is required** - - Behavior must be equivalent in: - - Interactive TTY - - Non-interactive shells - - `--quiet` - - `--json` - - Fully pre-specified flags - - Invalid operations must fail deterministically with non-zero exit code. +| Mode | Prompt | Output | Failure | +|---|---|---|---| +| Interactive TTY | Allowed when input is missing | Framed human output | Concise human error, non-zero exit | +| Fully specified flags | None required | Human output | Same policy and exit semantics | +| Non-TTY/piped | Never | Deterministic script-safe output | Non-zero without hanging | +| `--quiet` | Never | Essential result only | Concise error, non-zero exit | +| `--json` | Never | JSON only, including warnings/errors | JSON failure payload, non-zero exit | -3. **Prompt guard contract** - - Only prompt when all are true: - - stdout is TTY - - not `--quiet` - - not `--json` - - not automated/non-interactive context - -4. **Output contract** - - `--json`: machine-readable output only. - - `--quiet`: suppress non-essential output only. - - Neither mode weakens policy enforcement. - -5. **Cancellation contract** - - Handle prompt cancellation with `isCancel` + `cancel(...)`. - - Handle SIGINT cleanly and use consistent exit semantics. +Handle prompt cancellation with `isCancel` + `cancel(...)` and SIGINT with consistent exit semantics. ## Clack Primitive Standard @@ -140,9 +123,9 @@ Quiet output should still be complete enough for scripts and quick human scannin - Prefer this style over boxed notes for routine follow-up actions. - Reserve `note`/boxed callouts for rare, high-context guidance where a long paragraph is truly necessary. -## Parity Verification Matrix +## Completion Criteria -For each command/subcommand, manually verify: +Every command/subcommand must have a tested answer for: 1. default interactive TTY output 2. `--quiet` output (minimal but informative) @@ -150,67 +133,14 @@ 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 +Load `references/snippets.md` when implementing prompt guards, non-interactive fallback, spinner lifecycle, or JSON/human output branching. -```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 .'); - } -} -``` - -### 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'); -``` - -## Implementation Checklist - -1. Add or update core validators first. -2. Ensure validators execute in all modes. -3. Add interactive Clack UX only as enhancement. -4. Verify parity between interactive and non-interactive flows. -5. Ensure script-safe deterministic failure behavior. +Implementation is complete when validators run before every mode branch, interactive Clack UX is only an enhancement, and all five cases above produce deterministic output and exit behavior. ## 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` diff --git a/.agents/skills/clack-cli-patterns/references/snippets.md b/.agents/skills/clack-cli-patterns/references/snippets.md new file mode 100644 index 00000000..8ac30943 --- /dev/null +++ b/.agents/skills/clack-cli-patterns/references/snippets.md @@ -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 .'); + } +} +``` + +## 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'); +``` diff --git a/.agents/skills/communication-style/SKILL.md b/.agents/skills/communication-style/SKILL.md new file mode 100644 index 00000000..d1760fb8 --- /dev/null +++ b/.agents/skills/communication-style/SKILL.md @@ -0,0 +1,81 @@ +--- +name: communication-style +description: Use it always. +author: poteto (pstack) +--- + +# Communication style + +Edit text to remove AI patterns and add human voice. + +## Process + +1. Scan for the patterns below. +2. Rewrite. Preserve meaning, match intended tone. +3. Add soul (see next section). +4. Self-audit: "What makes this obviously AI generated?" Fix remaining tells. + +## Adding soul + +Removing patterns is half the job. Sterile, voiceless writing is just as obvious. + +- **Have opinions.** React to facts instead of neutrally listing pros and cons. +- **Vary rhythm.** Short sentences. Then longer ones that take their time. Mix it up. +- **Acknowledge complexity.** "Impressive but also kind of unsettling" beats "impressive." +- **Use "I" when it fits.** First person isn't unprofessional. +- **Let some mess in.** Perfect structure looks machine-made. +- **Be specific.** Not "this is concerning" but "there's something unsettling about agents churning away at 3am." + +## Patterns to detect and fix + +### Content + +1. **Puffery.** "pivotal moment", "testament to", "evolving landscape", "setting the stage for", "indelible mark", "deeply rooted". Cut puffery, state what happened. +2. **Name-dropping.** Listing media outlets without context. Pick one, say what was said. +3. **Superficial -ing phrases.** "highlighting...", "ensuring...", "reflecting...", "showcasing...", "fostering...". Delete or expand with real sources. +4. **Promotional language.** "nestled", "vibrant", "breathtaking", "groundbreaking", "renowned", "stunning", "must-visit". Use neutral descriptions. +5. **Vague attributions.** "Experts believe", "Industry reports suggest", "Some critics argue". Name the source or delete. +6. **Formulaic challenges.** "Despite challenges... continues to thrive." Replace with specific facts. + +### Language + +7. **AI vocabulary.** Additionally, crucial, delve, enduring, enhance, fostering, garner, interplay, intricate, landscape (abstract), pivotal, showcase, tapestry (abstract), testament, underscore, vibrant. Replace with plain words. +8. **Fancy ways to say "is".** "serves as", "stands as", "boasts", "features". Just say "is" or "has". +9. **"Not just X, but Y."** State the point directly instead. +10. **Rule of three.** Forcing ideas into groups of three. Use the natural number. +11. **Synonym cycling.** Protagonist, main character, central figure, hero all in one paragraph. Pick one, repeat it. +12. **False ranges.** "from X to Y" where X and Y aren't on a meaningful scale. List topics directly. + +### Style + +13. **Em dash overuse.** Avoid em dashes entirely. Use periods or commas only (no parentheses, no en dashes, no hyphen-as-dash substitutes). Em dashes are an AI tell, and reaching for parentheses instead just trades one tell for another. If a thought needs separation, end the sentence or use a comma. +14. **Colon overuse.** Colons are fine before a list or example. Not as mid-sentence connectors. "If you're coming from traditional automation: instead of registering event handlers, you describe conditions" adds nothing with the colon. Rewrite to let the point stand on its own without comparison framing. "Describing when the scheduler should fire works best as plain English." Same meaning, no crutch punctuation. +15. **Boldface overuse.** Don't bold every proper noun or acronym. +16. **Inline-header lists.** The tell is a bold label and colon that restates the line: "**Performance:** Performance improved...". Convert to prose. A bold lead-in that ends in a period, names the item, and is followed by genuinely new detail ("**Schema in TypeScript.** Tables live in one file.") is fine. +17. **Title case headings.** Use sentence case. +18. **Decorative emojis.** Remove from headings and bullets. +19. **Curly quotes.** Replace with straight quotes. + +### Communication artifacts + +20. **Chatbot phrases.** "I hope this helps!", "Let me know if...", "Of course!", "Certainly!", "Found the smoking gun!" Remove. +21. **Cutoff disclaimers.** "While specific details are limited..." Find sources or remove. +22. **Sycophantic tone.** "Great question! You're absolutely right!" Respond directly. + +### Filler + +23. **Filler phrases.** "In order to" becomes "To". "Due to the fact that" becomes "Because". "It is important to note that" gets deleted. +24. **Excessive hedging.** "could potentially possibly be argued that it might" becomes "may". +25. **Generic conclusions.** "The future looks bright." State specific plans or facts. + +### Jargon + +26. **Abstract metaphor nouns.** Substrate, wedge, vector, locus, vantage, nexus, primitive (as noun), harness (as metaphor), surface (as in "API surface"), bedrock, scaffolding (as metaphor), modality, paradigm, gold-plating, ratchet (as metaphor), evacuate (for moving code), endgame, north star, flywheel. These read as technical but usually have a plainer concrete word. "Substrate" becomes "base". "Wedge in" becomes "add". "Vector" becomes "way" or "method". "Gold-plating" becomes "more than the job needs". "Ratchet" becomes the mechanism's real name or "a limit that only tightens". "Evacuate" becomes "move out". "Endgame" becomes "the last phase". Pick the concrete word. + +### Plain speech + +27. **Say what it does, not how it feels.** "the database stays close at hand", "SQL you can read", "types that follow your schema" name a feeling. The fix names the mechanism or a number: "`.toSQL()` returns the exact string sent to the database", "a column rename fails the build". Ask what the sentence tells the reader to do or know, then write it. If you can't restate it as a concrete instruction, fact, or number, cut it. One more check: if the sentence could appear unchanged in another project's docs, it says nothing about this one. Cut it. +28. **Shorten or split dense sentences.** If the reader has to backtrack to parse a sentence, break it in two or drop clauses. One idea per sentence. +29. **Active voice.** Prefer it. Catch "is/are/was/were + past participle" and name the actor: "queries are validated" becomes "the compiler validates queries", "the file is parsed by the loader" becomes "the loader parses the file". Passive is fine only when the actor is unknown or genuinely doesn't matter. +30. **Cut adverbs, or use a stronger verb.** "runs quickly" becomes "is fast" or the number. "significantly improves" becomes the measured delta. An adverb propping up a weak verb means the verb is wrong. +31. **Prefer the plain word.** "utilize" becomes "use", "leverage" becomes "use", "facilitate" becomes "help", "numerous" becomes "many", "in the event that" becomes "if". The fancier synonym is rarely clearer. diff --git a/.agents/skills/desktop-shell/SKILL.md b/.agents/skills/desktop-shell/SKILL.md new file mode 100644 index 00000000..7c642910 --- /dev/null +++ b/.agents/skills/desktop-shell/SKILL.md @@ -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 + +## Required Context + +Read `packages/electron/README.md` and nearby `packages/electron` code before editing. Context gathering is complete when each changed behavior is assigned to main, preload, renderer/shared UI, or web/runtime ownership. + +Load `ui-api-decoupling` when a native change adds or alters a renderer-facing capability, `RuntimeAPIs`, runtime auth/URL behavior, or shared bridge contract. This skill owns the Electron privilege boundary; `ui-api-decoupling` owns the shared UI/runtime contract. + +## 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 renderer contracts and domain logic in `packages/ui`, server behavior in `packages/web`, and Electron focused on inherently native behavior: windows, menus, dialogs, notifications, updater, deep links, runtime host switching, privileged IPC, SSH, and tunnel lifecycle. +- 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 focused Electron tests and package checks. For startup, preload, routing, or packaging changes, completion requires both HMR development and bundled UI validation. For Windows process work, completion requires inspection of the complete process tree with no console flash; command success alone is insufficient. diff --git a/.agents/skills/drag-to-reorder/SKILL.md b/.agents/skills/drag-to-reorder/SKILL.md index b8eec042..e524d683 100644 --- a/.agents/skills/drag-to-reorder/SKILL.md +++ b/.agents/skills/drag-to-reorder/SKILL.md @@ -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 --- @@ -79,7 +79,7 @@ const onDragEnd = (e: DragEndEvent) => { IDs must be **stable per item** (derive from the item's identity, e.g. `type:name`), never the array index — index ids break tracking after the first move. -## Minimal working pattern (wrapping, variable width, desktop + touch) +## Minimal Wiring ```tsx import { DndContext, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; @@ -97,41 +97,21 @@ const Item: React.FC<{ id: string; label: string; onClick: () => void }> = ({ id ); }; -const Row: React.FC<{ items: Item[]; onReorder: (next: Item[]) => void }> = ({ items, onReorder }) => { - const sensors = useSensors( - useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), - useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 6 } }), - ); - const onDragEnd = (e: DragEndEvent) => { - const { active, over } = e; - if (!over || active.id === over.id) return; - const from = items.findIndex(i => i.id === active.id); - const to = items.findIndex(i => i.id === over.id); - onReorder(arrayMove(items, from, to)); - }; - return ( - - i.id)} strategy={rectSortingStrategy}> -
- {items.map(i => )} -
-
-
- ); -}; +// Configure sensors per Rule 3, reorder onDragEnd per Rule 5, and choose the +// SortableContext strategy from Rule 2. This item wiring preserves item width. ``` A clickable element can be draggable at the same time: keep `onClick` on the button and the activation constraint (distance/delay) lets a plain click/tap through. -## Pitfalls we already hit (don't repeat) +## Symptom Index | Symptom | Cause | Fix | |---------|-------|-----| -| Dragged item **stretches** to the target slot width | `CSS.Transform.toString` applies scaleX/scaleY | Use `CSS.Translate.toString` (Rule 1) | -| On narrow/multi-row: items **don't reflow to other rows, overlap**, unclear drop target | `horizontalListSortingStrategy` on a wrapping row | Use `rectSortingStrategy` (Rule 2) | +| Dragged item **stretches** to the target slot width | Scale from `CSS.Transform.toString` | Rule 1 | +| On narrow/multi-row: items **don't reflow to other rows, overlap**, unclear drop target | Single-row strategy on wrapping layout | Rule 2 | | **"Maximum update depth exceeded"** during drag + dragged element floats **offset from the cursor** | Live-reorder in `onDragOver` (empty strategy + `setState` each over) oscillates A↔B with variable sizes; the empty `DragOverlay` we paired with it was mispositioned | Don't reorder in `onDragOver`. Reorder once in `onDragEnd` (Rule 5). Only reach for live-reorder if you truly need physical row-reflow, and then guard against oscillation. | -| Touch drag scrolls the page instead of dragging | Missing `touch-action: none` | Add `touch-none` (Rule 4) | -| Touch: every finger move drags, or tap doesn't register | Single `PointerSensor` with distance | Split into MouseSensor + TouchSensor(delay) (Rule 3) | +| Touch drag scrolls the page instead of dragging | Missing touch ownership | Rule 4 | +| Touch: every finger move drags, or tap doesn't register | One sensor for mouse and touch | Rule 3 | ## If `rectSortingStrategy` still isn't crisp enough @@ -142,3 +122,7 @@ Reordering variable-width chips across wrapped rows is a documented rough edge i - Variable-width wrapping chips: `packages/ui/src/components/chat/DraftPresetChips.tsx` - Single-row tab strip: `packages/ui/src/components/ui/sortable-tabs-strip.tsx` - Library: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` (already in `packages/ui/package.json`) + +## Completion Criteria + +Verify every applicable rule on desktop and touch. Wrapping layouts must preserve item width, reflow across rows, allow taps and scrolling before long-press activation, and reorder exactly once on drag end with stable IDs. diff --git a/.agents/skills/locale-ui-patterns/SKILL.md b/.agents/skills/locale-ui-patterns/SKILL.md index ff57255b..8b455397 100644 --- a/.agents/skills/locale-ui-patterns/SKILL.md +++ b/.agents/skills/locale-ui-patterns/SKILL.md @@ -9,12 +9,16 @@ description: Use when creating or modifying OpenChamber UI text, labels, buttons User-facing UI text must go through `@/lib/i18n`; do not hardcode English strings in components. -Use this skill for any React UI change that adds or edits visible text, accessible labels, placeholders, tooltips, toasts, dialogs, settings labels, navigation labels, or empty/error states. +## Translate everything immediately (no English placeholders) + +Every key you add to a non-English dictionary MUST contain a real translation in that language — never the English source string as a stand-in. There is NO "leave it in English for now" convention in this project; if an agent told you there was, it was wrong. Copying the English value into `es.ts`/`fr.ts`/`ko.ts`/`pl.ts`/`pt-BR.ts`/`uk.ts`/`zh-CN.ts`/`zh-TW.ts` is a defect, not a deferral. The app ships every locale at once, so an untranslated key is a visible bug for those users. + +If you genuinely cannot translate a language, say so explicitly to the user instead of silently pasting English. Do not invent a fallback policy. ## Required Flow 1. Add or reuse a key in `packages/ui/src/lib/i18n/messages/en.ts`. -2. Add the same key to every non-English dictionary in `packages/ui/src/lib/i18n/messages/`. +2. Add the same key — fully translated, not the English text — to every non-English dictionary in `packages/ui/src/lib/i18n/messages/`. 3. In components, call `const { t } = useI18n()` from `@/lib/i18n` and render `t('key')`. 4. For locale names or language picker labels, use `label(locale)` from `useI18n()`. 5. Keep locale state in `packages/ui/src/lib/i18n/*`; do not add locale fields to broad stores like `useUIStore`. @@ -98,20 +102,11 @@ date : t('dialog.delete.description', { count }) ``` -## What Counts As UI Text +## Translation Boundary -- Button and menu labels -- Settings labels and descriptions -- Placeholder text -- Tooltip content -- Dialog titles/descriptions/actions -- Toast title/description/action labels -- Empty/error/loading states -- `aria-label`, `title`, image `alt` text when user-facing +Translate visible text, placeholders, tooltips, dialogs, toasts, empty/error/loading states, and user-facing `aria-label`, `title`, and `alt` text. -## Exceptions - -Do not translate: +Keep these literal: - Product names: `OpenChamber`, `OpenCode`, `GitHub` - Protocol/tool acronyms: `MCP`, `SSE`, `WebSocket`, `API` @@ -119,10 +114,11 @@ Do not translate: - File paths, command names, environment variables - User/generated content -## Review Checklist +## Completion Criteria - No new hardcoded user-facing English in changed UI files. -- Every new key exists in all dictionaries. +- Every new key exists in all dictionaries with a real translation. +- All translated values are resolved inside a reactive render/hook boundary. - No locale state added to broad/shared stores. - No full app remount for locale changes. - Locale switch preserves current UI state. diff --git a/.agents/skills/openchamber-change-discipline/SKILL.md b/.agents/skills/openchamber-change-discipline/SKILL.md new file mode 100644 index 00000000..1bebbdc0 --- /dev/null +++ b/.agents/skills/openchamber-change-discipline/SKILL.md @@ -0,0 +1,98 @@ +--- +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. + +## Before Editing + +1. Inspect nearby implementation, callers, and tests before introducing a pattern. +2. Classify every applicable change risk below. +3. Identify every affected consumer, runtime, persisted format, and public export. This step is complete only when each risk has an owner and required validation. + +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. + +## Structural Discipline + +- Preserve behavior established by callers and tests unless the request replaces it. Keep the diff scoped to the complete requested behavior. +- Make the normal use-case path read top to bottom in domain terms. Keep orchestration entrypoints thin and move mechanics or domain logic behind focused, intention-revealing boundaries. +- Pull complexity downward only when a boundary hides meaningful mechanics, owns an invariant, isolates a proven integration, or captures stable repetition. Do not spread obvious code across pass-through layers. +- Prefer explicit dependencies and dependency injection over hidden module coupling. +- Follow local TypeScript types; avoid `any`, blind casts, and guessed payload shapes. +- Reject invalid inputs and broken preconditions early so the valid path stays flat. Do not force a numeric happy-path/error-path ratio when correctness requires substantial failure handling. +- Require evidence before adding retries, caches, compatibility paths, lifecycle machinery, or generalized race handling. Security, data-loss, destructive-operation, and concurrency invariants still require proactive design when the risk is inherent to the operation. +- Make partial failure, rollback, cleanup, and user-visible outcomes explicit for destructive or multi-step work. + +## Review Prompts + +Before broadening a change, ask: + +- Is the new abstraction reused or merely possible to reuse? +- What concrete complexity, invariant, stable repetition, or boundary does each new helper, interface, layer, and file pay for? +- 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 + +| 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 plus the applicable failure/ordering cases: missing-versus-empty, malformed data, stale reads versus newer mutations, out-of-order writes, lifecycle handling for debounced writes, conversion, and failed-write/migration rollback | +| 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. +- Perform a final simplification pass: remove speculative branches, shallow wrappers, stale compatibility, and names that do not clarify intent. diff --git a/.agents/skills/performance-engineering/SKILL.md b/.agents/skills/performance-engineering/SKILL.md new file mode 100644 index 00000000..8194aaab --- /dev/null +++ b/.agents/skills/performance-engineering/SKILL.md @@ -0,0 +1,303 @@ +--- +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. + +Load `sync-state-invariants` when an optimization changes state authority, reconciliation, optimistic data, event ordering, cache lifecycle, or destructive cleanup. This skill owns measured cost; `sync-state-invariants` owns state correctness. + +## 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 + +Complete the numbered workflow in order. An optimization is complete only when the exact measured scenario meets its budget and separate correctness checks preserve every applicable state, identity, layout, and lifecycle transition. + +### 0. Trust The Measurement Before Trusting The Number + +A measurement setup that is wrong produces clean, confident, wrong numbers, and +a clean number ends an investigation. Establish validity first. + +**Prove the environment is not throttled.** Chrome stops producing frames and +throttles timers for windows it considers backgrounded or occluded, headless or +not. A capture taken that way reports near-zero rendering work no matter what +the page does. Disable background/occlusion throttling at launch and measure +frame liveness inside the capture. The same applies to any environment that +idles when unobserved. + +**Prove zero is a measurement.** A metric reading zero, absent, or perfectly +quiet is a claim that requires evidence, because a disabled instrument reports +exactly the same thing. `RunTask` only appears under the disabled-by-default +timeline category; a scenario opened for the wrong directory renders nothing at +all. Before believing a quiet result, confirm the instrument fired and the +workload actually ran: assert on an independent signal, such as DOM growth +alongside the application's own render counters. + +**Prove the workload is comparable.** When the stimulus varies in size between +runs, per-second and total figures are not comparable. Normalise by units of +work delivered, and check run-to-run spread on an unchanged build before +attributing any difference to a change. + +Do not report a number whose validity you have not established. State which +validity checks ran. + +### 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. + +Treat every proposed optimization as a hypothesis. Memoization, caches, indexes, workers, scheduling, retries, and lifecycle machinery must address an observed cost or failure in the measured path; “could be slow” or “might race” is not evidence. Keep only the smallest mechanism that meets the contract, except where an inherent security, data-loss, destructive-operation, or concurrency invariant requires proactive protection. + +**Never accept an "after" without a "before" on the identical scenario and +build.** Measuring a fixed build against a remembered number, a different +scenario, or a nearby baseline proves nothing: the mechanism you changed may +not even execute in the path you measured. Re-run the unchanged build through +the same scenario, however inconvenient the rebuild. Expect to discover that a +plausible fix changes nothing. + +**A sampling profiler cannot explain native work.** Self time attributed to +`(program)` says only that the time was not in interpreted JavaScript. Use the +timeline trace, which names parsing, style recalculation, layout, layerization, +paint, and raster, and reserve the sampler for attributing application code. + +**Reproduction may require production scale you do not have.** A threshold +effect is invisible below its threshold, and a development workspace is usually +below it. When a report will not reproduce, compare the reporter's scale +against yours on the specific dimension the code keys on before concluding the +bug is absent. + +Profiling identifies where time is spent; it does not prove behavioral equivalence. Separately verify the applicable state, identity, layout, and lifecycle transitions for every structural optimization. + +### 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(); +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. +- Treat every custom memo/equality comparator as a correctness boundary. Inventory every render-relevant value that comparator gates and observe its canonical identity or an explicit semantic version covering the same semantics. +- Do not compare a proxy, aggregate, fallback, or differently resolved identity when the gated render path uses another source. Stable entity IDs do not imply stable rendered content; changes to comparator-gated semantics under the same ID must invalidate affected consumers, while semantically equivalent replacements may remain stable. +- Prefer leaf subscriptions for isolated high-frequency state over threading broad state through custom comparators. Keep comparator work bounded so render fanout is not merely replaced by recursive comparison fanout. +- 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. + +## Virtualization Contracts + +Virtualization changes layout, mounting, measurement, focus, and scroll semantics. It is not behaviorally equivalent merely because steady-state visible rows look the same. + +Before virtualizing a collection, define: + +- the actual scrolling element and whether it directly contains the virtualizer or is an ancestor; +- how total virtual height and the final item remain reachable from that scroller; +- estimated versus measured sizes, including expanded, nested, and dynamically resized items; +- initialization, remount, and activation-threshold behavior; +- interactions that depend on mounted DOM, including incremental reveal, focus, selection, drag-and-drop, menus, and accessibility traversal. + +When activation is threshold-based, test threshold minus one, threshold, and threshold plus one. Also test applicable collapsed/expanded, hidden/visible, filtered/unfiltered, and short/long transitions. If the current DOM or scroll topology cannot expose the virtual tail reliably, correct that topology or retain normal rendering rather than virtualizing solely by item count. + +## 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. + +Do not introduce a cache merely to make an abstraction reusable or prepare for future consumers. First prove repeated work in the real path; then place the cache with the narrowest owner and lifetime that can invalidate it correctly. + +A cache inside an `O(consumers × entities × candidates)` loop is a mitigation, not automatically a complete fix. + +## Repository Tooling + +`scripts/perf/DOCUMENTATION.md` is the entry point: it covers every capture +command, how to stand up a production build to measure against, how to read the +artifacts, and the validity guarantees these scripts enforce. Read it before +measuring. + +Four unattended capture commands exist; prefer them over ad-hoc timing code, +and extend them when a scenario is missing rather than measuring by hand. + +| Command | Answers | +|---|---| +| `bun run profile:idle` | What the app does while nobody interacts with it. Supports `--session`, `--tab`, `--then-tab`, `--panel`, `--expand-projects` to reach a specific mounted state, plus `--baseline` and `--budget-*` for regression gating. | +| `bun run profile:session` | What a streaming assistant response costs. Creates a session, dispatches a prompt through the `openchamber session` CLI, and records until the session reports idle. Reports the long-task distribution, a timeline-trace breakdown, running animations, and output-normalised metrics. | +| `bun run profile:animation` | What a CSS animation costs, isolated from the app. Animate only `transform` and `opacity`; everything else recalculates style every frame. | +| `bun run profile:browser` | A manually driven capture when the interaction cannot be scripted. | + +Both automated commands fail loudly rather than reporting a clean result when +the renderer was throttled, the trace collected no tasks, or the scenario never +rendered. Keep that property when extending them. + +Measure a production build. A development build's render and bundle behaviour +does not represent what users run. + +## 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; +- when custom comparators change, tests proving both directions: unrelated or semantically equivalent updates preserve the boundary, while changes to comparator-gated identity, membership, content, and source semantics invalidate it; +- when memoized tree/list consumers change, same-ID replacements and rebuilt-container fixtures covering both semantic change and semantic equivalence; +- when virtualization changes, tests using the real scrolling ancestor that prove final-item/control reachability and stable scroll, focus, and interactions; include activation-boundary cases when such a boundary exists; +- 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. + +## Revert What You Cannot Measure + +A change that does not move its target metric is not a small win, a safety +improvement, or a cleanup. It is unvalidated complexity, and shipping it under +a performance rationale makes the next investigation harder by implying the +path was already optimised. Revert it and record the hypothesis as rejected. + +This applies to a change whose benefit appears only in reasoning, one measured +against the wrong baseline, and one whose measured scenario turns out to behave +identically without it. + +Report negative results explicitly. "Disabling this removed 40% of the +layerization, and the fix that preserved the visuals did not" is a finding, and +the next person needs it. + +## Know When To Stop + +Compare the remaining cost against the user-facing budget, not against zero. +When the interaction already sits far inside budget, further optimisation of +that path trades real regression risk for an invisible gain, and it displaces +work on the path the user actually reported. Say so and move on. + +Cost that comes from intentional, user-visible behaviour is not waste. Removing +it is a product decision, not a performance fix, and it needs the owner's +agreement rather than a quiet commit. + +## 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. + +## Exit Checklist + +- [ ] Measurement validity established: no throttling, instruments confirmed firing, workload comparable. +- [ ] Baseline captured from the unchanged build through the identical scenario. +- [ ] 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. +- [ ] Structural optimizations have transition-focused correctness coverage independent of performance measurements. +- [ ] When mount topology or activation boundaries change, instrumentation distinguishes those transitions from steady state. +- [ ] Every change retained is justified by a measured difference; unvalidated ones reverted and recorded as rejected. +- [ ] Remaining cost compared against the budget, and stopping justified when inside it. +- [ ] Correctness, type, lint, and relevant runtime validations pass. diff --git a/.agents/skills/relay-transport/SKILL.md b/.agents/skills/relay-transport/SKILL.md new file mode 100644 index 00000000..7645b59f --- /dev/null +++ b/.agents/skills/relay-transport/SKILL.md @@ -0,0 +1,67 @@ +--- +name: relay-transport +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 +--- + +## Overview + +OpenChamber has a private relay: a client (mobile app, browser, another desktop) reaches a user's instance through an OpenChamber-hosted relay over an **end-to-end encrypted tunnel**. All of the app's traffic — many HTTP requests, the event stream (SSE), and WebSockets (terminal, dictation) — is multiplexed and encrypted through **one** connection per client. + +Architecture overview: `packages/web/server/lib/relay/DOCUMENTATION.md`. Code: `packages/ui/src/lib/relay/` (client + shared, TS) and `packages/web/server/lib/relay/` (host, JS). + +Load `ui-api-decoupling` when the change adds or alters a shared runtime API, URL/auth contract, bridge, proxy, or runtime-switch behavior. This skill owns relay mechanics; `ui-api-decoupling` owns the shared UI/runtime boundary. + +**Why this skill exists:** relay bugs do not show up in normal testing. The event stream is SSE (which behaves differently from WebSockets), so a new WebSocket feature is often the *first* real WebSocket to cross the tunnel on mobile — and it fails there while working everywhere else. We have fixed the same class of bug across several iterations. The rules below are those lessons. + +## The core mental model + +- **The tunnel is transparent.** A feature should reach the server through the shared runtime transport (`runtimeFetch`, `openRuntimeWebSocket`) and never know whether it is direct or relayed. If a feature constructs its own `fetch`/`WebSocket` against a runtime URL, it bypasses the tunnel and breaks in relay mode. +- **Three transports behave differently over the tunnel:** + - HTTP and SSE authenticate with the client's **bearer token** (a header). They "just work" through the tunnel for any allowlisted `/api/*`, `/auth/*`, `/health` path. + - **WebSockets cannot send headers.** They authenticate with a short-lived **URL-scoped token** (`oc_url_token`) that must be minted first and passed as a query parameter. This is the source of most relay WS bugs. + +## WebSocket Endpoint Branch + +Adding a new WS endpoint (or porting one, e.g. the planned terminal port) requires ALL of these, or it breaks over the relay: + +1. **Open it via `openRuntimeWebSocket`** (`packages/ui/src/lib/relay/runtime-socket.ts`), never `new WebSocket(...)` directly. A raw `new WebSocket` against a runtime URL fails in relay mode (the resolver yields a tunnel-virtual/custom-scheme URL the platform rejects — surfaced as "The string did not match the expected pattern"). +2. **Add the path to BOTH allowlists** (they are separate and both required): + - Host tunnel dispatcher: `ALLOWED_WS_PATHS` in `packages/web/server/lib/relay/tunnel-host.js`. + - URL-token auth gate: `isUrlAuthWebSocketPath` in `packages/web/server/lib/ui-auth/ui-auth.js` (otherwise the `oc_url_token` is refused for that path → 401). +3. **Mint the URL token before connecting.** Call `refreshRuntimeUrlAuthToken()` and build the URL through the resolver's `websocket(...)` so `oc_url_token` is appended. SSE/HTTP do not need this; WS does. +4. **Do not touch origin handling.** The server rejects WS upgrades whose `Origin` it does not trust. Over the tunnel the host dials loopback and presents the loopback origin (`http://127.0.0.1:`), which the server trusts as same-origin — this already covers every allowlisted WS path. **Never reintroduce reliance on `window.location.origin`**: in the iOS WKWebView it is `"null"`/empty for the custom scheme, so forwarding it produces a 403. +5. **Test over the relay, not just direct/desktop.** A new WS may be the first WebSocket the mobile client runs through the tunnel (events are SSE-locked on Capacitor). Passing on desktop or a direct connection proves nothing about the relay path. + +## Wire Format And Codec Branch + +- **Two implementations must stay byte-compatible.** The E2EE and framing exist as TS (`packages/ui/src/lib/relay/{crypto,handshake,tunnel-codec}.ts`, normative) and a JS host mirror (`packages/web/server/lib/relay/{e2ee,tunnel-codec}.js`). Any wire-format, frame-type, handshake, or batching change must update **both** and keep `packages/web/server/lib/relay/cross-compat.test.js` green. +- **Frame types live in `protocol.ts`** and must match across `protocol.ts`, `tunnel-codec.ts`, and `tunnel-codec.js`. Adding a frame type without mirroring it corrupts the stream on one side. +- **Frame batching is capability-negotiated** in the handshake with a legacy fallback, so mixed client/host app versions still interoperate. Preserve the negotiation and the single-frame fallback; do not make batching unconditional. +- **The encrypted-frame counter/IV is per-direction and strictly increasing.** One encrypted WS message = one encrypt call = one counter tick. Keep encrypt+send serialized per direction; do not reorder or parallelize it. + +## Runtime Transport Branch + +- 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 Branch + +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. + +## Verification + +- 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. +- Run relay tests per file (`bun test `); the suite has order sensitivity. +- Validate both sides: `packages/ui` `type-check`/`lint`, and `node --check` on changed JS host files. + +Completion requires every applicable branch above: WS path allowlists/auth/origin and real relay exercise; mirrored TS/JS wire changes with cross-compat coverage; preserved direct and relay runtime branches; or reconnect pacing under offline, hidden, permanent-failure, recovery, and abort conditions. diff --git a/.agents/skills/serve-sim/SKILL.md b/.agents/skills/serve-sim/SKILL.md new file mode 100644 index 00000000..36d1ab7b --- /dev/null +++ b/.agents/skills/serve-sim/SKILL.md @@ -0,0 +1,57 @@ +--- +name: serve-sim +description: Use when working with the OpenChamber iOS Simulator app without opening Xcode - boot/install/launch the Capacitor iOS app, start a browser stream, tap/type/gesture/rotate, inspect accessibility, or hand a simulator URL to the user. +--- + +# serve-sim + +Use `serve-sim` to stream and control a booted Apple Simulator from the terminal. It captures the simulator framebuffer, serves a browser preview, and exposes CLI controls for taps, typing, gestures, hardware buttons, rotation, memory warnings, permissions, camera injection, and accessibility inspection. + +## Scripted Workflow + +Run the discrete scripts from the repository root so each step has an observable completion boundary: + +1. Build the simulator app: + ```sh + bun run mobile:build:ios:simulator + ``` + +2. Boot if needed, install, and launch: + ```sh + bun run mobile:sim:run + ``` + +3. Start the detached browser stream: + ```sh + bun run mobile:sim:serve + ``` + Surface the returned JSON `url`; it is the only authoritative stream address. + +4. Stop helpers when finished unless the user asks to keep them running: + ```sh + bun run mobile:sim:kill + ``` + +Completion means the app launched, the returned stream URL was surfaced, requested interactions were verified, and helpers were stopped or intentionally left running. + +## Manual Controls + +- Tap normalized coordinates: `bunx serve-sim tap 0.5 0.5` +- Type focused text: `bunx serve-sim type "hello"` +- Hardware home: `bunx serve-sim button home` +- Rotate: `bunx serve-sim rotate portrait` +- List streams: `bunx serve-sim --list -q` +- Accessibility tree: `curl http://localhost:3100/ax` + +Run direct CLI commands from `packages/mobile` (the binary lives in that package; plain `serve-sim` inside `with-mobile-env.mjs` from elsewhere fails with command not found). + +Coordinates are normalized `0..1`, not pixels. Prefer `tap` for simple taps; do not emulate taps using separate `gesture` begin/end commands because that can register as long press. + +## Preconditions + +- macOS host. +- Xcode installed; use `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer` if `xcode-select` points at CommandLineTools. +- Node 18+. +- At least one simulator can be booted with `xcrun simctl`. + +Use the scripts above instead of opening Xcode for build/install/launch. Consume JSON output rather than parsing human output. If accessibility lookup cannot identify a target, report the missing target instead of guessing coordinates. diff --git a/.agents/skills/settings-ui-patterns/SKILL.md b/.agents/skills/settings-ui-patterns/SKILL.md index c0d012e4..58a894f7 100644 --- a/.agents/skills/settings-ui-patterns/SKILL.md +++ b/.agents/skills/settings-ui-patterns/SKILL.md @@ -1,291 +1,88 @@ --- 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. +Settings are built from the shared primitives in +`packages/ui/src/components/sections/shared/SettingsSection.tsx`, +`SettingsPageLayout.tsx`, and `SettingsInfoHint.tsx`. Never hand-roll page +chrome, section headers, field rows, checkbox rows, or info tooltips with raw +divs — use the primitives, and extend them (in the shared file) when a new +shape is genuinely missing. -## Layout and Spacing Patterns +- Flat hierarchy through spacing and typography; no cards, boxed backgrounds, or row chrome. +- Secondary helper text is hidden behind an info icon (`info` prop); the default view stays quiet. +- Controls have one standard size (`h-9` / select `size="settings"`) and capped widths — no full-bleed inputs. +- Layouts respond to the settings pane width via container queries (`@xl:` / `@3xl:`), never viewport `sm:`/`lg:` breakpoints (the pane is much narrower than the viewport inside the dialog). +- Checkbox/radio state comes before labels; selected states are subtle and never shift layout. -### 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`). +## Load References By Task -### 2. Subsection Grouping -Group related controls with vertical spacing, not mandatory cards. +| Task | Required reference | +|---|---| +| Page skeleton, sections, hierarchy, nav placement, spacing, columns, responsiveness | `references/layout.md` | +| Field rows, checkboxes, radios, chips, selects, inputs, numeric steppers, info hints | `references/controls.md` | +| Adding/moving controls, pages, availability, anchors, or search entries | `references/search.md` | -- 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. +Load each reference whose task branch applies; reference loading is complete when layout, control, and search implications are each classified. -### 3. Header-to-Content Hierarchy (critical) -When removing cards/background wrappers, spacing must be rebalanced so header ownership stays clear. +## Quick Primitive Selection -- 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. +| Need | Shared primitive | +|---|---| +| Page wrapper (title, description, save status, scrolling, `@container`) | `SettingsPageLayout` | +| Titled block with divider | `SettingsSection` (`divider={false}` for the first one) | +| Label left / control right | `SettingsFieldRow` | +| Label above control (two-column cells, wide controls) | `SettingsStackedField` | +| Boolean | `SettingsCheckboxRow` | +| Mutually exclusive list | `SettingsRadioGroup` + `SettingsRadioOption` | +| Short segmented options | `SettingsChipGroup` | +| Sub-cluster with a quiet L3 title inside a section | `SettingsControlGroup` | +| Two-column area on wide panes | `SettingsTwoColumn` | +| Helper text on demand (hover + tap) | `info` prop or `SettingsInfoHint` | -### 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. +Do not introduce raw ``-based info icons, direct Remixicon components, hardcoded user-facing strings, or one-off color/button systems. New icons: reference a Remix icon name in code, then run `bun run icons:generate` to add it to the sprite. -- 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). +## Description Policy (info hints) -```tsx -
-
...
-
...
-
-``` +- Explanatory prose (what a feature does, when it applies) goes behind the info icon via the `info` prop — never as always-visible `description`. +- Stays visible: security/data-loss warnings, destructive consequences, required syntax/placeholder lists the user reads while typing, dynamic status, empty states, validation errors, active-flow wizard instructions. +- Mixed text: keep the warning sentence visible, move the explanation to `info`. -## Structural Patterns +## Save Feedback -### 1. Segmented Option Buttons (compact) -Use for short option sets where button-style segmented choice reads best (e.g. Default Tool Output). +`SettingsPageLayout showSaveStatus` renders the shared quiet indicator: success is silent, "Saving…" appears only past ~500 ms, failures show "Save failed". Anything persisted through `updateDesktopSettings` reports automatically; page-specific APIs must call `reportSettingsSaveState` from `@/lib/persistence`. Never add per-page save badges or success toasts for ordinary setting writes. -```tsx -
- - Collapsed - -
-``` +## Settings Search Contract -### 2. Radio Option Lists (compact rows) -Use for mutually exclusive mode/layout settings (e.g. Diff Layout, Diff View Mode). +Every stable Settings control addition or move must consider search in the same change: -- 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`. +- explicit registry item in `packages/ui/src/lib/settings/search.ts` when searchable; +- matching `data-settings-item` anchor (primitives accept `settingsItem`); +- localized title/description keys; +- availability matching actual render conditions; +- when a control moves to another page, update the item's `page` too. -```tsx -
-
- - Dynamic -
-
-``` +Dynamic entity rows normally are not indexed. Load `references/search.md` for exact rules. -### 3. Checkbox Setting Rows -Use shared `Checkbox` component from `@/components/ui/checkbox` for boolean toggles. +## Completion Criteria -- 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 -
- - Show Dotfiles -
-``` - -### 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 -
- Interface Font Size -
...
-
-``` - -#### 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 -
-
Light Theme ...
-
Dark Theme ...
-
-``` - -### 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 `