chore: remove temp design documents
This commit is contained in:
@@ -1,150 +0,0 @@
|
||||
# Settings Item Search Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Add Settings search that finds individual settings items, not only top-level pages.
|
||||
|
||||
The search should behave like this:
|
||||
|
||||
- User types a query in the Settings navigation area.
|
||||
- Results show matching concrete settings, grouped or labeled by their Settings page.
|
||||
- Each result shows the item title and, when available, its description.
|
||||
- Clicking a result opens the correct Settings page.
|
||||
- After the page renders, the matching row/card/section scrolls into view.
|
||||
- The matched item gets a short visual highlight so the user can see where they landed.
|
||||
|
||||
## Current Architecture Notes
|
||||
|
||||
- Settings shell lives in `packages/ui/src/components/views/SettingsView.tsx`.
|
||||
- Page metadata and slugs live in `packages/ui/src/lib/settings/metadata.ts`.
|
||||
- Settings localization lives in `packages/ui/src/lib/i18n/messages/*.settings.ts`.
|
||||
- Settings UI text is read through `useI18n()` and `t(key)`.
|
||||
- Standard page wrappers live in `packages/ui/src/components/sections/shared/`.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
Use an explicit searchable item registry instead of scraping React or the DOM.
|
||||
|
||||
Each searchable item should contain:
|
||||
|
||||
- `id`: stable item id, for example `appearance.language`.
|
||||
- `page`: target `SettingsPageSlug`, for example `appearance`.
|
||||
- `titleKey`: localized title key.
|
||||
- `descriptionKey`: optional localized description key.
|
||||
- `keywords`: optional non-visible search helpers.
|
||||
- `isAvailable`: optional runtime/mobile guard for item-level availability.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 'appearance.language',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.appearance.language.label',
|
||||
descriptionKey: 'settings.appearance.language.description',
|
||||
keywords: ['locale', 'translation', 'ui language'],
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create `packages/ui/src/lib/settings/search.ts`.
|
||||
- Export `SETTINGS_SEARCH_ITEMS`.
|
||||
- Export a helper to build localized search results from `t()`.
|
||||
- Filter by page availability and `visiblePageSlugs`.
|
||||
|
||||
2. Add search UI to `SettingsView.tsx`.
|
||||
- Search input should live in the left Settings navigation area on desktop.
|
||||
- On mobile, keep behavior simple: show results in the nav stage and open target page on select.
|
||||
- When query is empty, keep the existing navigation list.
|
||||
- When query has text, replace the normal nav list with concrete search results.
|
||||
|
||||
3. Add click behavior for a search result.
|
||||
- Set `settingsPage` to the result page.
|
||||
- Store pending target item id in component state/ref.
|
||||
- After content renders, find `[data-settings-item="<id>"]`.
|
||||
- Scroll it into view.
|
||||
- Add a temporary highlight using a data attribute or CSS class.
|
||||
|
||||
4. Add a tiny shared anchor/highlight pattern.
|
||||
- Prefer adding `data-settings-item="..."` to existing row/card containers.
|
||||
- Avoid wrappers that change layout.
|
||||
- Keep highlight styling generic, for example a short ring/background transition.
|
||||
|
||||
5. Add initial searchable coverage.
|
||||
- Start with high-value pages that already use many localized strings:
|
||||
- `appearance`
|
||||
- `chat`
|
||||
- `sessions`
|
||||
- `notifications`
|
||||
- `git`
|
||||
- `providers`
|
||||
- `agents`
|
||||
- Add more pages incrementally.
|
||||
|
||||
6. Validation.
|
||||
- Run `bun run type-check`.
|
||||
- Run `bun run lint`.
|
||||
- Manually verify search result navigation for at least one single page and one split page.
|
||||
|
||||
## Current Implementation Status
|
||||
|
||||
Done:
|
||||
|
||||
- `packages/ui/src/lib/settings/search.ts` exists and exports the explicit registry plus localized result builder.
|
||||
- Search input is wired into `SettingsView.tsx`.
|
||||
- Results are grouped by page header.
|
||||
- ArrowUp, ArrowDown, Enter, and Escape work while the search input is focused.
|
||||
- Result click opens the target page and scrolls to `[data-settings-item="..."]`.
|
||||
- Matching target gets a temporary highlight via `data-settings-search-highlight`.
|
||||
- Search respects page availability, `visiblePageSlugs`, and item-level platform/runtime/mobile guards.
|
||||
- Initial anchors exist for `appearance`, `chat`, `sessions`, `notifications`, `git`, and `usage`.
|
||||
|
||||
Covered pages/items so far:
|
||||
|
||||
- `appearance`: themes, localization, PWA/mobile-only controls, layout controls, navigation controls, usage reports.
|
||||
- `chat`: render mode, transport, reasoning, layout/message toggles, mobile status bar, dotfiles, queue/draft/spellcheck.
|
||||
- `sessions`: defaults, retention, desktop network controls, OpenCode CLI controls.
|
||||
- `notifications`: delivery, events, background push.
|
||||
- `git`: GitHub account, identities, changes view, Gitmoji, gitignored files.
|
||||
- `usage`: header menu visibility, model quotas section.
|
||||
- `agents`: create action plus static editor fields for name, mode, model, temperature, Top P, system prompt, and permissions.
|
||||
- `commands`: create action plus static editor fields for name, agent, model, and template.
|
||||
- `mcp`: create action plus static editor sections for server, command/URL, environment variables, and advanced remote options.
|
||||
- `plugins`: add action plus static editor fields for spec, options JSON, and file content.
|
||||
- `snippets`: create action plus snippet content editor.
|
||||
- `providers`: connect action plus auth, connection details, and models sections.
|
||||
- `skills.installed`: create action plus basic information, instructions, and supporting files sections.
|
||||
- `behavior`: global AGENTS.md and response style sections.
|
||||
- `projects`: static project metadata fields and worktree section, excluding individual projects.
|
||||
- `skills.catalog`: source repository, catalog search, and add catalog action, excluding individual catalog skills/sources.
|
||||
- `magic-prompts`: visible prompt, instructions, and reset-all action, excluding individual prompt result generation beyond the selected editor page.
|
||||
- `shortcuts`: keyboard shortcut editor section.
|
||||
- `voice`: voice setup, speech recognition, and playback sections.
|
||||
- `tunnel`: provider, tunnel type, TTLs, managed remote/local configuration, and start/connect link sections.
|
||||
- `remote-instances`: client auth/pairing and desktop direct-host sections; SSH instance dialog fields stay out of search because they require selected-instance state.
|
||||
|
||||
Still pending:
|
||||
|
||||
- Add state-aware filtering for settings that are hidden based on current settings values, not just platform. Examples: `chat.activity-default-mode`, `chat.collapsible-reasoning`.
|
||||
- Add focused tests for `buildSettingsSearchResults`, especially runtime/mobile filtering.
|
||||
|
||||
Out of scope by decision:
|
||||
|
||||
- Do not generate search results from dynamic store entities such as individual agents, commands, MCP servers, snippets, plugins, skills, providers, or projects.
|
||||
- For split pages, search should cover predictable static create actions, editor fields, and sections only.
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- Do not rely on localized key naming alone for navigation. The registry is the source of truth.
|
||||
- Do not parse JSX or scrape the DOM to discover settings automatically.
|
||||
- Search should use current locale strings, with English fallback already handled by i18n.
|
||||
- Do not introduce broad Zustand state for transient search query/highlight state. Keep it local to `SettingsView` unless another surface needs it.
|
||||
- Keep page behavior unchanged when the query is empty.
|
||||
- If a page is unavailable in the current runtime, its search items must not appear.
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- Add fuzzy ranking instead of simple substring matching.
|
||||
- Support deep-linking to settings items from URLs or app commands.
|
||||
- Add complete registry coverage for all Settings pages.
|
||||
@@ -1,326 +0,0 @@
|
||||
# Preview — Remote-host relay (design)
|
||||
|
||||
Status: design only, no implementation.
|
||||
Owner: TBD.
|
||||
Audience: contributors planning the next phase of the embedded preview feature.
|
||||
|
||||
## Problem
|
||||
|
||||
The current preview implementation (`packages/web/server/lib/preview/proxy-runtime.js`,
|
||||
`packages/ui/src/components/layout/ContextPanel.tsx`) terminates inside the
|
||||
OpenChamber server process and forwards requests to a **loopback** target
|
||||
(`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`). It works for these topologies:
|
||||
|
||||
| Topology | Works today? |
|
||||
| ------------------------------------------------------------------------ | ------------ |
|
||||
| Web UI in browser, OpenChamber server on same host as dev server | yes |
|
||||
| Electron desktop, dev server on same host | yes |
|
||||
| VS Code extension, dev server on same host | yes |
|
||||
| Mobile/tablet hitting OpenChamber over LAN, dev server on host | yes |
|
||||
| **Remote OpenChamber** (cloud / shared / tunneled), dev server on user's local machine | **no** |
|
||||
|
||||
The blocked case is real: a user runs `openchamber serve` on a remote box (or a
|
||||
hosted OpenChamber instance) but their dev server (`vite`, `next dev`, etc.)
|
||||
runs on their laptop. The proxy correctly refuses to talk to non-loopback
|
||||
targets — that is a deliberate SSRF gate, not a bug. We need a separate path
|
||||
that tunnels traffic from the remote OpenChamber back to the user's laptop
|
||||
without weakening that gate.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Replacing the existing loopback proxy. The local-loopback path is the common
|
||||
case and stays unchanged.
|
||||
- Acting as a generic public ingress for arbitrary local services. We only
|
||||
expose dev servers selected through the preview UI, scoped to the active
|
||||
user's session.
|
||||
- Providing a hosted relay service. The relay is something the user runs;
|
||||
OpenChamber provides the agent + the server endpoints.
|
||||
|
||||
## Constraints (carried forward from the loopback proxy)
|
||||
|
||||
- Same-origin in the browser. The iframe must load from the OpenChamber
|
||||
origin so HTTPS, cookies, and CSP behave predictably.
|
||||
- Per-target cookie auth. A target id must not be guessable, and the cookie
|
||||
must be HttpOnly + scoped to that target's path.
|
||||
- WebSocket upgrade support (HMR is a hard requirement; without it the
|
||||
feature is uninteresting).
|
||||
- Strip frame-busting headers on the response.
|
||||
- Strip OpenChamber credentials before forwarding to the dev server.
|
||||
- Survive partial failure cleanly: if the agent disconnects, the iframe
|
||||
should land on the existing "dev server is not responding" overlay, not a
|
||||
zombie hang.
|
||||
|
||||
## Architecture
|
||||
|
||||
Three components, in order of where they run.
|
||||
|
||||
### 1. Local agent (runs on the user's laptop)
|
||||
|
||||
A small process the user starts on the same machine as the dev server. Two
|
||||
shipping options:
|
||||
|
||||
- A subcommand of the existing CLI: `openchamber preview-agent`.
|
||||
- A standalone single-binary build for users who do not have the full UI
|
||||
installed locally.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Open exactly one outbound, authenticated WebSocket to the remote
|
||||
OpenChamber server (`wss://<host>/api/preview/agent`). Outbound-only — no
|
||||
inbound port on the user's machine, so it works behind NAT, VPN,
|
||||
corporate firewall, etc.
|
||||
- Authenticate with a short-lived enrollment token issued by the remote
|
||||
OpenChamber server (see "Pairing flow").
|
||||
- Advertise the set of dev servers the user has authorised. Scope is
|
||||
loopback-only on the agent side (same allowlist as the existing proxy:
|
||||
`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`). The agent never proxies to
|
||||
arbitrary hosts on the user's network.
|
||||
- Multiplex per-request streams over the single control WebSocket
|
||||
(frame protocol below). Each browser request becomes one logical stream.
|
||||
- Forward HTTP and upgraded WebSocket connections to the local dev server.
|
||||
- Send authoritative `agent-disconnected` notifications so the server can
|
||||
evict targets immediately rather than waiting for TTL.
|
||||
|
||||
Deliberately out of scope for the agent:
|
||||
|
||||
- TLS termination. The agent only talks to loopback over plain HTTP; the
|
||||
outbound link to OpenChamber is TLS via the server's existing cert.
|
||||
- Anything that mutates the user's filesystem.
|
||||
- Acting as a general SOCKS/HTTP proxy. It is dev-server-scoped.
|
||||
|
||||
### 2. Remote OpenChamber server (extends `proxy-runtime.js`)
|
||||
|
||||
Adds two new surfaces alongside the existing loopback proxy:
|
||||
|
||||
- `GET /api/preview/agent` (WebSocket): the single control channel an agent
|
||||
connects to after enrollment. Authenticated by the enrollment token + the
|
||||
user's UI session.
|
||||
- `POST /api/preview/targets/remote`: same shape as the existing
|
||||
`POST /api/preview/targets`, but the URL is interpreted **relative to a
|
||||
connected agent**. The body becomes
|
||||
`{ agentId, url, ttlMs? }` (or the existing endpoint accepts an optional
|
||||
`agentId` and dispatches to the right path). The response keeps the same
|
||||
contract: `{ id, proxyBasePath, expiresAt }`. The browser does not learn
|
||||
it is talking to a remote agent — that is a server-side detail.
|
||||
|
||||
The existing `/api/preview/proxy/:id/*` route is reused unchanged from the
|
||||
browser's perspective. Internally it now dispatches based on the registered
|
||||
target type:
|
||||
|
||||
- `kind: 'loopback'` (existing) → `http-proxy-middleware` to a local origin.
|
||||
- `kind: 'agent'` (new) → encode the request into a frame, push it onto the
|
||||
matching agent's WebSocket, await the response frames, stream them back
|
||||
to the browser.
|
||||
|
||||
This dispatch boundary is the only invasive change to the existing runtime.
|
||||
The factory stays `createPreviewProxyRuntime`; the agent registry, frame
|
||||
codec, and response streaming live in a sibling module
|
||||
(`packages/web/server/lib/preview/agent-runtime.js`) so the loopback path
|
||||
remains readable and individually testable.
|
||||
|
||||
### 3. Browser (UI layer)
|
||||
|
||||
Almost no change. `PreviewPane` already POSTs to `/api/preview/targets` and
|
||||
loads the iframe at the returned `proxyBasePath`. The remote case adds:
|
||||
|
||||
- A small "no agent connected" empty state when the user's profile has no
|
||||
active agent but tries to preview a non-public URL. Gives them the exact
|
||||
command to run and a one-click copy of the enrollment token.
|
||||
- The existing 502 / dev-server-down overlay handles agent disconnects too
|
||||
— the proxy returns 502 if the agent vanishes mid-request.
|
||||
|
||||
## Pairing / enrollment flow
|
||||
|
||||
The agent must prove it is acting on behalf of a specific UI user, and the
|
||||
server must be able to revoke that proof.
|
||||
|
||||
1. User opens Settings → Preview → "Connect a local dev-server agent".
|
||||
2. Server mints a short-lived (5 min) enrollment token bound to the user's
|
||||
UI session id, with a single allowed scope: `preview-agent.connect`. UI
|
||||
shows the command:
|
||||
```
|
||||
openchamber preview-agent --server https://<host> --token <enrollment-token>
|
||||
```
|
||||
3. Agent posts the enrollment token to `POST /api/preview/agent/enroll` and
|
||||
receives a long-lived `agentId` + `agentSecret`. Stored in the agent's
|
||||
config dir (`$XDG_CONFIG_HOME/openchamber/agent.json` or platform
|
||||
equivalent).
|
||||
4. Agent opens the control WebSocket, authenticating with `agentId` +
|
||||
`agentSecret`. The server verifies and registers the agent against the
|
||||
owning user.
|
||||
5. Agent sends an initial `hello` frame with: agent version, OS, hostname
|
||||
hint (display only — never used for routing), and a list of dev-server
|
||||
URLs the user has explicitly approved on the agent side.
|
||||
|
||||
Revocation:
|
||||
|
||||
- User can revoke an agent from Settings; the server invalidates the
|
||||
`agentSecret` and closes any open WebSocket.
|
||||
- The agent honours `disconnect` frames from the server with a clean
|
||||
shutdown.
|
||||
- Enrollment tokens are single-use and expire after 5 min.
|
||||
|
||||
## Wire protocol (control WebSocket)
|
||||
|
||||
Binary frames, little-endian, one frame = one logical operation. JSON metadata
|
||||
header followed by an opaque body. Designed to be implementable in Node and
|
||||
Bun without exotic deps.
|
||||
|
||||
```
|
||||
+--------+--------+--------+----------------------+----------------------+
|
||||
| u8 ver | u8 op | u32 len| metadata (JSON, len) | body (remaining) |
|
||||
+--------+--------+--------+----------------------+----------------------+
|
||||
```
|
||||
|
||||
Operations:
|
||||
|
||||
| op | name | direction | metadata | body |
|
||||
| ---- | ----------------- | -------------- | ------------------------------------------------------------------- | ----------------------------------- |
|
||||
| 0x01 | hello | agent → server | `{ agentVersion, hostnameHint, allowedTargets: [{origin}] }` | empty |
|
||||
| 0x02 | hello-ack | server → agent | `{ ok, serverVersion }` or `{ ok: false, reason }` | empty |
|
||||
| 0x10 | http-request | server → agent | `{ streamId, method, path, headers, originHint }` | request body bytes |
|
||||
| 0x11 | http-response-head| agent → server | `{ streamId, status, headers }` | empty |
|
||||
| 0x12 | http-response-data| agent → server | `{ streamId, fin: bool }` | response body chunk |
|
||||
| 0x13 | http-error | agent → server | `{ streamId, code, message }` | empty |
|
||||
| 0x20 | ws-open | server → agent | `{ streamId, path, headers, subprotocols }` | empty |
|
||||
| 0x21 | ws-open-ack | agent → server | `{ streamId, ok, status?, subprotocol? }` | empty |
|
||||
| 0x22 | ws-frame | both | `{ streamId, opcode: 'text'|'binary', fin: bool }` | frame payload |
|
||||
| 0x23 | ws-close | both | `{ streamId, code?, reason? }` | empty |
|
||||
| 0x30 | cancel | server → agent | `{ streamId }` | empty |
|
||||
| 0xFE | ping | both | `{ ts }` | empty |
|
||||
| 0xFF | disconnect | server → agent | `{ reason }` | empty |
|
||||
|
||||
Notes:
|
||||
|
||||
- `streamId` is server-assigned for `http-request` and `ws-open`. It scopes
|
||||
ordering and back-pressure per logical request.
|
||||
- Body chunks for HTTP responses are streamed (`fin: false` until the last
|
||||
chunk). The server proxies them to the browser without buffering, so
|
||||
large downloads do not balloon memory on either side.
|
||||
- The `originHint` lets the agent log which approved target a request was
|
||||
routed to; routing itself is determined by the registered target's
|
||||
`agentId` + origin, not by anything the browser sends.
|
||||
- Back-pressure: if the server's downstream socket is paused, it stops
|
||||
reading from the agent's WebSocket. WebSocket flow control then applies
|
||||
end-to-end. We do not implement an additional credit scheme until
|
||||
measurement shows we need one.
|
||||
|
||||
## Security model
|
||||
|
||||
Every guarantee the loopback proxy gives must hold here too. Checked
|
||||
against the same threat model:
|
||||
|
||||
- **Server-side SSRF**: target URLs are still validated against the loopback
|
||||
allowlist — but on the agent, not the server. The server never makes a
|
||||
network call on behalf of a target.
|
||||
- **Cross-user target access**: a target id is owned by the user that
|
||||
registered it. Cookie + path scope unchanged.
|
||||
- **Cross-agent leakage**: a target id is also bound to the specific
|
||||
`agentId` it was registered against. Even if two users somehow share a
|
||||
target id (they cannot — ids are 128-bit random), dispatch only reaches
|
||||
the agent the target was bound to.
|
||||
- **Agent impersonation**: `agentSecret` is per-agent, stored only on the
|
||||
user's machine, transported only over TLS during enrollment + connect.
|
||||
Revocable from Settings.
|
||||
- **Frame-busting headers**: stripped server-side after the agent returns
|
||||
the response, identical to the loopback path. Same code path
|
||||
(`stripFrameBustingHeaders`) — keep it as a single point of truth.
|
||||
- **Dev-server credentials**: the agent strips `cookie`, `authorization`,
|
||||
and `x-openchamber-ui-session` before forwarding to the local dev
|
||||
server, mirroring the existing `proxyReq` handler.
|
||||
- **Public-internet exposure**: no inbound port opens on the user's
|
||||
machine; no egress to non-loopback addresses; the agent process refuses
|
||||
to start with `0.0.0.0` upstream targets that resolve off-loopback.
|
||||
- **Connection pinning**: when the agent's WebSocket disconnects, all of
|
||||
its targets are evicted immediately and any in-flight streams are
|
||||
aborted with 502. The cached entry on the browser side (see
|
||||
`previewProxyTargetCache` in `ContextPanel.tsx`) will then re-register
|
||||
on the next attempt and surface the "no agent connected" empty state.
|
||||
|
||||
Out-of-scope hardening to revisit later:
|
||||
|
||||
- mTLS for the agent ↔ server link (current proposal: TLS + agentSecret;
|
||||
mTLS is a future option for self-hosters who want it).
|
||||
- Audit logging of every proxied request (today the loopback path doesn't
|
||||
do this; the remote path should not become an exception without a UX
|
||||
for inspecting the log).
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Failure | Behaviour |
|
||||
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| Agent never connected | `POST /api/preview/targets/remote` returns 409 with `{ error: 'No agent connected' }`. UI shows empty state. |
|
||||
| Agent disconnected mid-request | Server cancels the stream, returns 502 to the browser, evicts the target. Existing overlay handles it. |
|
||||
| Dev server down on user's laptop | Agent forwards the connection refusal as `http-error`; server emits 502. Existing overlay handles it. |
|
||||
| Slow agent / dev server | Streamed response keeps flowing; no buffering on the server. WebSocket flow control gates the data rate. |
|
||||
| Server restarted | Agent reconnects with stored `agentSecret`. Browser-side cache 404s on next request and re-registers. |
|
||||
| Enrollment token expired | `POST /api/preview/agent/enroll` returns 401 with a clear error; UI prompts to mint a new one. |
|
||||
| Two agents registered for same user | Allowed. The browser-side flow always picks the most recently active agent for a given upstream URL. |
|
||||
|
||||
## Open questions
|
||||
|
||||
These need a decision before implementation, not before the doc lands.
|
||||
|
||||
1. **CLI surface.** Is `openchamber preview-agent` the right verb, or should
|
||||
it live under `openchamber agent preview`? Bias: the former; only one
|
||||
agent today, and we can rename without breaking anything if we ever ship
|
||||
a second.
|
||||
2. **Multi-agent UX.** When a user has two agents online (laptop + desktop)
|
||||
and registers a `localhost:3000` preview, which one wins? Most-recent
|
||||
activity is a sensible default but we should also let the user pin a
|
||||
target to an agent.
|
||||
3. **Browser-side detection of remote vs loopback.** Today the UI has no
|
||||
reason to know. If the empty state needs the user's enrolled agents,
|
||||
that becomes a new `GET /api/preview/agents` endpoint. Acceptable.
|
||||
4. **Storage of `agentSecret`.** Plain file under the agent config dir is
|
||||
simplest. OS keychain integration is nicer but a much larger surface.
|
||||
Bias: file first, keychain later.
|
||||
5. **Frame protocol vs. full HTTP/2 / gRPC.** The custom frame protocol is
|
||||
maybe 200 lines in each runtime. gRPC would handle streaming and back
|
||||
pressure for us but adds a heavy dep. Bias: custom frames; revisit only
|
||||
if we hit a back-pressure or multiplexing bug we cannot solve cleanly.
|
||||
6. **Compression.** The current loopback path forces `accept-encoding:
|
||||
identity` to keep the proxy simple. The remote path probably wants
|
||||
gzip/br between the agent and the server to save bandwidth on slow
|
||||
links — but the dev server may not be configured for it. Decide once we
|
||||
measure.
|
||||
|
||||
## Implementation milestones
|
||||
|
||||
Each milestone is independently shippable and reviewable. Numbers are
|
||||
sequence, not effort.
|
||||
|
||||
1. Agent registry + enrollment endpoints on the server. No proxying yet.
|
||||
Settings UI to mint and revoke enrollment tokens.
|
||||
2. Standalone agent that connects, says hello, and stays connected with
|
||||
ping/pong. No proxying yet. Validates the auth + reconnect story.
|
||||
3. HTTP-only proxying through the agent (`http-request` /
|
||||
`http-response-*`). Browser can register a remote target and load
|
||||
static pages. No HMR yet.
|
||||
4. WebSocket proxying through the agent (`ws-open` / `ws-frame` /
|
||||
`ws-close`). HMR works.
|
||||
5. Failure-mode polish: 502 on disconnect, target eviction, browser-side
|
||||
empty state, "agent connected" indicator in Settings.
|
||||
6. Documentation + tutorial for the remote-host scenario; update
|
||||
`docs/REVERSE_PROXY.md` cross-link.
|
||||
|
||||
## Why not …?
|
||||
|
||||
- **A reverse SSH tunnel from the agent.** Works but requires SSH server
|
||||
on the OpenChamber host, exposes a port, and breaks the same-origin
|
||||
guarantee unless we also reverse-proxy that port through the
|
||||
OpenChamber HTTP server. The control-WebSocket design avoids all of
|
||||
that and keeps a single TLS endpoint.
|
||||
- **Cloudflare/ngrok-style hosted relay.** Would work but turns
|
||||
OpenChamber into a service that depends on a third party (or on us
|
||||
hosting a relay). The agent design lets users run entirely
|
||||
self-hosted.
|
||||
- **WebRTC data channels.** Lower latency in theory, much harder to debug
|
||||
and to reason about behind corporate NATs. Not worth the complexity
|
||||
for HTTP + WS forwarding.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Loopback runtime: `packages/web/server/lib/preview/proxy-runtime.js`
|
||||
- Browser PreviewPane + cache: `packages/ui/src/components/layout/ContextPanel.tsx`
|
||||
- Reverse-proxy deployment notes: `docs/REVERSE_PROXY.md`
|
||||
@@ -1,766 +0,0 @@
|
||||
# Review Flow Implementation Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Build an end-to-end OpenChamber review handoff flow that lets one session implement changes and another normal session review them, with OpenChamber metadata connecting the two sessions invisibly.
|
||||
|
||||
The agents must not see session IDs, metadata, linked-session wording, or routing details. They should only receive natural prompts:
|
||||
|
||||
- Review session initial prompt: a handoff plus an instruction to review it.
|
||||
- Review-to-implementer prompt: another agent reviewed the changes and left feedback; resolve relevant issues.
|
||||
- Implementer-to-review prompt: the agent implementing changes responded to previous feedback; review latest state again.
|
||||
|
||||
The review session is not a child/subsession. It is a normal session in the same directory as the original session.
|
||||
|
||||
## Proposed Command Name
|
||||
|
||||
Do not use `/review`, because that overlaps with OpenCode's default review command semantics.
|
||||
|
||||
Use `/handoff-review` unless we choose a shorter name before implementation.
|
||||
|
||||
Other acceptable names if `/handoff-review` feels too long:
|
||||
|
||||
- `/review-handoff`
|
||||
- `/ask-review`
|
||||
- `/start-review`
|
||||
|
||||
This plan assumes `/handoff-review`.
|
||||
|
||||
## Existing Code Paths To Reuse
|
||||
|
||||
### Slash Command Routing
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `packages/ui/src/sync/session-ui-store.ts`
|
||||
- `packages/ui/src/lib/opencode/client.ts`
|
||||
- `packages/ui/src/lib/magicPrompts.ts`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- `routeMessage(...)` detects messages starting with `/`.
|
||||
- It checks command metadata from `getDirectoryState(requestDirectory)?.command` and `useCommandsStore.getState().commands`.
|
||||
- If the command exists, it uses `optimisticSend(...)` and calls `opencodeClient.sendCommand(...)`.
|
||||
- `opencodeClient.sendCommand(...)` calls SDK `client.session.command(...)` with `sessionID`, `command`, `arguments`, selected model, selected agent, variant, files, and client-generated `messageID`.
|
||||
|
||||
Reuse this for invoking the handoff-generation command. The review flow should not invent a separate command transport.
|
||||
|
||||
### Magic Prompt Registry
|
||||
|
||||
Relevant file:
|
||||
|
||||
- `packages/ui/src/lib/magicPrompts.ts`
|
||||
|
||||
Current examples:
|
||||
|
||||
- `session.summary.visible`
|
||||
- `session.summary.instructions`
|
||||
- `session.review.visible`
|
||||
- `session.review.instructions`
|
||||
- `git.commit.generate.visible`
|
||||
- `git.commit.generate.instructions`
|
||||
|
||||
Add new prompt entries for this flow instead of hardcoding long prompts in components/actions.
|
||||
|
||||
Needed prompt entries:
|
||||
|
||||
- `session.reviewHandoff.visible`
|
||||
- `session.reviewHandoff.instructions`
|
||||
- `session.reviewSession.visible`
|
||||
- `session.reviewSession.instructions` if we need hidden instructions for the review session starter prompt
|
||||
- `session.reviewFeedbackToImplementer.visible`
|
||||
- `session.implementationResponseToReviewer.visible`
|
||||
|
||||
The two cross-session prompts must match the agreed wording closely.
|
||||
|
||||
Review feedback sent back to the original session:
|
||||
|
||||
```md
|
||||
Another agent reviewed your changes and left the feedback below.
|
||||
|
||||
Please review the feedback, resolve the relevant issues, and explain what you changed.
|
||||
|
||||
<review feedback>
|
||||
```
|
||||
|
||||
Implementation response sent back to the review session:
|
||||
|
||||
```md
|
||||
The agent implementing the changes has responded to the previous review feedback.
|
||||
|
||||
Please review the latest state again and report any remaining issues.
|
||||
|
||||
<implementation response / latest assistant message>
|
||||
```
|
||||
|
||||
Initial review session prompt should be similar to:
|
||||
|
||||
```md
|
||||
Please review the changes described in this handoff.
|
||||
|
||||
Focus on correctness, regressions, missing implementation, missing tests, and whether the implementation satisfies the stated intent. Provide concise, actionable feedback for the agent implementing the changes.
|
||||
|
||||
<handoff>
|
||||
```
|
||||
|
||||
The handoff-generation prompt should be based on the summary command, but explicitly include the user's intent and enough implementation context for another agent to review.
|
||||
|
||||
### Handoff Generation Concept
|
||||
|
||||
Relevant existing prompt:
|
||||
|
||||
- `session.summary.instructions` in `packages/ui/src/lib/magicPrompts.ts`
|
||||
|
||||
Current summary instructions already include:
|
||||
|
||||
- completed work
|
||||
- in-progress work
|
||||
- modified files and why
|
||||
- open questions and next steps
|
||||
- user requests, constraints, preferences
|
||||
- technical decisions and rationale
|
||||
|
||||
The new handoff prompt should keep those ideas and make intent explicit:
|
||||
|
||||
- What the user wanted and why
|
||||
- What was implemented
|
||||
- What files changed and why
|
||||
- Important design choices
|
||||
- Known limitations or uncertainty
|
||||
- Validation/test status if known from the session
|
||||
- Anything the reviewer should pay special attention to
|
||||
|
||||
This is an implementation detail, not a risk. The prompt should be specific enough that the review agent can judge intent and implementation without needing private OpenChamber routing context.
|
||||
|
||||
### Active Session Generation Concept
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `packages/ui/src/lib/gitApi.ts`
|
||||
- `packages/ui/src/components/views/GitView.tsx`
|
||||
|
||||
Current commit-message generation uses:
|
||||
|
||||
- `resolveSessionGenerationContext()` to find current session, model, agent, and variant.
|
||||
- `runStructuredGenerationInActiveSession(...)` to send a visible prompt plus hidden synthetic instructions to the active session.
|
||||
- `extractAssistantText(...)` and JSON parsing to get output from the assistant response.
|
||||
|
||||
Important difference for review flow:
|
||||
|
||||
- Commit generation uses `client.session.prompt(...)` and receives the response directly.
|
||||
- Slash commands use `client.session.command(...)`, are effectively fire-and-forget from the UI path, and rely on SSE to populate messages/status.
|
||||
|
||||
For `/handoff-review`, prefer the visible slash command path so the original session contains the generated handoff. Then wait for the resulting assistant output through sync state. Reuse the commit-generation concepts for:
|
||||
|
||||
- selected model/agent/variant resolution
|
||||
- extracting text from assistant message parts
|
||||
- forcing chat scroll if useful
|
||||
- timeout/error handling style
|
||||
|
||||
Create a small reusable helper for waiting for the next completed assistant text after a known user command message ID.
|
||||
|
||||
### Session Create/Update/Delete
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `packages/ui/src/lib/opencode/client.ts`
|
||||
- `packages/ui/src/sync/session-actions.ts`
|
||||
- `packages/ui/src/sync/event-reducer.ts`
|
||||
- `packages/ui/src/stores/useGlobalSessionsStore.ts`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- `opencodeClient.createSession(...)` calls `client.session.create(...)` using the legacy OpenCode session API.
|
||||
- OpenCode supports `metadata` on that API, but OpenChamber currently only forwards `parentID` and `title`.
|
||||
- `opencodeClient.updateSession(...)` currently only forwards `title` and `time.archived`.
|
||||
- OpenCode `metadata` update replaces the whole metadata object. It does not deep-merge.
|
||||
- `deleteSession(...)` and `deleteSessionInDirectory(...)` optimistically remove the session, then call `opencodeClient.deleteSession(...)`, and restore snapshots on failure.
|
||||
- `event-reducer.ts` replaces session objects from `session.created` and `session.updated` events.
|
||||
|
||||
Add metadata support here first. The review flow depends on it.
|
||||
|
||||
### Context Panel Session Tabs
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `packages/ui/src/stores/useUIStore.ts`
|
||||
- `packages/ui/src/components/session/sidebar/SessionNodeItem.tsx`
|
||||
- `packages/ui/src/components/layout/ContextPanel.tsx`
|
||||
- `packages/ui/src/components/chat/message/MessageBody.tsx`
|
||||
- `packages/ui/src/components/chat/message/parts/ToolPart.tsx`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- `useUIStore.openContextPanelTab(directory, tab)` opens or upserts context panel tabs.
|
||||
- Chat tabs use `mode: 'chat'`.
|
||||
- Existing dedupe key convention for session chat tabs is `session:<sessionID>`.
|
||||
- Sidebar already opens a session in the side panel with:
|
||||
|
||||
```ts
|
||||
openContextPanelTab(sessionDirectory, {
|
||||
mode: 'chat',
|
||||
dedupeKey: `session:${session.id}`,
|
||||
label: sessionTitle,
|
||||
})
|
||||
```
|
||||
|
||||
Reuse the same convention for opening the review session in the context panel.
|
||||
|
||||
### Assistant Message Action Buttons
|
||||
|
||||
Relevant file:
|
||||
|
||||
- `packages/ui/src/components/chat/message/MessageBody.tsx`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- `AssistantMessageActionButtons` renders icon-only buttons for copy, save image, and TTS.
|
||||
- The buttons use shared `Button`, `Tooltip`, and `Icon` components.
|
||||
- The shared icon sprite already contains `arrow-left-right`.
|
||||
|
||||
Extend this action area with an optional review-transfer action. Do not import icons directly from Remixicon.
|
||||
|
||||
## Metadata Contract
|
||||
|
||||
Use a namespaced metadata object so we do not collide with user or upstream metadata.
|
||||
|
||||
Original session metadata:
|
||||
|
||||
```ts
|
||||
{
|
||||
openchamber: {
|
||||
reviewSessionID: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Review session metadata:
|
||||
|
||||
```ts
|
||||
{
|
||||
openchamber: {
|
||||
kind: 'review'
|
||||
originalSessionID: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Only one review session per original session.
|
||||
- If original metadata already has `openchamber.reviewSessionID`, reuse that session instead of creating a new review session.
|
||||
- The review session must not have `parentID` set to the original session.
|
||||
- Both sessions must stay in the same directory.
|
||||
- Metadata is internal routing state only. Never include it in prompts.
|
||||
- Metadata updates must preserve unrelated metadata keys.
|
||||
|
||||
Recommended helpers:
|
||||
|
||||
```ts
|
||||
type OpenChamberSessionMetadata = {
|
||||
openchamber?: {
|
||||
kind?: 'review'
|
||||
originalSessionID?: string
|
||||
reviewSessionID?: string
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
```
|
||||
|
||||
Helper functions should live in a focused module, for example:
|
||||
|
||||
- `packages/ui/src/lib/sessionReviewMetadata.ts`
|
||||
|
||||
Functions:
|
||||
|
||||
- `getOpenChamberMetadata(session)`
|
||||
- `isReviewSession(session)`
|
||||
- `getOriginalSessionID(session)`
|
||||
- `getReviewSessionID(session)`
|
||||
- `withReviewSessionLink(metadata, reviewSessionID)`
|
||||
- `withReviewSessionMarker(metadata, originalSessionID)`
|
||||
- `withoutReviewSessionLink(metadata, reviewSessionID)`
|
||||
|
||||
The helpers should clone only the metadata branch they change and preserve all unrelated metadata.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Load Required Skills Before Editing
|
||||
|
||||
When implementing this plan, load these skills before changing code:
|
||||
|
||||
- `ui-api-decoupling` because the work changes SDK data access and session API wrapper behavior.
|
||||
- `theme-system` because the work adds a UI button/icon.
|
||||
- `locale-ui-patterns` because the work adds tooltips, aria labels, toasts, and command text.
|
||||
|
||||
If the final implementation touches Settings magic prompt UI, also load:
|
||||
|
||||
- `settings-ui-patterns`
|
||||
|
||||
### 2. Add Metadata Support To OpenCode Client Wrapper
|
||||
|
||||
File:
|
||||
|
||||
- `packages/ui/src/lib/opencode/client.ts`
|
||||
|
||||
Change `createSession` signature from:
|
||||
|
||||
```ts
|
||||
async createSession(params?: { parentID?: string; title?: string }, directory?: string | null): Promise<Session>
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```ts
|
||||
async createSession(
|
||||
params?: {
|
||||
parentID?: string
|
||||
title?: string
|
||||
metadata?: Record<string, unknown>
|
||||
},
|
||||
directory?: string | null,
|
||||
): Promise<Session>
|
||||
```
|
||||
|
||||
Forward `metadata: params?.metadata` only when it is defined.
|
||||
|
||||
Change `updateSession` patch type from:
|
||||
|
||||
```ts
|
||||
patch: { title?: string; time?: { archived?: number | null } }
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```ts
|
||||
patch: {
|
||||
title?: string
|
||||
metadata?: Record<string, unknown>
|
||||
time?: { archived?: number | null }
|
||||
}
|
||||
```
|
||||
|
||||
Forward `metadata` when defined.
|
||||
|
||||
Important: this method should still replace metadata because the upstream API replaces metadata. Do not hide this with an implicit merge here. Add merge behavior in a separate helper so call sites are explicit.
|
||||
|
||||
### 3. Make Session Types Metadata-Aware In OpenChamber
|
||||
|
||||
OpenCode SDK response types should include metadata in the current v2 SDK legacy `Session`, but verify local imports and generated types used by OpenChamber.
|
||||
|
||||
Files to inspect/update:
|
||||
|
||||
- `packages/ui/src/stores/types/sessionTypes.ts`
|
||||
- Any local `Session` wrapper/normalizer if present
|
||||
- `packages/ui/src/sync/sanitize.ts`
|
||||
- `packages/ui/src/sync/event-reducer.ts`
|
||||
- `packages/ui/src/stores/useGlobalSessionsStore.ts`
|
||||
|
||||
Goal:
|
||||
|
||||
- `session.metadata` should survive list, get, create, update, SSE event replacement, global sessions, and reconnect recovery.
|
||||
- Do not strip `metadata` in sanitation helpers.
|
||||
- Do not create new broad store subscriptions. Use leaf selectors where UI only needs review metadata for one session.
|
||||
|
||||
### 4. Add Explicit Metadata Merge Helpers
|
||||
|
||||
Add a helper, likely in `packages/ui/src/sync/session-actions.ts` or a small module imported by it:
|
||||
|
||||
```ts
|
||||
async function patchSessionMetadata(
|
||||
sessionId: string,
|
||||
directory: string | null | undefined,
|
||||
updater: (metadata: Record<string, unknown>) => Record<string, unknown>,
|
||||
): Promise<Session>
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
1. Read the current session with `opencodeClient.getSession(sessionId)` using the correct directory.
|
||||
2. Read `current.metadata ?? {}`.
|
||||
3. Apply updater.
|
||||
4. Call `opencodeClient.updateSession(sessionId, { metadata: nextMetadata }, directory)`.
|
||||
5. Upsert the returned session into `useGlobalSessionsStore` and the relevant child store if needed.
|
||||
|
||||
Do not swallow fetch/update errors. Callers need to know if metadata linkage failed.
|
||||
|
||||
### 5. Add Review Flow Magic Prompts
|
||||
|
||||
File:
|
||||
|
||||
- `packages/ui/src/lib/magicPrompts.ts`
|
||||
|
||||
Add these prompt records:
|
||||
|
||||
1. `session.reviewHandoff.visible`
|
||||
|
||||
Suggested template:
|
||||
|
||||
```txt
|
||||
Prepare a handoff for another agent to review this work.
|
||||
```
|
||||
|
||||
2. `session.reviewHandoff.instructions`
|
||||
|
||||
Suggested template:
|
||||
|
||||
```txt
|
||||
Produce a review handoff for another agent. Do not compact or mutate session history. Your output is an assistant message that OpenChamber will send to a separate reviewer agent.
|
||||
|
||||
Include:
|
||||
- The user's original intent and any later clarifications that changed the intent
|
||||
- What was implemented and why
|
||||
- Files changed, with brief purpose per file
|
||||
- Important design decisions and tradeoffs
|
||||
- Validation/tests run, if known
|
||||
- Known gaps, uncertainty, or areas the reviewer should inspect closely
|
||||
|
||||
Formatting:
|
||||
- Concise markdown with clear sections
|
||||
- No preamble like "Here is a handoff"
|
||||
- Do not mention OpenChamber metadata, linked sessions, session IDs, or routing
|
||||
- Respond in the same language the user used most in the conversation
|
||||
```
|
||||
|
||||
3. `session.reviewSession.visible`
|
||||
|
||||
Suggested template with `{{handoff}}` placeholder:
|
||||
|
||||
```txt
|
||||
Please review the changes described in this handoff.
|
||||
|
||||
Focus on correctness, regressions, missing implementation, missing tests, and whether the implementation satisfies the stated intent. Provide concise, actionable feedback for the agent implementing the changes.
|
||||
|
||||
{{handoff}}
|
||||
```
|
||||
|
||||
4. `session.reviewFeedbackToImplementer.visible`
|
||||
|
||||
Suggested template with `{{review_feedback}}` placeholder:
|
||||
|
||||
```txt
|
||||
Another agent reviewed your changes and left the feedback below.
|
||||
|
||||
Please review the feedback, resolve the relevant issues, and explain what you changed.
|
||||
|
||||
{{review_feedback}}
|
||||
```
|
||||
|
||||
5. `session.implementationResponseToReviewer.visible`
|
||||
|
||||
Suggested template with `{{implementation_response}}` placeholder:
|
||||
|
||||
```txt
|
||||
The agent implementing the changes has responded to the previous review feedback.
|
||||
|
||||
Please review the latest state again and report any remaining issues.
|
||||
|
||||
{{implementation_response}}
|
||||
```
|
||||
|
||||
### 6. Add Localized UI Strings
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/ui/src/lib/i18n/messages/en.ts`
|
||||
- Other locale files as required by the project pattern
|
||||
|
||||
Add strings for:
|
||||
|
||||
- Command autocomplete description for `/handoff-review`.
|
||||
- Review flow button aria label on review session: “Send review feedback to implementing agent”.
|
||||
- Review flow button aria label on original session: “Send implementation response to reviewing agent”.
|
||||
- Tooltip text for both directions.
|
||||
- Toasts for starting handoff generation, review session creation/reuse, transfer success, transfer failure, missing linked session, missing assistant text.
|
||||
|
||||
Follow locale-ui-patterns. Do not hardcode user-facing text inside components.
|
||||
|
||||
### 7. Register The New OpenChamber Slash Command
|
||||
|
||||
There are two possible implementation paths. Pick the one matching how OpenChamber-owned commands are currently registered.
|
||||
|
||||
Likely locations:
|
||||
|
||||
- `packages/ui/src/lib/magicPrompts.ts`
|
||||
- command autocomplete/store code around `useCommandsStore`
|
||||
- command rendering in `ChatInput` / command autocomplete components
|
||||
|
||||
The command should appear as `/handoff-review` in OpenChamber command autocomplete.
|
||||
|
||||
It should be treated as an OpenChamber flow command, not only a raw OpenCode command, because after the handoff assistant output completes OpenChamber must create/reuse/open/send to the review session.
|
||||
|
||||
Implementation options:
|
||||
|
||||
1. Intercept `/handoff-review` in `routeMessage(...)` before normal OpenCode command lookup.
|
||||
2. Add it to the command store as an OpenChamber-owned command with a handler.
|
||||
|
||||
Prefer the smallest approach consistent with existing command architecture.
|
||||
|
||||
### 8. Implement Handoff Generation And Wait Helper
|
||||
|
||||
Add a helper that starts the handoff command in the original session and resolves with the assistant handoff text.
|
||||
|
||||
Possible module:
|
||||
|
||||
- `packages/ui/src/lib/reviewFlow.ts`
|
||||
|
||||
Inputs:
|
||||
|
||||
```ts
|
||||
{
|
||||
originalSessionID: string
|
||||
directory: string
|
||||
providerID: string
|
||||
modelID: string
|
||||
agent?: string
|
||||
variant?: string
|
||||
}
|
||||
```
|
||||
|
||||
Flow:
|
||||
|
||||
1. Render `session.reviewHandoff.visible` and `session.reviewHandoff.instructions`.
|
||||
2. Send a user message to the original session using `opencodeClient.sendMessage(...)` or the existing command route, depending on final command integration.
|
||||
3. Include the visible handoff request as the visible user text.
|
||||
4. Include hidden instructions as synthetic additional part if using `sendMessage(...)`.
|
||||
5. Capture the generated user message ID.
|
||||
6. Wait until a later assistant message for the same session is complete and has text.
|
||||
7. Extract text with the same idea as `flattenAssistantTextParts(...)` / `extractAssistantText(...)`.
|
||||
8. Timeout with a clear failure if no handoff arrives.
|
||||
|
||||
Waiting rules:
|
||||
|
||||
- Prefer sync store state over polling the server repeatedly.
|
||||
- Use existing `getSyncMessages(sessionID)` and `getSyncParts(sessionID)` from `sync-refs` if they expose enough data.
|
||||
- If a subscription-based wait is not easy, use a bounded interval that reads sync refs and stops on timeout or completion.
|
||||
- Ensure it waits for assistant completion, not just first streaming text.
|
||||
- Avoid broad store subscriptions in React components.
|
||||
|
||||
### 9. Create Or Reuse The Review Session
|
||||
|
||||
After handoff text is available:
|
||||
|
||||
1. Read original session with `opencodeClient.getSession(originalSessionID)`.
|
||||
2. Read `original.metadata.openchamber.reviewSessionID`.
|
||||
3. If it exists:
|
||||
- Try to get that review session in the same directory.
|
||||
- If it exists and has `metadata.openchamber.kind === 'review'`, reuse it.
|
||||
- If it is missing/deleted, clear the stale link and create a new review session.
|
||||
4. If it does not exist, create a new normal session in the same directory with metadata:
|
||||
|
||||
```ts
|
||||
{
|
||||
openchamber: {
|
||||
kind: 'review',
|
||||
originalSessionID,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. Patch original session metadata with:
|
||||
|
||||
```ts
|
||||
{
|
||||
openchamber: {
|
||||
reviewSessionID: reviewSession.id,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Preserve unrelated metadata on both sessions.
|
||||
|
||||
If metadata patching original fails after creating review session, report failure clearly. Do not silently proceed with an unlinked session.
|
||||
|
||||
### 10. Send Initial Prompt To Review Session
|
||||
|
||||
After create/reuse:
|
||||
|
||||
1. Render `session.reviewSession.visible` with `handoff`.
|
||||
2. Send it to the review session as a normal user message.
|
||||
3. Use the same provider/model/agent/variant policy as the current session unless product decision says otherwise.
|
||||
4. Do not mention session IDs or linked sessions.
|
||||
|
||||
Important:
|
||||
|
||||
- If reusing an existing review session, still send the new handoff prompt into it.
|
||||
- Reuse does not mean “do nothing”; it means continue the same review conversation.
|
||||
|
||||
### 11. Open Review Session In Context Panel
|
||||
|
||||
Use:
|
||||
|
||||
```ts
|
||||
useUIStore.getState().openContextPanelTab(directory, {
|
||||
mode: 'chat',
|
||||
dedupeKey: `session:${reviewSession.id}`,
|
||||
label: reviewSession.title,
|
||||
})
|
||||
```
|
||||
|
||||
This should happen after the review session exists and the initial prompt has been sent, or immediately after creation if sending happens asynchronously but errors are still surfaced.
|
||||
|
||||
### 12. Add Cross-Session Transfer Button On Assistant Messages
|
||||
|
||||
File:
|
||||
|
||||
- `packages/ui/src/components/chat/message/MessageBody.tsx`
|
||||
|
||||
Add optional props to `AssistantMessageActionButtons`:
|
||||
|
||||
```ts
|
||||
reviewTransferAction?: {
|
||||
ariaLabel: string
|
||||
tooltip: string
|
||||
disabled?: boolean
|
||||
onClick: () => Promise<void> | void
|
||||
}
|
||||
```
|
||||
|
||||
Render an icon-only button with:
|
||||
|
||||
```tsx
|
||||
<Icon name="arrow-left-right" ... />
|
||||
```
|
||||
|
||||
Visibility rules:
|
||||
|
||||
- Only assistant messages.
|
||||
- Only messages with copyable text.
|
||||
- In a review session: show button to send review feedback to the original session.
|
||||
- In an original session with `metadata.openchamber.reviewSessionID`: show button to send implementation response to the review session.
|
||||
- Do not show in mini-chat if that surface should avoid extra controls; follow current action-button surface rules.
|
||||
|
||||
To avoid button spam:
|
||||
|
||||
- Preferred first implementation: show on assistant messages where normal assistant action buttons already show.
|
||||
- Do not add the button to user messages.
|
||||
- If this feels too noisy in testing, narrow to latest completed assistant message per session as a follow-up, but not required for initial end-to-end implementation.
|
||||
|
||||
### 13. Implement Review Feedback Transfer
|
||||
|
||||
When clicking the button in a review session:
|
||||
|
||||
1. Get current review session metadata.
|
||||
2. Resolve `originalSessionID`.
|
||||
3. Extract the clicked assistant message text.
|
||||
4. Render `session.reviewFeedbackToImplementer.visible` with `review_feedback`.
|
||||
5. Send it as a normal user message into the original session.
|
||||
6. Use original session directory.
|
||||
7. Optionally open/focus the original session or leave context panel as-is. The agreed behavior only requires sending.
|
||||
8. Show success/failure toast.
|
||||
|
||||
Message sent to the agent must be exactly natural-language feedback, not routing data.
|
||||
|
||||
### 14. Implement Implementation Response Transfer
|
||||
|
||||
When clicking the button in the original session:
|
||||
|
||||
1. Get original session metadata.
|
||||
2. Resolve `reviewSessionID`.
|
||||
3. Extract the clicked assistant message text.
|
||||
4. Render `session.implementationResponseToReviewer.visible` with `implementation_response`.
|
||||
5. Send it as a normal user message into the review session.
|
||||
6. Use same directory.
|
||||
7. Open/focus the review session context panel tab, because review continuation happens there.
|
||||
8. Show success/failure toast.
|
||||
|
||||
### 15. Cleanup Metadata When Deleting Review Session
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/ui/src/sync/session-actions.ts`
|
||||
- `packages/ui/src/lib/opencode/client.ts`
|
||||
|
||||
Before deleting a session:
|
||||
|
||||
1. Read the session being deleted.
|
||||
2. If it is a review session and has `originalSessionID`, read the original session.
|
||||
3. If original metadata has `reviewSessionID` equal to the deleted review session ID, patch original metadata to remove it.
|
||||
4. Then delete the review session.
|
||||
|
||||
Failure behavior:
|
||||
|
||||
- If metadata cleanup fails, do not delete silently. Return failure and show the existing delete failure path/toast.
|
||||
- If original session no longer exists, continue deleting the review session; there is nothing to clean.
|
||||
- If delete fails after metadata cleanup succeeded, restore the original metadata link as part of rollback if possible. At minimum, log and surface the delete failure.
|
||||
|
||||
Also apply this to `deleteSessionInDirectory(...)`.
|
||||
|
||||
### 16. Cleanup Stale Link When Reusing Review Session
|
||||
|
||||
If original metadata points to a review session that no longer exists:
|
||||
|
||||
1. Patch original metadata to remove stale `reviewSessionID`.
|
||||
2. Create a fresh review session.
|
||||
3. Patch original metadata with the fresh review session ID.
|
||||
|
||||
This is not a background reconciler. It only happens when the user starts the review flow.
|
||||
|
||||
### 17. Tests
|
||||
|
||||
Add focused tests for helpers and flow boundaries.
|
||||
|
||||
Likely files:
|
||||
|
||||
- New `packages/ui/src/lib/sessionReviewMetadata.test.ts`
|
||||
- Existing `packages/ui/src/sync/session-actions.test.ts`
|
||||
- Component test around `MessageBody` only if nearby test patterns exist
|
||||
|
||||
Test cases:
|
||||
|
||||
1. Metadata helper marks review session without removing unrelated metadata.
|
||||
2. Metadata helper links original session without removing unrelated metadata.
|
||||
3. Metadata helper removes review link only when it matches the deleted review session ID.
|
||||
4. `createSession` forwards metadata to SDK client.
|
||||
5. `updateSession` forwards metadata to SDK client.
|
||||
6. Review flow reuses existing review session ID instead of creating another.
|
||||
7. Review flow clears stale review session ID when referenced review session is missing.
|
||||
8. Delete review session cleans original metadata before deleting.
|
||||
9. Transfer prompt for review-to-implementer contains no session ID / metadata / linked-session wording.
|
||||
10. Transfer prompt for implementer-to-reviewer contains no session ID / metadata / linked-session wording.
|
||||
|
||||
### 18. Validation
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
bun run type-check
|
||||
bun run lint
|
||||
```
|
||||
|
||||
Manual validation checklist:
|
||||
|
||||
1. Start `/handoff-review` in a normal session.
|
||||
2. Confirm a handoff assistant message appears in original session.
|
||||
3. Confirm a normal review session is created in the same directory, not as a child.
|
||||
4. Confirm original metadata has `openchamber.reviewSessionID`.
|
||||
5. Confirm review metadata has `openchamber.kind === 'review'` and `openchamber.originalSessionID`.
|
||||
6. Confirm review session opens in context panel.
|
||||
7. Confirm review session receives the initial handoff review prompt.
|
||||
8. Confirm arrow-left-right appears on review assistant message actions.
|
||||
9. Click it and confirm original session receives the agreed review feedback prompt.
|
||||
10. Confirm arrow-left-right appears on original assistant message actions when original has review metadata.
|
||||
11. Click it and confirm review session receives the agreed implementation response prompt.
|
||||
12. Run `/handoff-review` again on the same original session and confirm it reuses the existing review session.
|
||||
13. Delete the review session and confirm original metadata link is removed.
|
||||
14. Delete original session and confirm no review cleanup crash occurs.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not add a review status state machine.
|
||||
- Do not allow multiple review sessions for one original session in this first implementation.
|
||||
- Do not expose metadata, linked sessions, or session IDs to agents.
|
||||
- Do not use parent/child session relationships for this feature.
|
||||
- Do not change OpenCode core or SDK unless OpenChamber cannot access metadata from the existing SDK types.
|
||||
- Do not make a background metadata reconciler.
|
||||
|
||||
## Main Implementation Risks To Watch While Coding
|
||||
|
||||
These are coding concerns, not product blockers:
|
||||
|
||||
- Metadata replacement must not drop unrelated metadata.
|
||||
- Event/store sanitation must not strip `metadata` from session records.
|
||||
- The handoff wait helper must wait for completed assistant output, not first streaming text.
|
||||
- Cross-session sends must use the correct directory dynamically, not cached closure values.
|
||||
- Message action buttons must not subscribe broad chat rows to global session collections.
|
||||
- Delete cleanup should not delete the review session if cleanup fails in a way that would leave confusing metadata behind.
|
||||
Reference in New Issue
Block a user