diff options
Diffstat (limited to '.dispatch/wire.reference.md')
| -rw-r--r-- | .dispatch/wire.reference.md | 357 |
1 files changed, 250 insertions, 107 deletions
diff --git a/.dispatch/wire.reference.md b/.dispatch/wire.reference.md index 44b0fe7..b430a45 100644 --- a/.dispatch/wire.reference.md +++ b/.dispatch/wire.reference.md @@ -4,98 +4,59 @@ > types WITHOUT following the `file:` dep symlink out of this repo (which hangs on a permission > prompt). Your CODE still imports `@dispatch/wire` normally — this file is for READING only. > -> **Orchestrator:** SNAPSHOT of `[email protected]` (compaction). Regenerate -> whenever `@dispatch/wire` changes. +> **Orchestrator:** SNAPSHOT of `[email protected]` (workspaces + computers + provider-retry + concurrency-`queued` status). Regenerate whenever `@dispatch/wire` changes. > -> **2026-06-22 delta (compaction handoff — package bumped `0.10.0` → `0.11.0`, ADDITIVE):** -> adds `CompactionResult` — the result of a compaction operation (`summary`, `messagesSummarized`, -> `messagesKept`). The summary text is the model's output; the FE doesn't render it directly (it -> becomes the conversation's first system message after compaction). +> **2026-06-27 delta (workspace starring — ADDITIVE to `[email protected]`, NO version bump):** `Workspace` gains a +> required `starred: boolean` (defaults to `false` on creation). A starred workspace's agents receive +> PRIORITY in the concurrency limiter queue — they jump ahead of agents from non-starred workspaces +> (oldest-agent-first within each group). Toggled via dedicated `PUT`/`DELETE /workspaces/:id/star` endpoints +> (no body; both create-on-miss and return the updated `Workspace`). `PUT /workspaces/:id` does NOT accept a +> `starred` field. See `backend-handoff.md`. > -> **2026-06-22 delta (conversation lifecycle handoff — package bumped `0.9.0` → `0.10.0`, ADDITIVE):** -> adds `ConversationStatus` (`"active" | "idle" | "closed"`) — the per-conversation lifecycle -> status. `ConversationMeta` gains a `status` field. `active` = a turn is generating; `idle` = -> exists, not generating; `closed` = dismissed (hidden from the tab bar). Transitions are -> backend-owned: `idle → active` on turn start, `active → idle` on turn settle, `→ closed` on -> `POST /conversations/:id/close`. Pushed to all WS clients via `conversation.statusChanged` -> (see `[email protected]`). +> **2026-06-26 delta (provider concurrency — ADDITIVE to `[email protected]`, NO version bump):** `ConversationStatus` +> widened to `"active" | "queued" | "idle" | "closed"`. `queued` = the turn is in flight but waiting for a +> per-provider concurrency slot (broadcast-only via `conversation.statusChanged`, never persisted); the FE shows +> a loading ring (vs the dots of `active`). See `backend-handoff.md` CR-13. > -> **2026-06-21 delta (conversation.open handoff — package bumped `0.8.0` → `0.9.0`, ADDITIVE):** -> adds `ConversationMeta` — metadata for a conversation (id, title, createdAt, lastActivityAt), -> returned by `GET /conversations` (the list endpoint, see `[email protected]`). +> **2026-06-26 delta (vision handoff — ADDITIVE to `[email protected]`, NO version bump):** adds a new +> `ImageChunk` variant to the `Chunk` union (`{ type: "image", url, mimeType? }` — `url` is a base64 data +> URL or an `http(s)://` URL) and a transport-facing `ImageInput` (`{ url, mimeType? }`, what a client +> sends on `ChatRequest.images`; the orchestrator converts each into an `ImageChunk` on the persisted user +> message). Vision-capable models receive image chunks natively; non-vision models never see them directly +> — the orchestrator's vision handoff transcribes each to a text description (persisted as a separate +> `text` chunk in the SAME user message). See `backend-handoff.md` §2j. > -> **2026-06-21 delta (message-queue + steering handoff — package bumped `0.7.0` → `0.8.0`, ADDITIVE):** -> adds the per-conversation **message queue** + **steering** feature. While a turn is GENERATING, -> a client enqueues a user message (via the `chat.queue` WS op or `POST /conversations/:id/queue`, -> see `[email protected]`); it is delivered mid-turn as **steering** — injected at the next -> tool-result boundary so the model sees it alongside the tool results and can adjust course. If the -> turn ends with a non-empty queue (no tool call fired), the queue is carried into a NEW turn as its -> opening prompt (no `steering` event — the new turn's `user-message` covers it). +> **2026-06-26 update (image storage — NO type change, behavior only):** `ImageChunk.url` for PERSISTED +> chunks is now a compact relative HTTP path (`/images/<conversationId>/<uuid>.png`) served by the backend's +> new `GET /images/:conversationId/:imageId` endpoint (raw bytes + correct Content-Type), NOT a base64 data +> URL — images are stored on disk under tmp, not in the SQLite conversation store (keeps payloads small). +> `ImageInput.url` (what a client SENDS on `ChatRequest.images`) is UNCHANGED — still a data URL or +> `http(s)://` URL; the backend saves it to tmp and returns the compact path in the persisted chunk. A client +> resolves a relative `url` against its API base (`resolveImageUrl`); a data URL (the optimistic echo) or an +> absolute URL passes through unchanged. See `backend-handoff.md` §2j. > -> Adds: -> - **`QueuedMessage`** (`{ id, text, queuedAt }`) — a message held in the queue (stable id for UI -> keying + dedup). -> - **`QueuePayload`** (`{ messages: QueuedMessage[] }`) — the payload of the message-queue -> extension's per-conversation `custom` surface field (`rendererId: "message-queue"`). Carried on -> the SURFACE channel (NOT the chat stream) — the queue is control/state. Empty `messages` = empty -> queue. See `transport-contract.reference.md` for the surface + the enqueue op. -> - **`TurnSteeringEvent`** (`{ type: "steering"; conversationId; turnId; text }`) — a NEW -> `AgentEvent` union member, emitted on the chat stream when the kernel drains a non-empty queue -> at a tool-result boundary. Render `text` as a USER bubble in the transcript (positioned after -> the tool-result it followed); the queue surface separately clears on drain. One event per drain; -> `text` is the combined text of all drained messages. Late-join safe (buffered into the in-flight -> turn's event buffer, mirroring `user-message`). Carry-to-new-turn does NOT emit `steering`. -> ADDITIVE to the union — if you have an exhaustive `AgentEvent` switch, add a `steering` case. +> **2026-06-23 delta (workspaces handoff — package bumped `0.11.0` → `0.12.0`, ADDITIVE):** adds +> `Workspace` + `WorkspaceEntry` (a list entry with a conversation count) and a required +> `workspaceId: string` on `ConversationMeta` (`"default"` for legacy/unspecified conversations). A +> workspace is a URL-driven grouping of conversations that owns a default cwd; conversations that +> haven't set their own cwd inherit `workspace.defaultCwd`. See `backend-handoff-workspaces-reply.md`. > -> **2026-06-12 delta (reasoning-effort handoff — package bumped `0.6.1` → `0.7.0`, ADDITIVE):** -> adds the **`ReasoningEffort`** type — the per-request thinking-depth ladder -> `"low" | "medium" | "high" | "xhigh" | "max"`. Provider-agnostic; the Anthropic provider maps -> levels to extended-thinking token budgets (low 4096 · medium 10240 · high 16384 · xhigh 32768 · -> max 65536); providers without a thinking knob ignore it. Resolution is SERVER-owned (do not -> re-implement): per-turn `ChatRequest.reasoningEffort` override → persisted per-conversation value -> (`GET`/`PUT /conversations/:id/reasoning-effort`, see `[email protected]`) → default -> `"high"`. Higher levels mean longer runs of `reasoning-delta` events before the first text delta. -> See the `ReasoningEffort` definition below. +> **2026-06-25 delta (SSH handoff #1 — ADDITIVE to `[email protected]`, NO version bump):** adds a REQUIRED +> `defaultComputerId: string | null` on `Workspace` (null = local / no SSH; the computer analog of +> `defaultCwd`) and two new read-only view types: `Computer` (a discovered `~/.ssh/config` `Host` alias) +> and `ComputerEntry extends Computer` (a list entry with a `usageCount`). `alias` IS the `computerId` +> users select (persisted per-conversation/per-workspace like cwd). The full HTTP API surface +> (`GET /computers`, `PUT /conversations/:id/computer`, `PUT /workspaces/:id/default-computer`, +> `GET /computers/:alias/status`, `chat.send computerId`) comes in a LATER handoff — NOT consumed yet. > -> **2026-06-12 delta (CR-5 history windowing — package bumped `0.6.0` → `0.6.1`, DOC-ONLY):** the -> per-conversation `seq` numbering is now a WRITTEN CONTRACTUAL GUARANTEE on `StoredChunk`: -> **1-based, monotonic, gap-free** — a conversation's first chunk is always `seq === 1` and -> numbering never skips. A client holding only a windowed suffix of the log derives "older chunks -> exist server-side" purely from `oldestLoaded.seq > 1` (no `earliestSeq`/`hasOlder` field exists). -> -> **2026-06-12 delta (CR-3 user-message handoff — package bumped `0.5.0` → `0.6.0`, ADDITIVE):** adds a -> new `AgentEvent` union member `TurnInputEvent` (`{ type: "user-message"; conversationId; turnId; text }`) -> that surfaces the turn's USER prompt INTO the outward event stream. Emitted ONCE as the FIRST event of -> every turn (before `turn-start`), so it is buffered + replayed to every subscriber — live AND late-join -> — and rides `chat.delta`/NDJSON like any other event. Fixes CR-3 (a pure watcher couldn't see the prompt -> until seal). The sender still echoes its own prompt optimistically, so consumers DE-DUP against that -> (by text); a pure watcher renders it directly. Persistence/metrics unchanged. See `TurnInputEvent` below. -> -> **2026-06-12 delta (context-size handoff — package bumped `0.4.0` → `0.5.0`):** adds an OPTIONAL -> `contextSize?: number` to BOTH `TurnDoneEvent` (live `done`) and `TurnMetrics` (persisted) — the -> turn's FINAL step `inputTokens + outputTokens` (current context occupancy), NOT the aggregate -> `usage` (which overcounts multi-step turns). The two carriers are equal for the same turn. Current -> value = the LATEST turn's `contextSize`; `undefined` ⇒ render "unknown", never `0`. See the field -> doc-comments on `TurnMetrics`/`TurnDoneEvent` below. -> -> **0.3.0 changes (token + timing metrics):** -> - **Live per-step/per-turn telemetry on the event stream** (transient — NOT persisted): -> `TurnUsageEvent` gained an OPTIONAL `stepId?` (attribute tokens per step). A NEW -> `TurnStepCompleteEvent` (`type: "step-complete"`, REQUIRED `stepId`) carries the per-step -> generation timing `ttftMs?` / `decodeMs?` / `genTotalMs?` (all optional — present only when the -> runtime had a clock; `ttftMs`/`decodeMs` additionally require a first content token). `TurnDoneEvent` -> gained an OPTIONAL `durationMs?` (total turn wall-clock) + OPTIONAL `usage?` (aggregate across -> steps). `TurnToolResultEvent` gained an OPTIONAL `durationMs?` (tool execution time). -> - **Durable, replayable metrics** (persisted, keyed per turn): NEW `StepMetrics` + `TurnMetrics` -> — the persisted counterparts of the live `usage` + `step-complete` + `done` packets. Served by -> `GET /conversations/:id/metrics` (see `transport-contract.reference.md`). Build the SAME -> `TurnMetrics` shape from the live events for the in-flight turn; the durable endpoint supplies it -> for sealed turns. TPS is derived (`usage.outputTokens / (genTotalMs / 1000)`), not on the wire. -> - **0.2.0 (still current — step grouping):** `ToolCallChunk`/`ToolResultChunk` carry an OPTIONAL -> `stepId?: StepId`; `TurnToolCallEvent`/`TurnToolResultEvent` carry a REQUIRED `stepId: StepId`. -> Group batched/parallel tool calls by `stepId` equality. Live: read `event.stepId`. Replay: read -> `storedChunk.chunk.stepId` (NOT the envelope; tolerate absence). `StoredChunk` envelope is -> UNCHANGED (`{ seq, role, chunk }` — carries NO `turnId`). +> **⚠️ CROSS-REPO DIVERGENCE (2026-06-25, BLOCKING FE typecheck):** the backend `feature/ssh-support` +> branch (where the SSH types landed) was cut from `8a74335` and is MISSING the `TurnProviderRetryEvent` / +> `provider-retry` `AgentEvent` addition that is on `dev` (and which the FE already consumes — see §2c of +> `backend-handoff.md`). The mirror below KEEPS `TurnProviderRetryEvent` (it is the FE's expected contract +> and matches `dev`); it is marked where it appears. Until the backend merges `dev` into +> `feature/ssh-support`, the FE pinned to the `feature/ssh-support` wire will NOT typecheck (11 errors, +> all the missing `provider-retry` seam). The SSH `Computer`/`defaultComputerId` types ARE present on +> `feature/ssh-support` and are consumed below. ```ts /** @@ -136,7 +97,8 @@ export type Chunk = | ToolCallChunk | ToolResultChunk | ErrorChunk - | SystemChunk; + | SystemChunk + | ImageChunk; /** A piece of plain text content from the assistant or user. */ export interface TextChunk { @@ -213,6 +175,51 @@ export interface SystemChunk { } /** + * An image attached to a message (e.g. a user-pasted screenshot or pasted + * photo). Carries a `url` that is EITHER a base64 data URL + * (`data:image/png;base64,…`) OR an `http(s)://` URL OR — for PERSISTED chunks + * (history/replay) — a compact relative HTTP path (`/images/<conversationId>/ + * <uuid>.png`) served by the backend's `GET /images/:conversationId/:imageId` + * endpoint (images are stored on disk under tmp, NOT in the conversation store, + * to keep SQLite payloads small). A client resolves a relative path against its + * API base URL; a data URL (the optimistic echo / a pasted image) or an + * absolute URL is rendered as-is. Vision-capable models receive it natively + * (the provider serializes it to its image-content format); non-vision models + * never see it directly — the orchestrator's **vision handoff** transcribes it + * to a text description (via a vision-capable model) and feeds that text + * instead, so a text-only model can still reason about the image's contents. + * + * When a transcription was performed, it is persisted as a separate `text` + * chunk alongside the `image` chunk in the SAME user message, so the + * description is reused on every later turn (no re-transcription) and a + * client renders both the original image and its textual analysis. + */ +export interface ImageChunk { + readonly type: "image"; + /** Image source: a base64 data URL (`data:image/…;base64,…`), an `http(s)://` URL, or a compact relative path (`/images/<conv>/<uuid>.png`) for persisted chunks. */ + readonly url: string; + /** + * Optional MIME type of the image (e.g. `"image/png"`). Inferred from the + * data URL when absent; present so a client can render an icon/label without + * parsing the URL. Optional — callers that only have a URL omit it. + */ + readonly mimeType?: string; +} + +/** + * An image a client attaches to a chat message (`ChatRequest.images`). The + * transport-facing input shape; the orchestrator converts each `ImageInput` + * into an `ImageChunk` on the persisted user message. Carries the same `url` + * semantics as `ImageChunk.url`. + */ +export interface ImageInput { + /** Image source: a base64 data URL (`data:image/…;base64,…`) or an `http(s)://` URL. */ + readonly url: string; + /** Optional MIME type (e.g. `"image/png"`). Optional — inferred from the data URL when absent. */ + readonly mimeType?: string; +} + +/** * A chat message: a role plus an ordered sequence of chunks. Messages are the * unit passed to and from the provider; chunks are the unit persisted and * rendered. @@ -299,8 +306,8 @@ export interface StepMetrics { * Durable per-turn metrics for a completed (sealed) turn — the persisted, * replayable counterpart of the live `done` event's aggregate `usage` + * `durationMs`, plus the per-step breakdown. `usage` is the aggregate across all - * steps; `steps` carries each step's `StepMetrics` in step order. Stored by - * `conversation-store` keyed by `turnId` and served by + * steps; `steps` carries each step's `StepMetrics` in step order. Persisted per + * turn by `conversation-store` (returned in turn-append order) and served by * `GET /conversations/:id/metrics`. (`turnId` is the plain wire string carried * on every `AgentEvent`, the join key to the live stream.) */ @@ -373,6 +380,7 @@ export type AgentEvent = | TurnUsageEvent | TurnStepCompleteEvent | TurnErrorEvent + | TurnProviderRetryEvent // ⚠️ divergent: present on `dev`, MISSING on the pinned `feature/ssh-support` wire (see header + backend-handoff.md §2c) | TurnDoneEvent | TurnSealedEvent | TurnSteeringEvent; @@ -393,13 +401,15 @@ export interface TurnStartEvent { /** * The user prompt that opened this turn, surfaced INTO the turn's outward event - * stream so a WATCHER (subscribed but not the sender) can render the prompt - * mid-turn — the user message is otherwise persisted only at seal. Emitted ONCE - * as the FIRST event of the turn (before `turn-start`); buffered + replayed to - * every subscriber (live + late-join). The sender echoes its own prompt - * optimistically, so DE-DUP against that (by text); a pure watcher renders it - * directly. Carries the raw `text` passed to the provider. (Turn-scoped: it - * carries `turnId`, so a multi-turn transcript attributes each prompt to its turn.) + * stream. The user message is persisted only when the turn seals (atomically with + * the assistant reply), so without this event a client that is merely WATCHING a + * conversation (subscribed but not the sender) has no source for the prompt text + * mid-turn — it would see the streaming reply with no preceding user bubble until + * seal. Emitted once, as the FIRST event of the turn (before `turn-start`), so it + * is buffered and replayed to every subscriber — live and late-join — exactly like + * the rest of the turn. The sender already echoes its own prompt optimistically, so + * a consumer should de-dup against that (e.g. by text); a pure watcher renders it + * directly. Carries the raw prompt `text` (the same text passed to the provider). */ export interface TurnInputEvent { readonly type: "user-message"; @@ -527,6 +537,31 @@ export interface TurnErrorEvent { readonly code?: string; } +/** + * A retryable provider error is being retried with backoff. Emitted once per + * scheduled retry, BEFORE the sleep, so the UI can show "⚠ Server overloaded — + * retrying in 5s…" immediately. TRANSIENT: emitted to the frontend but NOT + * persisted into the model's message history (it never pollutes the prompt). + * + * When the retry budget is exhausted, the existing `error` event is emitted and + * the turn seals — so the final failure is still a persisted error. `attempt` is + * 0-based (the Nth retry about to happen); `delayMs` is the scheduled sleep + * before that retry fires. + */ +export interface TurnProviderRetryEvent { + readonly type: "provider-retry"; + readonly conversationId: string; + readonly turnId: string; + /** 0-based: this is the Nth retry about to happen. */ + readonly attempt: number; + /** ms the client should expect to wait before the retry fires. */ + readonly delayMs: number; + /** The endpoint's error verbatim (e.g. "HTTP 429: {…overloaded_error…}"). */ + readonly message: string; + /** The HTTP code when known (e.g. "429"). */ + readonly code?: string; +} + /** The turn has completed (model finished generating). */ export interface TurnDoneEvent { readonly type: "done"; @@ -545,11 +580,11 @@ export interface TurnDoneEvent { */ readonly usage?: Usage; /** - * **Context size** — tokens the conversation occupies right now: the turn's - * FINAL step `inputTokens + outputTokens` (the prompt sent into the last LLM - * round-trip plus that round-trip's output). This is the "tokens in context" - * figure a client renders as the chat's current context usage, and a client - * treats the LATEST turn's value as the live total. + * **Context size** — the number of tokens the conversation now occupies: this + * (the most recent) turn's FINAL step `inputTokens + outputTokens` (the full + * prompt sent into the last LLM round-trip plus that round-trip's output). This + * is the "tokens in context" figure a client renders as the chat's current + * context usage, and a client treats the LATEST turn's value as the live total. * * Deliberately NOT the aggregate `usage` above: `usage` SUMS each step's * `inputTokens`, which overcounts a multi-step / tool-calling turn because every @@ -598,18 +633,24 @@ export interface TurnSteeringEvent { // ─── Conversation metadata ─────────────────────────────────────────────────── /** - * The per-conversation lifecycle status. `active` = a turn is generating; - * `idle` = exists, not generating; `closed` = dismissed (hidden from the tab - * bar, not deleted). Transitions are backend-owned and pushed via the - * `conversation.statusChanged` WS message (see `transport-contract`). + * The lifecycle status of a conversation, used for tab persistence across + * devices. `active` = an agent is currently generating; `queued` = the turn is + * in flight but waiting for a per-provider concurrency slot (broadcast-only, + * never persisted — CR-13; the tab shows a ring vs the dots of `active`); + * `idle` = exists but not generating; `closed` = user dismissed the tab + * (hidden from the tab bar, not deleted). New conversations start as `idle`; + * transitions to `active` on turn-start (or `queued` when the request blocks on + * a concurrency slot before generation begins), back to `idle` on turn + * done/error, and to `closed` on user close. */ -export type ConversationStatus = "active" | "idle" | "closed"; +export type ConversationStatus = "active" | "queued" | "idle" | "closed"; /** * Metadata for a conversation, returned by `GET /conversations` (the list * endpoint). The title defaults to the first user message (truncated) and can * be set via `PUT /conversations/:id/title`. `createdAt` is set on first write; - * `lastActivityAt` is updated on every append. + * `lastActivityAt` is updated on every append. `status` tracks the tab lifecycle + * for cross-device persistence. */ export interface ConversationMeta { readonly id: string; @@ -617,7 +658,17 @@ export interface ConversationMeta { readonly lastActivityAt: number; readonly title: string; readonly status: ConversationStatus; - /** Points to the archive conversation with full pre-compaction history. */ + /** + * The workspace this conversation belongs to. Always present; reads as + * `"default"` for legacy conversations that were never explicitly assigned. + * Conversations created with no `workspaceId` default to `"default"`. + */ + readonly workspaceId: string; + /** + * Set on a compacted conversation: points to the archive conversation ID + * that holds the full pre-compaction history. Absent on conversations + * that have never been compacted. + */ readonly compactedFrom?: string; } @@ -627,6 +678,8 @@ export interface ConversationMeta { * Result of a compaction operation. `summary` is the text the model produced; * `messagesKept` is how many recent messages were retained after the summary; * `messagesSummarized` is how many old messages were replaced by the summary. + * `newConversationId` is the ID of the new conversation that holds the full + * pre-compaction history (non-destructive — the original history is preserved). */ export interface CompactionResult { readonly summary: string; @@ -634,4 +687,94 @@ export interface CompactionResult { readonly messagesSummarized: number; readonly messagesKept: number; } + +// ─── Workspaces ────────────────────────────────────────────────────────────── + +/** + * A named, URL-driven grouping of conversations that owns a default cwd. + * Every conversation belongs to exactly one workspace; conversations that + * haven't set their own per-conversation cwd inherit `defaultCwd`. + * + * Workspaces are backend-owned (so cross-device just works): the workspace + * entity and each conversation's `workspaceId` live server-side. The + * `"default"` workspace is always present and non-deletable; conversations + * created with no `workspaceId` are assigned to `"default"`. + */ +export interface Workspace { + /** The URL slug (immutable). Lowercase `[a-z0-9-]`, 1–40 chars. */ + readonly id: string; + /** Display title (editable). Defaults to `id` on creation. */ + readonly title: string; + /** The workspace's default cwd, or `null` (fall through to server default). */ + readonly defaultCwd: string | null; + /** + * The workspace's default computer — an SSH config `Host` alias that + * conversations in this workspace inherit when they set no `computerId` of + * their own. `null` means local (no SSH; today's behavior). The computer + * analog of `defaultCwd`. Resolved per-conversation by `getEffectiveComputer` + * (per-conv `computerId` → this → `null`/local). + */ + readonly defaultComputerId: string | null; + /** + * Whether the workspace is starred by the user. Starred workspaces receive + * PRIORITY in the concurrency limiter queue — their agents jump ahead + * of agents from non-starred workspaces (oldest-agent-first within each group). + * Defaults to `false` on creation. + */ + readonly starred: boolean; + /** Epoch-ms when the workspace was first created. */ + readonly createdAt: number; + /** Epoch-ms of the most recent conversation activity in this workspace. */ + readonly lastActivityAt: number; +} + +/** + * A workspace entry in the list response (`GET /workspaces`) — a `Workspace` + * plus a conversation count. + */ +export interface WorkspaceEntry extends Workspace { + /** Number of conversations assigned to this workspace. */ + readonly conversationCount: number; +} + +// ─── Computers ─────────────────────────────────────────────────────────────── + +/** + * A read-only view of a remote computer discovered from the system's + * `~/.ssh/config` — a "computer" is a `Host` alias, NOT an editable entity + * (there is no Computer CRUD store). To add a computer, the user adds a `Host` + * block to `~/.ssh/config`; Dispatch discovers it on the next `listComputers()` + * read. Every field below is resolved from the config (first-match-wins for + * `HostName`/`User`/`Port`/`IdentityFile`). + * + * `alias` is the `computerId` users select — the string persisted per + * conversation and per workspace (the computer analog of `cwd`). `knownHost` + * drives the frontend "known/new" indicator and is read-only. + */ +export interface Computer { + /** The SSH config `Host` alias — also the `computerId` users select. */ + readonly alias: string; + /** Resolved `HostName`/IP from the config (falls back to the alias itself). */ + readonly hostName: string; + /** Resolved port (config `Port`, default 22). */ + readonly port: number; + /** Resolved user (config `User`, default the current user). */ + readonly user: string; + /** Resolved `IdentityFile` path (from the config, or `null` = default `~/.ssh/id_*`). */ + readonly identityFile: string | null; + /** + * Whether the host's key is already in `~/.ssh/known_hosts` (i.e. previously + * connected). Drives the frontend "known/new" indicator. Read-only. + */ + readonly knownHost: boolean; +} + +/** + * A computer entry in the list response (`GET /computers`) — a `Computer` plus + * a usage count. Parallel to `WorkspaceEntry`. + */ +export interface ComputerEntry extends Computer { + /** Number of conversations/workspaces whose `computerId` resolves to this alias. */ + readonly usageCount: number; +} ``` |
