@openchamber/sdk 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md CHANGED
@@ -218,6 +218,22 @@ Projects contain `id`, `name`, `directory`. Worktrees contain `directory`, `name
218
218
 
219
219
  `activity` is `unknown`, `idle`, `running`, `retrying`, `waiting-permission`, or `waiting-question` (the agent put a form to the user and is waiting on the answer). `outcome` is the last observed `completed` or `failed` turn, or `null` when unknown or working. Outcomes are in memory for the latest 2,000 observed sessions, reset on runtime switch, and are not reconstructed from persisted history. A later idle event preserves an observed failure until another run starts. `completed` never means the extension's task is Done. Blocking-request contents and approve/reply actions are not exposed.
220
220
 
221
+ ### Running shells
222
+
223
+ These methods need the `shells` capability. They expose the running shell commands in a scope — one session and its subagents, a registered project, or every session — and let the extension read their output and stop them. OpenChamber answers from its local store; no network read happens per request.
224
+
225
+ | Method | Result |
226
+ | --- | --- |
227
+ | `onRunningShells(scope, listener)` | subscription: the running shell commands in scope — one session and its subagents (`{ kind: 'session', sessionId }`), a registered project (`{ kind: 'project', projectId }`), or every session (`{ kind: 'global' }`) — oldest first, with each command's `background` flag, plus the ones that ended since OpenChamber started watching |
228
+ | `readShellOutput(shellId, { cursor?, tailBytes? })` | one page of a running or ended command's output; `tailBytes` is capped at 65536 |
229
+ | `stopShell(shellId)` | sends the agent the stop note first, then removes the command |
230
+
231
+ Await subscription registration to handle refusal, then retain its returned unsubscribe function. Each subscription sends an initial snapshot, then changes, and counts against the same 32-subscription limit as the workspace subscriptions. The snapshot is `{ kind: 'shells', scope, shells, ended }`, where `scope` echoes the subscription's scope; each shell is `{ id, sessionID, command, startedAt, background }`, at most 200, oldest first. `background` is `true` for a job the turn does not wait for, and `false` while the turn that ran it is blocked on it.
232
+
233
+ `ended` holds the commands in scope that OpenChamber saw running and saw end, oldest end first, at most 200 with the newest kept. Each adds `status`, `endedAt`, and `exit` when the command exited with a code. `status` is `exited`, `timeout`, `killed` (a signal), `stopped` (by a user: Stop in the app, `stopShell`, or aborting the turn that waited on it), or `unknown` (the end fell into a reconnect and OpenChamber only saw the command gone). The list lives in memory: it starts empty when the app loads and resets on a runtime switch, so keep anything you show across reloads in extension storage.
234
+
235
+ `readShellOutput` returns `{ output, cursor, skipped }`; omit `cursor` to start `tailBytes` before the end, and `skipped` says earlier output was dropped. OpenCode keeps an ended command's output until it evicts the command (the latest 25 ended commands per directory); a stopped command's output is removed with it. A shell OpenChamber has not seen, a stopped one, and one whose output OpenCode no longer keeps are `NOT_FOUND`; `stopShell` on a shell that is not running is `NOT_FOUND` too. An unapproved `shells` capability is `NOT_GRANTED`.
236
+
221
237
  ### Extension storage
222
238
 
223
239
  `host.storage.get(key, options?)` returns JSON or `undefined` for a missing key. JSON `null` is a stored value. `set(key, value, options?)` and `delete(key, options?)` return `Promise<void>`; `keys(options?)` returns `Promise<string[]>` in sorted order.
@@ -310,6 +326,8 @@ Use `mountPopoverAnchor` from `@openchamber/sdk/ui` for DOM anchors. It measures
310
326
  | `MODEL_FAILED` | The Small Model returned an error |
311
327
  | `UNSUPPORTED` | This host surface cannot do that (for example `openCommit` without a Diff view) |
312
328
  | `SERVICE_FAILED` | Service crashed or never became ready |
329
+ | `REQUEST_FAILED` | `serviceRequest` was sent but got no usable answer; the service may have acted on it |
330
+ | `RESPONSE_TOO_LARGE` | `request` / `serviceRequest` answer over 8 000 000 bytes; the call ran, ask for a smaller page |
313
331
 
314
332
 
315
333
  ### 1.4 Field limits (client clamps before send)
@@ -327,7 +345,7 @@ Use `mountPopoverAnchor` from `@openchamber/sdk/ui` for DOM anchors. It measures
327
345
  | Branch name | 200 |
328
346
  | Request path | 2 000 |
329
347
  | Request body | 64 000 |
330
- | Request response | 256 000 |
348
+ | Request response (bytes) | 8 000 000 |
331
349
  | Request timeout | 20 000 ms |
332
350
  | `resolve` answer (host waits) | 20 000 ms |
333
351
  | Background action loading and execution | 20 000 ms |
@@ -449,7 +467,7 @@ Used by the OpenChamber host and by tools that validate packages. Guests rarely
449
467
  "entry": "panel/index.html"
450
468
  },
451
469
  "attach": "dialog",
452
- "capabilities": ["prompt", "sessions", "files"],
470
+ "capabilities": ["prompt", "sessions", "files", "shells"],
453
471
  "filesystem": ["~/.config/opencode/opencode.json", "/tmp/acme/**"],
454
472
  "actions": [
455
473
  { "id": "create-task", "label": "Create task from message", "icon": "add-circle", "where": "message", "roles": ["assistant"] },
@@ -475,7 +493,7 @@ Used by the OpenChamber host and by tools that validate packages. Guests rarely
475
493
  | `panel.entry` | Optional visible-panel HTML path inside the package. No `..`, absolute path, or URL. Its scripts must be built. Omitting it removes the rail icon and visible views; `background.entry` can still run code. With neither entry, only `tools` plus package identity/version/engines are allowed. `hasGuestPage` means a visible panel, not background execution |
476
494
  | `background.entry` | Optional package-local `.html` path. Loaded on demand for background actions and slash commands, preferred over `panel.entry` for these calls. Adds no rail icon. The file and its built scripts must exist. Malformed declarations are `invalid-background` |
477
495
  | `attach` | `true` / `"panel"` → + menu opens rail; `"dialog"` → host window; omit/`false` → off menus. Object form `{ "mode": "panel" \| "dialog", "entry"?: "panel/attach.html" }`: `entry` (dialog only, same path rules as `panel.entry`, must exist with built scripts) is the page the dialog loads instead of `panel.entry` |
478
- | `capabilities` | Optional list of `prompt`, `sessions`, `files`, `model`. `files` is read **and** write inside the open project; `model` is `generate`. Approved once at install |
496
+ | `capabilities` | Optional list of `prompt`, `sessions`, `files`, `model`, `shells`. `files` is read **and** write inside the open project; `model` is `generate`; `shells` is the running-shell methods. Approved once at install |
479
497
  | `actions` | Optional, 1–8 entries, unique kebab-case `id`, `label` 1–40 chars, optional `icon` with the `panel.icon` rules, `where: "message" \| "session"`, optional `mode: "open" \| "background"`. Message actions may narrow `roles` to `["user"]` / `["assistant"]`, default both. Session actions may request `payload: ["messages"]`, adding the `conversation` capability. Invalid shape is `invalid-actions`. Default `open` mode opens the guest with `ready.item`, using the attach dialog for `attach: "dialog"` and the rail otherwise. `background` calls `onAction` in a temporary hidden frame; see Background actions above |
480
498
  | `commands` | Optional, 1–8 entries, unique `name` matching `/^[a-z][a-z0-9-]{0,23}$/`, optional `description` 1–80 chars (`invalid-commands`). `/name args` in the chat box calls `onResolve` instead of the model and attaches what it returns. A name the composer already has (built-in, OpenCode command, skill) is ignored with a console warning |
481
499
  | `tools` | Optional, 1–16 entries that say how the extension's tool calls look in the chat. `match` is the full tool name OpenCode reports (`mcp.jira.search`, `jira_search`), 1–128 chars of `[A-Za-z0-9_.:-]`, with `*` allowed once at the end as a suffix wildcard (`mcp.jira.*`). Optional `name` (1–40, the header title when `title` is absent or renders empty), `icon` (Remixicon name or package `.svg` path, same rules as `panel.icon`; the SVG is drawn in the text colour at the glyph size), `title` / `subtitle` templates (1–200, `{input.path}` / `{output.path}` / `{metadata.path}` placeholders, a missing path renders empty, values are cut at 200), `output` `"auto"` (default) \| `"text"` \| `"json"` \| `"markdown"` \| `"code"` \| `"table"`, `language` (code only), `columns` (table only, 1–16 dotted paths; rows are the output array or `output.items`). Bad shape is `invalid-tools`. An exact `match` beats a wildcard from any extension; among equals the first extension wins. Only an enabled, fully approved extension's rules apply |
package/DOCUMENTATION.md CHANGED
@@ -30,9 +30,11 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
30
30
  - `contributes.statusSection` is `true` or `{ entry, title?, height?, defaultExpanded?, requiresProject? }` (`StatusSectionContribution`). `true` reuses `panel.entry` and fails as `invalid-status-section` without one; the object form's `entry` is package-local `.html`, `title` 1 to `GUEST_STATUS_SECTION_TITLE_MAX` (60), `height` `GUEST_STATUS_SECTION_HEIGHT_MIN`..`MAX` (24..320). `defaultExpanded` defaults to true and only applies when the user has no saved expansion choice. `requiresProject` defaults to false; when true, the actual Work Status directory controls both presence and body rendering. `resolveStatusSectionEntry` resolves shorthand. The object form is a frame of its own: without `panel.entry` or `background.entry` it permits `capabilities`, `service`, `integration`, and `filesystem`, and refuses `page`, `attach`, `actions`, and `commands` as `invalid-panel`, because those are opened or invoked elsewhere. The server validates the HTML/scripts, exposes `statusEntry`/`statusTitle`/`statusHeight`, and counts `statusEntry` as a runtime entry for serving and storage. The host mounts `PluginPane surface="status"` inside the Work Status panel (`components/chat/work-status/WorkStatusExtensionSection.tsx`, see that folder's `DOCUMENTATION.md`): the same sandbox, guest-scoped token, context, grants, approval, and pause gates as a panel. Guest `resize` (`host.setHeight`, `ResizeRequest { height }`, integer 0..`GUEST_FRAME_HEIGHT_MAX`) is a request answered with an empty result on every surface; only `status` sizes its frame from it, through `clampStatusSectionHeight`, and a page docked to a shared surface sizes its dock from it (`DockedGuestPage` in `ContextPanel`: thickness across the dock edge, at least `GUEST_SURFACE_DOCK_SIZE_MIN`, at most half the panel through CSS). Each frame's `setHeight` reaches only its own pane. A status frame never registers a command resolver, clears a badge, or closes rail tabs. Host-rendered controls exist only after a mounted, expanded status frame calls `setStatusControls`; folding, hiding, or retiring the frame removes them. A project change clears header controls; publish them again from `onDirectory`. Workspace subscriptions remain active.
31
31
  - `openCommit(sha)` posts `open-commit { sha }`. `sha` is 7 to 64 hex characters (`GUEST_COMMIT_SHA`); the client refuses anything else as `HOST_REJECTED` before posting and the host schema drops it. The host (`lib/guests/open-commit.ts`) reads the commit itself with `git.getGitLog(directory, { maxCount: 1, to: sha })` in the frame's current directory, checks the returned hash starts with the requested one (`NOT_FOUND` otherwise), selects it in `useCommitSelectionStore` under the Diff view's key (directory, current branch, git runtime key), and opens the context panel's Diff tab in `commit` scope. No project is `NO_DIRECTORY`. VS Code and mobile answer `UNSUPPORTED` explicitly; they load no guests today, so this is the contract for when they do. No capability is needed: it only shows the user their own history.
32
32
  - Scrollbars in guest documents follow the app's overlay behavior: hidden at rest, shown on hover and for one second after the element last scrolled. `GUEST_SCROLLBAR_CSS` does the hiding and hover; `installGuestScrollbarActivity` (one capturing passive `scroll` listener per document) marks the scrolling element with `data-oc-scrolling`. The server appends both to every served guest HTML as `GUEST_SCROLLBAR_SCRIPT` (the function's own source), so already installed bundles get it without a rebuild; the UI kit installs it too for previews. An authored CSP that blocks inline scripts keeps the hover-only behavior. Forced-colors mode keeps system colors.
33
+ - The UI kit's look follows the running app; its geometry follows the bundle. `src/ui/style.ts` splits the kit CSS into sizes (display, dimensions, spacing, type scale), which ship in each bundle, and `GUEST_UI_LOOK_CSS` (colours, fonts, borders, radii, shadows, hover/pressed/selected/focus), exported from `@openchamber/sdk/ui`. The server appends the look to the same `data-openchamber-guest-styles` element after the authored document, so it follows the kit's own `<style>` in the head and wins at equal specificity: an installed extension takes a restyle without a rebuild, while a size change reaches it only when its author rebuilds. Look selectors keep the specificity of the rules they override in older bundles, and `style.test.ts` fails when a geometry property enters the look. Rules for markup an older bundle lacks are keyed on what newer markup adds (a select option carries `data-mark` with its check mark; an option without it keeps the selection fill). Authors override kit rules with a more specific selector; an equal-specificity override in the head loses to the host's look.
33
34
  - `contributes.fileEditors` is 1 to `GUEST_FILE_EDITORS_MAX` (8) editors, each `{ id, title, match, entry }` (`FileEditorContribution`, `src/file-editor.ts`): unique kebab-case `id`, `title` 1 to `GUEST_FILE_EDITOR_TITLE_MAX` (60), `match` 1 to `GUEST_FILE_EDITOR_PATTERNS_MAX` (16) file-name globs (`isFileEditorPattern`: 1 to 128 characters, no `/`, `\` or NUL, not only wildcards; `matchesFileEditorPattern` compares the name case-insensitively, `*` any run, `?` one character), `entry` package-local `.html`, optional `content` `text` (default) or `binary` (`FileEditorContentKind`); anything else is `invalid-file-editors`. Like the status section, editors are frames of their own: without `panel.entry` or `background.entry` a package may still declare `capabilities`, `service`, `integration`, and `filesystem`, and `page`, `attach`, `actions`, `commands` are `invalid-panel`. The server checks each entry's HTML and built scripts at install, carries `fileEditors` on the catalog row, and counts them as runtime entries for serving and storage (`hasGuestFrame` in `server/lib/guests/catalog.js`). The host owns the file. Wire: host → guest `file-open { path, name, readOnly, encoding: 'text', content } | { ..., encoding: 'binary', bytes }` (a push, sent on `hello` and iframe load, never on other host-state pushes; `connectHost` drops a repeat of the same file), `file-snapshot { id, payload: { purpose: 'save' | 'handoff' } }` answered by guest `file-snapshot-result { id, payload: { snapshot: { content, version } | { bytes, version } } | { error } }` with no `result` back, and `file-saved { version }`; guest → host fire-and-forget `file-change { dirty, edited }`, `file-save`, and `file-unsupported` (no `id`, no answer). Bytes are a `Uint8Array<ArrayBuffer>`, copied by structured clone, so the host schema's `z.instanceof(Uint8Array)` holds in the receiving realm; `sameFileEditorDocument` compares them byte for byte for the repeat-push check. Content on both sides is capped at `GUEST_FILE_EDITOR_CONTENT_MAX` (20 000 000 characters or bytes, `fileEditorPayloadSize`); the guest refuses an oversized snapshot with an error instead of letting the host schema drop it. `onFileOpen` registration installs one capturing `keydown` listener that turns Cmd/Ctrl+S into `file-save`, because key events inside the frame never reach the host. `createFileSaveTracker` is the pure dirty bookkeeping: a written snapshot's version is marked saved, so edits made during the write stay dirty. The host side is `packages/ui/src/lib/guests/file-editor-channel.ts` (one channel per mounted editor; disconnect rejects every outstanding snapshot request), `PluginPane surface="file"`, and `components/views/files/GuestFileEditor.tsx`; the Files view contract is in that folder's `DOCUMENTATION.md`. VS Code and mobile keep the catalog empty, so files there stay with the host's own viewers.
34
35
  - `contributes.page` is `true` or `{ entry, title? }`, requires `panel.entry`, and adds a user-opened full-screen page. `resolvePageEntry` resolves shorthand; the server validates its HTML/scripts and exposes `pageEntry`/`pageTitle`. `PluginPane surface="page"` uses the same sandbox. `useUIStore.openGuestPageId` is transient and mutually exclusive with other main pages. The sidebar menu opens it; `close()` or explicit session navigation closes it. Pages are not context-rail surfaces.
35
36
  - `workspace.ts` owns project/worktree/session snapshots and storage types; `workspace-schemas.ts` validates their wire contracts. The existing `sessions` grant covers lists, subscriptions, opening sessions, and targeted creation. `lib/guests/workspace.ts` projects shared project, topology, session, and live-status stores; reads perform no network or git discovery. A registered observer is shared per runtime/extension/query and stops with its last listener. `PluginPane` limits each frame to 32 subscriptions and clears them on unmount, grant withdrawal, and runtime switch. Initial snapshots and updates have explicit loading/error coverage. `HostClient.onProjects/onWorktrees/onSessions` return promises of unsubscribe functions, so approval failures reach the caller.
37
+ - `shells.ts` owns the running-shell contract: the `shells` declared capability, the snapshot and result types, and `GUEST_SHELL_ID_MAX` (128), `GUEST_SHELLS_MAX` (200), and `GUEST_SHELL_OUTPUT_TAIL_MAX` (65536). Wire: guest `shells-subscribe` `{ subscriptionId, scope }` with scope `{ kind: 'session', sessionId }`, `{ kind: 'project', projectId }`, or `{ kind: 'global' }`, `shells-unsubscribe` `{ subscriptionId }`, `shell-output` `{ shellId, cursor?, tailBytes? }`, and `shell-stop` `{ shellId }`; host push `shells` `{ subscriptionId, snapshot }` where the snapshot carries `kind: 'shells'`, an echo of the requested `scope`, the running `shells`, and `ended` (`GuestEndedShell`: a running shell plus `status` from `GUEST_SHELL_END_STATUSES`, `endedAt`, optional integer `exit`). `lib/guests/shells.ts` projects `useBackgroundShellsStore` (the requested scope: a session and its subagents, a registered project's sessions matched by directory rather than by `projectID`, or every session; running oldest start first, ended oldest end first keeping the newest `GUEST_SHELLS_MAX`) from memory, so a read never hits the network or git; `observeGuestShells` shares one observer per runtime and scope. The store's `ended` list is the app's own observation of `shell.exited` / `shell.deleted` (see `sync/DOCUMENTATION.md`), so it is empty after a reload. `readGuestShellOutput` reads a running or ended shell in the directory it ran in, and answers `NOT_FOUND` for a shell the store never saw, a `stopped` one (OpenCode unlinks its output on removal), and an OpenCode 404 (evicted past the 25 retained exited commands per directory); other failures pass through. `PluginPane` wires the four effects, checks the `shells` grant per call, counts shell subscriptions against the frame's 32-subscription limit, and answers `stopShell` on a shell that is not running with `NOT_FOUND`. `opencodeClient.stopBackgroundShells` writes the agent's cancellation note before it kills the command and kills nothing when the note fails.
36
38
  - `storage` is instance-owned JSON under the guest's namespace, without a new capability. `storage.js` serializes authorization, read-modify-write, and removal per namespace; writes use unique temporary files and rename. Missing keys differ from stored null; corrupt files and failed writes fail without clearing data. Device scope is an explicit SDK opt-in only when ready features advertise `deviceStorage`; it uses the host runtime key plus guest ID and server-issued installation `storageId`, and has no instance fallback. Different runtime identities remain separate even if a server duplicates a storage ID. URL-based runtime keys separate address aliases; saved-host and relay keys retain their existing identity rules. IndexedDB limits are 2 MiB per namespace and 16 MiB of serialized preference data across the database; browser metadata has separate overhead. A confirmed uninstall makes the namespace inaccessible, but an offline profile retains its bytes until site data is removed. Device storage does not promise secret or SSO storage. Limits and public result shapes live in `API.md`. For project data, extensions namespace keys with project IDs.
37
39
 
38
40
  - Manifest `apiVersion` is `1`. Wire envelope `v` stays `1`. Any other value is `unsupported-api-version`. Optional `engines.openchamber` is `1.22.0` or `>=1.22.0`. Install refuses with `host-too-old` when this OpenChamber build is older. Package installs also need a semver `version` on `package.json` (`1.0.0`); the catalog exposes it and Settings → Extensions shows `v1.0.0` on the card. Already-installed packages without `version` still list; new installs without it fail as `invalid-manifest`.
@@ -41,7 +43,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
41
43
  - Extra manifest keys are dropped, not forwarded.
42
44
  - Theme text fields are required on `apiVersion: 1`: `primaryText`, `successText`, `warningText`, `errorText`, `infoText`. Optional syntax fields are `syntaxKeyword`, `syntaxString`, `syntaxNumber`, `syntaxFunction`, `syntaxType`, `syntaxComment`, `syntaxVariable`, and `syntaxOperator`. Host calculation and UI-kit application are documented in `src/ui/DOCUMENTATION.md`. Keep the wire schema, TypeScript contract and iframe mapping in sync.
43
45
  - `connectHost` rejects when `parent === self`. A silent host rejects in-flight RPCs as `HOST_TIMEOUT` after 20s. Dispose rejects the rest as `HOST_UNAVAILABLE`.
44
- - Failed `result` may carry `code`. Known guest codes are `HOST_UNAVAILABLE`, `HOST_TIMEOUT`, `HOST_REJECTED`, `DISCONNECTED`, `DISABLED`, `BAD_PATH`, `NO_INTEGRATION`, `NO_SESSION`, `SESSION_BUSY`, `NO_SERVICE`, `SERVICE_FAILED`, `NOT_GRANTED`, `NO_DIRECTORY`, `NOT_FOUND`, `FILE_TOO_LARGE`, `DENIED`, `NO_MODEL`, `MODEL_FAILED`, `UNSUPPORTED`. Anything else becomes `HOST_REJECTED`. The `error` string stays.
46
+ - Failed `result` may carry `code`. Known guest codes are `HOST_UNAVAILABLE`, `HOST_TIMEOUT`, `HOST_REJECTED`, `DISCONNECTED`, `DISABLED`, `BAD_PATH`, `NO_INTEGRATION`, `NO_SESSION`, `SESSION_BUSY`, `NO_SERVICE`, `SERVICE_FAILED`, `REQUEST_FAILED`, `RESPONSE_TOO_LARGE`, `NOT_GRANTED`, `NO_DIRECTORY`, `NOT_FOUND`, `FILE_TOO_LARGE`, `DENIED`, `NO_MODEL`, `MODEL_FAILED`, `UNSUPPORTED`. Anything else becomes `HOST_REJECTED`. The `error` string stays.
45
47
  - `parseManifest` takes a typed document. File or network bytes go through `parseManifestJson`. Both return a result and do not throw on junk.
46
48
  - The host parses iframe `event.data` with `guestMessageSchema` (`@openchamber/sdk/schemas`). The guest reads host pushes with `readHostMessage` in `contract.ts`: envelope and discriminant only, because the sender is the verified parent frame. `protocol.ts` asserts the hand-written `HostMessage` / `GuestMessage` types against the zod schemas at compile time, so the two cannot drift.
47
49
  - `ready.item` is `GuestItem | null`: the composer chip (`AttachIssueRequest`, unchanged, `kind` still `issue` / `pull`), or a `GuestMessageItem` / `GuestSessionItem` from a declared action, discriminated by `kind: 'message' | 'session'` and carrying the manifest `action` id. A message item's `text` is what `formatMessageRecordText` renders for the export, capped at `GUEST_ITEM_MESSAGE_TEXT_MAX` (200 000). A session item carries `messages` only when the action declared `payload: ["messages"]` and `conversation` is granted; `buildGuestSessionItem` in `packages/ui/src/lib/guests/actions.ts` drops the oldest messages until the serialized item is under `GUEST_ITEM_SESSION_MAX` (2 000 000) and sets `truncated`. The host schema refuses a larger item. `isGuestAttachItem` / `isGuestMessageItem` / `isGuestSessionItem` narrow it.
@@ -51,7 +53,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
51
53
  - Host → guest `resolve` is `{ id, payload: { command, args } }`; the guest answers `resolve-result` with the same `id` and `{ item }` (a clamped `AttachIssueRequest` or `null`) or `{ error }` (1 to `GUEST_RESOLVE_ERROR_MAX` characters). The host waits `GUEST_REQUEST_TIMEOUT_MS`, then treats silence as a failure and never sends a `result` for a `resolve-result`. `badge` is `{ count }` with `count` an integer 0..999 or `null`; the host schema drops anything else.
52
54
  - `ready` always carries `session` (`{ id, title, busy }` or `null`), `surface` (`panel` on the rail, `dialog` in the attach window, `page` full screen, `background` hidden, `status` in the Work Status panel, `file` as a Files view editor), `connection` (`{ connected, account }`), and `settings` (declared keys only). Title falls back to `id` when the host record has no title. `busy` is live `session_status` `busy` or `retry`. Missing status is `busy: false`, not an empty success that clears a previous snapshot. `model` is `providerID/id` when the host session record has both. `agent` is the session agent name when present. `surface` is which host chrome mounted the iframe. It is not `openSurface`. `item` is the attached item the surface was opened for (the user clicked that item's chip on the composer; the same `AttachIssueRequest` shape the guest handed to `attach`, with `text` filled from the chip's context) or `null` from the rail icon and the + menu. The host also pushes `item` when it changes while the frame is up; `onItem` replays the latest value. `ready.theme.tokens` is the host colour bag: surfaces (`background`, `elevated`, `mutedSurface`, `subtle`), text (`foreground`, `elevatedForeground`, `muted`), interaction (`border`, `hover`, `active`, `selection`, `selectionForeground`, `focus`), `primary` / `primaryForeground`, the four status colours, `font`, `mono`, and `radius`. `applyHostReady` writes it onto the iframe. The host also pushes `connection` and `settings` the same way as `session`. Access tokens never appear in `ready` or a result.
53
55
  - `writeClipboard` text is 1–32000 characters. `compose` text is 1–16000 after trim. Compose default mode is `append`. Compose writes the chat composer. It does not send.
54
- - `contributes.capabilities` is a list of `prompt`, `sessions`, `files`, and/or `model` (`DECLARED_GUEST_CAPABILITIES`). `model` gates `generate`: `{ prompt, system?, maxOutputTokens? }` → `{ text }`, posted with a `GUEST_GENERATE_TIMEOUT_MS` (90s) timeout instead of the usual 20s because the model answers slowly; the host answers `NO_MODEL` when the small-model service resolves nothing (or only Claude Code) and `MODEL_FAILED` when the provider call fails. `requestedGuestCapabilities` adds `conversation`, `service`, `network`, `filesystem`, and `origins` from a session action with `payload: ["messages"]`, `contributes.service`, `contributes.integration`, `contributes.filesystem`, and `contributes.origins`; the user approves that whole list once at install (`PUT /api/guests/:id/capabilities`), and a package that later asks for more is re-approved. `hasGuestCapability` gates `prompt({ send })`, `startSession`, `request`, `serviceRequest`, and the four file calls; a refusal is `NOT_GRANTED` (`NO_SERVICE` for the service).
56
+ - `contributes.capabilities` is a list of `prompt`, `sessions`, `files`, `model`, and/or `shells` (`DECLARED_GUEST_CAPABILITIES`). `model` gates `generate`: `{ prompt, system?, maxOutputTokens? }` → `{ text }`, posted with a `GUEST_GENERATE_TIMEOUT_MS` (90s) timeout instead of the usual 20s because the model answers slowly; the host answers `NO_MODEL` when the small-model service resolves nothing (or only Claude Code) and `MODEL_FAILED` when the provider call fails. `requestedGuestCapabilities` adds `conversation`, `service`, `network`, `filesystem`, and `origins` from a session action with `payload: ["messages"]`, `contributes.service`, `contributes.integration`, `contributes.filesystem`, and `contributes.origins`; the user approves that whole list once at install (`PUT /api/guests/:id/capabilities`), and a package that later asks for more is re-approved. `hasGuestCapability` gates `prompt({ send })`, `startSession`, `request`, `serviceRequest`, the four file calls, and the three shell calls; a refusal is `NOT_GRANTED` (`NO_SERVICE` for the service).
55
57
  - `contributes.filesystem` is 1 to 16 globs, each 1 to 256 characters, starting with `/` or `~/`, with no `..`, empty segment, NUL, or backslash (`isGuestFilesystemPattern`; parse failure is `invalid-filesystem`). `**` spans directories, `*` and `?` stay inside one segment, and a pattern ending in `/**` also matches the directory itself. The public catalog row carries the list as `filesystem` so the approval dialog can show it.
56
58
  - `readFile` / `writeFile` / `listDir` / `stat` are `file-read` / `file-write` / `file-list` / `file-stat` on the wire. `guestFileScope` decides the grant: a relative path is `project` (capability `files`, joined to the open project; none open is `NO_DIRECTORY`), `/…` or `~/…` is `filesystem` (must match a declared pattern). The server compares canonical paths, so a symlink out of either tree is `BAD_PATH`, as is any `..` segment. Results are `{ content }`, `{ written: true }`, `{ entries: { name, kind }[] }`, and `{ kind, size, mtime }` where `kind` may be `missing`. `GUEST_FILE_PATH_MAX` (1024), `GUEST_FILE_CONTENT_MAX` (2 000 000 characters, read and write), `GUEST_FILE_LIST_MAX` (2000 entries). The client refuses an empty, oversized, NUL, or backslash path as `BAD_PATH` and oversized content as `FILE_TOO_LARGE` before posting; the host schema accepts only what `isGuestFilePath` does, so a dropped message cannot turn into `HOST_TIMEOUT`. Nothing about the write is logged, not even the path.
57
59
  - `contributes.attach` is `true`, `false`, `"panel"`, `"dialog"`, or `{ "mode": "panel" | "dialog", "entry"?: string }`. `true` and `"panel"` add a + menu row that opens the rail. `"dialog"` opens a host window that mounts the same iframe. The object form's `entry` is an HTML page inside the package (same path rules as `panel.entry`, install checks it exists and that its scripts are built) that the dialog loads instead of `panel.entry`; it is only accepted with `mode: "dialog"`, and `resolveAttachEntry(contributes)` returns it or `null`. The public catalog row carries it as `attachEntry`. Clicking the guest's chip on the composer reopens that guest with the chip as `ready.item`: a dialog guest in the attach window, a panel guest on the rail. New Worktree shows the same dialog-mode guests next to GitHub and Linear. Omitted or `false` stays off those menus. Public 1.0 is web and desktop. VS Code and mobile mark the guest catalog `unsupported`. They do not show the row.
@@ -61,7 +63,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
61
63
  - `prompt` is `{ text, send? }`. `send` omitted or `false` replace-composes on the current session. `send: true` sends on that session with the same model path as `startSession`. No session is `NO_SESSION`. A busy session on send is `SESSION_BUSY`. Compose still works while busy. The result is `{ sent }`. The guest does not pick a model or agent.
62
64
  - `sessionLink` is the same attach fields on the current session. It does not create a session. No project or no session is a refusal.
63
65
  - `onSessionLifecycle` is a host push. Live `session_status` `busy` and `retry` are `started`. `idle` is `completed`. An unknown status type is `failure`. That is not abort or crash. A late listener gets the last phase, from `ready.session.busy` when no dedicated push has arrived.
64
- - `oauthStart` opens the provider authorize URL, or the first-party Linear authorize URL for a host Linear guest. `oauthDisconnect` drops that guest's tokens, or the Linear connection for a host Linear guest. Token guests have no authorize URL. `request` is `{ method, path, query?, body? }`. `path` must start with `/`, stay on the declared `apiOrigin`, and never include a scheme. The host attaches `Authorization`. Token guests send the pasted token as is (`token.scheme` omitted or `raw`), as `Bearer <token>` (`token.scheme: "bearer"`), or as `Basic base64(username:token)` (`token.scheme: "basic"`; the Integrations card asks for a username, `token.usernameLabel` names that field, and the server stores the encoded pair as the access token so requests never need the username again). OAuth and host Linear send Bearer. The result is `{ status, body }`. A 409 `DISCONNECTED` or 400 `BAD_PATH` / `NO_INTEGRATION` lands on the guest as `HostRequestError.code`. Silence for 20s is `HOST_TIMEOUT`. The guest parses JSON. A host Linear guest may also `GET /api/linear/issues/get` through that same `request` call. The host answers from the first-party Linear route, which already asks Linear for public file URLs. Guest request itself does not know Linear.
66
+ - `oauthStart` opens the provider authorize URL, or the first-party Linear authorize URL for a host Linear guest. `oauthDisconnect` drops that guest's tokens, or the Linear connection for a host Linear guest. Token guests have no authorize URL. `request` is `{ method, path, query?, body? }`. `path` must start with `/`, stay on the declared `apiOrigin`, and never include a scheme. The host attaches `Authorization`. Token guests send the pasted token as is (`token.scheme` omitted or `raw`), as `Bearer <token>` (`token.scheme: "bearer"`), or as `Basic base64(username:token)` (`token.scheme: "basic"`; the Integrations card asks for a username, `token.usernameLabel` names that field, and the server stores the encoded pair as the access token so requests never need the username again). OAuth and host Linear send Bearer. The result is `{ status, body }`. An answer over `GUEST_REQUEST_RESPONSE_MAX` (8 000 000 bytes) is `RESPONSE_TOO_LARGE`; the host never hands over a cut-off body. A 409 `DISCONNECTED` or 400 `BAD_PATH` / `NO_INTEGRATION` lands on the guest as `HostRequestError.code`. Silence for 20s is `HOST_TIMEOUT`. The guest parses JSON. A host Linear guest may also `GET /api/linear/issues/get` through that same `request` call. The host answers from the first-party Linear route, which already asks Linear for public file URLs. Guest request itself does not know Linear.
65
67
  - `contributes.service` names a host-spawned entry on `apiVersion: 1`. `serviceRequest` / `serviceStatus` proxy to that process on loopback. See `GUEST_SERVICES.md`. Permissions are grant UI only in phase 1. No `host.docker`.
66
68
 
67
69
  ## Host hole
package/GUEST_SERVICES.md CHANGED
@@ -111,6 +111,7 @@ Error codes:
111
111
  - `DISABLED` — extension paused in Settings → Extensions (service stopped; tokens/grants stay)
112
112
  - `SERVICE_FAILED` — process crashed or never became ready; nothing was sent to it
113
113
  - `REQUEST_FAILED` — the request was sent but got no usable answer (timeout, dropped connection); the service may have acted on it
114
+ - `RESPONSE_TOO_LARGE` — the service answered with more than `GUEST_REQUEST_RESPONSE_MAX` bytes; it acted on the request, the answer is dropped whole
114
115
  - Existing: `HOST_TIMEOUT`, `HOST_REJECTED`, `HOST_UNAVAILABLE`, `BAD_PATH`
115
116
 
116
117
  Server routes (authenticated UI session):
package/README.md CHANGED
@@ -256,6 +256,8 @@ host.onReady((ctx) => {
256
256
 
257
257
  OpenChamber supplies thin, theme-aware native scrollbars inside extension documents, including nested lists, tabs, and textareas. Like the app's own, they stay hidden until you hover or scroll. The UI kit includes the same defaults for development previews. Existing installed bundles get the host stylesheet without rebuilding. Custom rendering hosts can use `GUEST_SCROLLBAR_CSS` from `@openchamber/sdk`. Authors can override these default rules; an extension's CSP still applies.
258
258
 
259
+ The kit's look comes from the OpenChamber version that runs your extension: colours, borders, corner radii, and hover, selected and focus states follow the app after an update, with no rebuild on your side. Sizes and spacing stay the ones your bundle was built with, so a layout built around them doesn't shift; they change when you rebuild against a newer SDK. The app adds its look after your document, so to restyle a kit control, use a selector more specific than the kit's, such as `#root .oc-sdk-btn`. Custom rendering hosts can use `GUEST_UI_LOOK_CSS` from `@openchamber/sdk/ui`.
260
+
259
261
  ## Schemas
260
262
 
261
263
  `@openchamber/sdk/schemas` exports the zod schemas for the manifest and the messages, for tools that validate extensions. The main entry has no zod dependency, so a page bundle stays small.
@@ -1,5 +1,6 @@
1
1
  import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from './api-version.ts';
2
2
  import type { FileEditorChange, FileEditorDocument, FileSnapshotRequest, FileSnapshotResultPayload } from './file-editor.ts';
3
+ import type { GuestRunningShellsSnapshot, GuestShellOutputRequest, GuestShellOutputResult, GuestShellStopResult, GuestShellsSubscription } from './shells.ts';
3
4
  import type { GuestSessionWorktree, GuestStorageRequest, GuestStorageResult, GuestWorkspaceQuery, GuestWorkspaceSnapshot, GuestWorkspaceSubscription, GuestWorkspaceUpdate, GuestWorktree } from './workspace.ts';
4
5
  import type { GuestStatusControl, GuestStatusControlEvent } from './status-controls.ts';
5
6
  import type { GuestPopoverClosedEvent, GuestPopoverContext, GuestPopoverRequest } from './popover.ts';
@@ -166,7 +167,7 @@ export type GenerateRequest = {
166
167
  export type GenerateResult = {
167
168
  text: string;
168
169
  };
169
- export type HostResultPayload = GuestStorageResult | GuestWorkspaceSnapshot | GuestRequestResult | StartSessionResult | PromptResult | ServiceStatusResult | FileReadResult | FileWriteResult | FileListResult | FileStatResult | GenerateResult;
170
+ export type HostResultPayload = GuestStorageResult | GuestWorkspaceSnapshot | GuestShellOutputResult | GuestShellStopResult | GuestRequestResult | StartSessionResult | PromptResult | ServiceStatusResult | FileReadResult | FileWriteResult | FileListResult | FileStatResult | GenerateResult;
170
171
  export declare const isStartSessionResult: (value: HostResultPayload | undefined) => value is StartSessionResult;
171
172
  export declare const isPromptResult: (value: HostResultPayload | undefined) => value is PromptResult;
172
173
  export declare const EMPTY_GUEST_CONNECTION: GuestConnection;
@@ -334,7 +335,8 @@ export declare const GUEST_SESSION_AGENT_MAX = 80;
334
335
  export declare const GUEST_SETTING_VALUE_MAX = 2000;
335
336
  export declare const GUEST_REQUEST_PATH_MAX = 2000;
336
337
  export declare const GUEST_REQUEST_BODY_MAX = 64000;
337
- export declare const GUEST_REQUEST_RESPONSE_MAX = 256000;
338
+ /** Bytes of a `request` / `serviceRequest` answer; a larger one is `RESPONSE_TOO_LARGE`, never cut off. */
339
+ export declare const GUEST_REQUEST_RESPONSE_MAX = 8000000;
338
340
  export declare const GUEST_REQUEST_TIMEOUT_MS = 20000;
339
341
  /** Guest file path, in characters. */
340
342
  export declare const GUEST_FILE_PATH_MAX = 1024;
@@ -362,7 +364,7 @@ export declare const GUEST_BADGE_MAX = 999;
362
364
  export declare const GUEST_FRAME_HEIGHT_MAX = 10000;
363
365
  /** Characters in a `resolve-result` error string. */
364
366
  export declare const GUEST_RESOLVE_ERROR_MAX = 500;
365
- export declare const HOST_REQUEST_ERROR_CODES: readonly ["HOST_UNAVAILABLE", "HOST_TIMEOUT", "HOST_REJECTED", "DISCONNECTED", "DISABLED", "BAD_PATH", "NO_INTEGRATION", "NO_SERVICE", "SERVICE_FAILED", "NO_SESSION", "SESSION_BUSY", "NOT_GRANTED", "NO_DIRECTORY", "NOT_FOUND", "FILE_TOO_LARGE", "DENIED", "NO_MODEL", "MODEL_FAILED", "UNSUPPORTED"];
367
+ export declare const HOST_REQUEST_ERROR_CODES: readonly ["HOST_UNAVAILABLE", "HOST_TIMEOUT", "HOST_REJECTED", "DISCONNECTED", "DISABLED", "BAD_PATH", "NO_INTEGRATION", "NO_SERVICE", "SERVICE_FAILED", "REQUEST_FAILED", "RESPONSE_TOO_LARGE", "NO_SESSION", "SESSION_BUSY", "NOT_GRANTED", "NO_DIRECTORY", "NOT_FOUND", "FILE_TOO_LARGE", "DENIED", "NO_MODEL", "MODEL_FAILED", "UNSUPPORTED"];
366
368
  export declare const SERVICE_STATUS_VALUES: readonly ["stopped", "starting", "ready", "failed"];
367
369
  export type ServiceStatus = (typeof SERVICE_STATUS_VALUES)[number];
368
370
  export type ServiceStatusResult = {
@@ -490,10 +492,17 @@ export type HostResultMessage = Envelope & {
490
492
  error: string;
491
493
  code: HostRequestErrorCode;
492
494
  });
495
+ export type HostShellsMessage = Envelope & {
496
+ type: 'shells';
497
+ payload: {
498
+ subscriptionId: string;
499
+ snapshot: GuestRunningShellsSnapshot;
500
+ };
501
+ };
493
502
  export type HostMessage = (Envelope & {
494
503
  type: 'workspace';
495
504
  payload: GuestWorkspaceUpdate;
496
- }) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostStatusControlEventMessage | HostPopoverClosedMessage | HostResolveMessage | HostActionMessage | HostFileOpenMessage | HostFileSnapshotMessage | HostFileSavedMessage | HostResultMessage;
505
+ }) | HostShellsMessage | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostStatusControlEventMessage | HostPopoverClosedMessage | HostResolveMessage | HostActionMessage | HostFileOpenMessage | HostFileSnapshotMessage | HostFileSavedMessage | HostResultMessage;
497
506
  type GuestCall<Type extends string, Payload = never> = Envelope & {
498
507
  type: Type;
499
508
  id: string;
@@ -577,9 +586,17 @@ export type GuestFileSaveMessage = Envelope & {
577
586
  export type GuestFileUnsupportedMessage = Envelope & {
578
587
  type: 'file-unsupported';
579
588
  };
589
+ export type GuestShellsSubscribeMessage = GuestCall<'shells-subscribe', GuestShellsSubscription>;
590
+ export type GuestShellsUnsubscribeMessage = GuestCall<'shells-unsubscribe', {
591
+ subscriptionId: string;
592
+ }>;
593
+ export type GuestShellOutputMessage = GuestCall<'shell-output', GuestShellOutputRequest>;
594
+ export type GuestShellStopMessage = GuestCall<'shell-stop', {
595
+ shellId: string;
596
+ }>;
580
597
  export type GuestMessage = GuestCall<'workspace-read', GuestWorkspaceQuery> | GuestCall<'workspace-subscribe', GuestWorkspaceSubscription> | GuestCall<'workspace-unsubscribe', {
581
598
  subscriptionId: string;
582
- }> | GuestCall<'storage', GuestStorageRequest> | GuestCall<'open-session', {
599
+ }> | GuestShellsSubscribeMessage | GuestShellsUnsubscribeMessage | GuestShellOutputMessage | GuestShellStopMessage | GuestCall<'storage', GuestStorageRequest> | GuestCall<'open-session', {
583
600
  sessionId: string;
584
601
  }> | GuestHelloMessage | GuestToastMessage | GuestOpenUrlMessage | GuestOpenSurfaceMessage | GuestClipboardWriteMessage | GuestComposeMessage | GuestAttachMessage | GuestStartSessionMessage | GuestPromptMessage | GuestSessionLinkMessage | GuestCloseMessage | GuestOauthStartMessage | GuestOauthDisconnectMessage | GuestRequestMessage | GuestServiceRequestMessage | GuestServiceStatusMessage | GuestFileReadMessage | GuestFileWriteMessage | GuestFileListMessage | GuestFileStatMessage | GuestGenerateMessage | GuestBadgeMessage | GuestResizeMessage | GuestOpenCommitMessage | GuestStatusControlsMessage | GuestPopoverOpenMessage | GuestPopoverCloseMessage | GuestPopoverAnchorMessage | GuestActionResultMessage | GuestResolveResultMessage | GuestFileSnapshotResultMessage | GuestFileChangeMessage | GuestFileSaveMessage | GuestFileUnsupportedMessage;
585
602
  export declare const isServiceStatusResult: (value: HostResultPayload | undefined) => value is ServiceStatusResult;
package/dist/contract.js CHANGED
@@ -33,7 +33,8 @@ export const GUEST_SESSION_AGENT_MAX = 80;
33
33
  export const GUEST_SETTING_VALUE_MAX = 2_000;
34
34
  export const GUEST_REQUEST_PATH_MAX = 2_000;
35
35
  export const GUEST_REQUEST_BODY_MAX = 64_000;
36
- export const GUEST_REQUEST_RESPONSE_MAX = 256_000;
36
+ /** Bytes of a `request` / `serviceRequest` answer; a larger one is `RESPONSE_TOO_LARGE`, never cut off. */
37
+ export const GUEST_REQUEST_RESPONSE_MAX = 8_000_000;
37
38
  export const GUEST_REQUEST_TIMEOUT_MS = 20_000;
38
39
  /** Guest file path, in characters. */
39
40
  export const GUEST_FILE_PATH_MAX = 1_024;
@@ -71,6 +72,8 @@ export const HOST_REQUEST_ERROR_CODES = [
71
72
  'NO_INTEGRATION',
72
73
  'NO_SERVICE',
73
74
  'SERVICE_FAILED',
75
+ 'REQUEST_FAILED',
76
+ 'RESPONSE_TOO_LARGE',
74
77
  'NO_SESSION',
75
78
  'SESSION_BUSY',
76
79
  'NOT_GRANTED',
@@ -210,6 +213,7 @@ export const isFileStatResult = (value) => Boolean(value && 'kind' in value && '
210
213
  export const isGenerateResult = (value) => Boolean(value && 'text' in value && String(value.text) === value.text && !('status' in value));
211
214
  const HOST_PUSH_TYPES = new Set([
212
215
  'workspace',
216
+ 'shells',
213
217
  'ready', 'directory', 'session', 'connection', 'settings', 'session-lifecycle', 'item', 'resolve', 'action',
214
218
  'status-control-event', 'popover-closed',
215
219
  'file-open', 'file-snapshot', 'file-saved',
@@ -1,6 +1,6 @@
1
1
  /** `1.22.0` or `>=1.22.0`. Ranges beyond a floor are not accepted. */
2
2
  export declare const OPENCHAMBER_ENGINE_PATTERN: RegExp;
3
- export type OpenChamberVersionParts = {
3
+ type OpenChamberVersionParts = {
4
4
  major: number;
5
5
  minor: number;
6
6
  patch: number;
@@ -14,3 +14,4 @@ export declare const openChamberEngineMinimum: (engine: string) => string | null
14
14
  * Unknown host versions fail closed when an engine is declared.
15
15
  */
16
16
  export declare const hostMeetsOpenChamberEngine: (hostVersion: string, engine: string) => boolean;
17
+ export {};
package/dist/host.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { JsonValue } from './contract.ts';
4
4
  import { type GuestPopoverClosedEvent, type GuestPopoverRequest } from './popover.ts';
5
5
  import { type FileEditorChange, type FileEditorDocument, type FileEditorSnapshot, type FileSnapshotPurpose } from './file-editor.ts';
6
6
  import { type AttachIssueRequest, type GuestActionItem, type ComposeRequest, type PromptRequest, type PromptResult, type SessionLifecycleEvent, type StartSessionRequest, type GuestConnection, type GuestItem, type GuestMessage, type GuestRequest, type GuestRequestResult, type GuestSettings, type HostReadyContext, type HostRequestErrorCode, type ResolveRequest, type SessionSnapshot, type StartSessionResult, type ToastRequest, type ServiceStatusResult, type FileListResult, type FileReadResult, type FileStatResult, type FileWriteResult, type GenerateRequest, type GenerateResult } from './contract.ts';
7
+ import { type GuestRunningShellsSnapshot, type GuestShellOutputResult, type GuestShellStopResult, type GuestShellsScope } from './shells.ts';
7
8
  export type HostFrame = {
8
9
  addEventListener: Window['addEventListener'];
9
10
  removeEventListener: Window['removeEventListener'];
@@ -26,6 +27,12 @@ export type HostClient = {
26
27
  onProjects: (listener: (snapshot: GuestProjectsSnapshot) => void) => Promise<() => void>;
27
28
  onWorktrees: (projectId: string, listener: (snapshot: GuestWorktreesSnapshot) => void) => Promise<() => void>;
28
29
  onSessions: (projectId: string, listener: (snapshot: GuestSessionsSnapshot) => void) => Promise<() => void>;
30
+ onRunningShells: (scope: GuestShellsScope, listener: (snapshot: GuestRunningShellsSnapshot) => void) => Promise<() => void>;
31
+ readShellOutput: (shellId: string, options?: {
32
+ cursor?: number;
33
+ tailBytes?: number;
34
+ }) => Promise<GuestShellOutputResult>;
35
+ stopShell: (shellId: string) => Promise<GuestShellStopResult>;
29
36
  openSession: (sessionId: string) => Promise<void>;
30
37
  storage: {
31
38
  get: (key: string, options?: GuestStorageOptions) => Promise<JsonValue | undefined>;
package/dist/host.js CHANGED
@@ -4,6 +4,7 @@ import { isGuestStatusControls } from "./status-controls.js";
4
4
  import { isGuestPopoverRequest } from "./popover.js";
5
5
  import { GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_VERSION_MAX, fileEditorPayloadSize, sameFileEditorDocument, } from "./file-editor.js";
6
6
  import { GUEST_FILE_CONTENT_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_TOAST_MAX, GUEST_FILE_PATH_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TIMEOUT_MS, GUEST_REQUEST_TIMEOUT_MS, GUEST_RESOLVE_ERROR_MAX, isGuestFilePath, isGuestRequestPath, clampAttachRequest, clampBadgeCount, clampFrameHeight, isGuestCommitSha, clampPromptRequest, clampStartSessionRequest, readHostMessage, isGenerateResult, isFileListResult, isFileReadResult, isFileStatResult, isJsonValue, isFileWriteResult, isServiceStatusResult, isGuestRequestResult, isPromptResult, isStartSessionResult, } from "./contract.js";
7
+ import { GUEST_SHELL_ID_MAX, GUEST_SHELL_OUTPUT_TAIL_MAX } from "./shells.js";
7
8
  // Structural, not `instanceof KeyboardEvent`: the frame's own realm owns that
8
9
  // constructor, and non-browser runtimes have none.
9
10
  const isKeyEvent = (event) => 'key' in event && 'metaKey' in event && 'ctrlKey' in event;
@@ -59,6 +60,7 @@ export const connectHost = (options = {}) => {
59
60
  let saveShortcutInstalled = false;
60
61
  const pending = new Map();
61
62
  const workspaceListeners = new Map();
63
+ const shellsListeners = new Map();
62
64
  let disposed = false;
63
65
  const ids = { value: 0 };
64
66
  let lastReady = null;
@@ -103,6 +105,12 @@ export const connectHost = (options = {}) => {
103
105
  emit([listener], message.payload.snapshot);
104
106
  return;
105
107
  }
108
+ if (message.type === 'shells') {
109
+ const listener = shellsListeners.get(message.payload.subscriptionId);
110
+ if (listener)
111
+ emit([listener], message.payload.snapshot);
112
+ return;
113
+ }
106
114
  if (message.type === 'ready') {
107
115
  lastReady = message.payload;
108
116
  lastLifecycle = lifecycleFromSession(message.payload.session);
@@ -372,6 +380,28 @@ export const connectHost = (options = {}) => {
372
380
  post({ ...envelope, type: 'workspace-unsubscribe', id: nextId(ids), payload: { subscriptionId } });
373
381
  };
374
382
  };
383
+ const subscribeShells = async (scope, listener) => {
384
+ if (scope.kind === 'session')
385
+ requireIdentity(scope.sessionId);
386
+ if (scope.kind === 'project')
387
+ requireIdentity(scope.projectId);
388
+ const subscriptionId = nextId(ids);
389
+ shellsListeners.set(subscriptionId, listener);
390
+ try {
391
+ await request({ ...envelope, type: 'shells-subscribe', id: nextId(ids), payload: { subscriptionId, scope } });
392
+ }
393
+ catch (error) {
394
+ shellsListeners.delete(subscriptionId);
395
+ if (!disposed)
396
+ post({ ...envelope, type: 'shells-unsubscribe', id: nextId(ids), payload: { subscriptionId } });
397
+ throw error;
398
+ }
399
+ return () => {
400
+ if (!shellsListeners.delete(subscriptionId) || disposed)
401
+ return;
402
+ post({ ...envelope, type: 'shells-unsubscribe', id: nextId(ids), payload: { subscriptionId } });
403
+ };
404
+ };
375
405
  const storage = async (payload) => {
376
406
  if ('key' in payload && (payload.key.length === 0 || payload.key.length > GUEST_STORAGE_KEY_MAX)) {
377
407
  throw new HostRequestError('HOST_REJECTED', 'Storage key must contain 1 to 128 characters.');
@@ -432,6 +462,31 @@ export const connectHost = (options = {}) => {
432
462
  listener(snapshot); }),
433
463
  onSessions: (projectId, listener) => subscribeWorkspace({ kind: 'sessions', projectId }, (snapshot) => { if (snapshot.kind === 'sessions')
434
464
  listener(snapshot); }),
465
+ onRunningShells: (scope, listener) => subscribeShells(scope, listener),
466
+ readShellOutput: async (shellId, options) => {
467
+ requireIdentity(shellId, GUEST_SHELL_ID_MAX);
468
+ const cursor = options?.cursor;
469
+ if (cursor !== undefined && (!Number.isInteger(cursor) || cursor < 0)) {
470
+ throw new HostRequestError('HOST_REJECTED', 'Shell output cursor must be a non-negative integer.');
471
+ }
472
+ const tailBytes = options?.tailBytes;
473
+ if (tailBytes !== undefined && (!Number.isInteger(tailBytes) || tailBytes < 1 || tailBytes > GUEST_SHELL_OUTPUT_TAIL_MAX)) {
474
+ throw new HostRequestError('HOST_REJECTED', `Shell output tailBytes must be an integer between 1 and ${GUEST_SHELL_OUTPUT_TAIL_MAX}.`);
475
+ }
476
+ const result = await send({ ...envelope, type: 'shell-output', id: nextId(ids), payload: { shellId, cursor, tailBytes } });
477
+ const output = result;
478
+ if (!output || typeof output.output !== 'string' || !Number.isFinite(output.cursor)) {
479
+ throw new HostRequestError('HOST_REJECTED', 'Host did not return shell output.');
480
+ }
481
+ return output;
482
+ },
483
+ stopShell: async (shellId) => {
484
+ requireIdentity(shellId, GUEST_SHELL_ID_MAX);
485
+ const result = (await send({ ...envelope, type: 'shell-stop', id: nextId(ids), payload: { shellId } }));
486
+ if (result?.stopped !== true)
487
+ throw new HostRequestError('HOST_REJECTED', 'Host did not stop the shell.');
488
+ return { stopped: true };
489
+ },
435
490
  openSession: async (sessionId) => {
436
491
  requireIdentity(sessionId);
437
492
  await request({ ...envelope, type: 'open-session', id: nextId(ids), payload: { sessionId } });
@@ -856,6 +911,10 @@ export const connectHost = (options = {}) => {
856
911
  post({ ...envelope, type: 'workspace-unsubscribe', id: nextId(ids), payload: { subscriptionId } });
857
912
  }
858
913
  workspaceListeners.clear();
914
+ for (const subscriptionId of shellsListeners.keys()) {
915
+ post({ ...envelope, type: 'shells-unsubscribe', id: nextId(ids), payload: { subscriptionId } });
916
+ }
917
+ shellsListeners.clear();
859
918
  disposed = true;
860
919
  resolveHandler = null;
861
920
  actionHandler = null;
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export { GUEST_SCROLLBAR_CSS, GUEST_SCROLLBAR_SCRIPT } from './scrollbar-style.t
3
3
  export { guestFramePolicy } from './frame-policy.ts';
4
4
  export type { GuestLoadState, GuestProject, GuestWorktree, GuestSessionActivity, GuestSessionRecord, GuestDirectoryCoverage, GuestProjectsSnapshot, GuestWorktreesSnapshot, GuestSessionsSnapshot, GuestWorkspaceSnapshot, GuestWorkspaceQuery, GuestWorkspaceSubscription, GuestWorkspaceUpdate, GuestStorageRequest, GuestStorageResult, GuestStorageOptions, GuestStorageScope, GuestSessionWorktree } from './workspace.ts';
5
5
  export { GUEST_STORAGE_KEY_MAX, GUEST_STORAGE_KEYS_MAX, GUEST_STORAGE_VALUE_BYTES, GUEST_STORAGE_TOTAL_BYTES, GUEST_DEVICE_STORAGE_TOTAL_BYTES } from './workspace.ts';
6
+ export type { GuestRunningShell, GuestEndedShell, GuestShellEndStatus, GuestRunningShellsSnapshot, GuestShellsScope, GuestShellsSubscription, GuestShellOutputRequest, GuestShellOutputResult, GuestShellStopResult, } from './shells.ts';
7
+ export { GUEST_SHELL_END_STATUSES, GUEST_SHELL_ID_MAX, GUEST_SHELLS_MAX, GUEST_SHELL_OUTPUT_TAIL_MAX, } from './shells.ts';
6
8
  export { GUEST_STATUS_CONTROLS_MAX, GUEST_STATUS_CONTROL_ID, GUEST_STATUS_CONTROL_LABEL_MAX, GUEST_STATUS_CONTROL_OPTIONS_MAX, GUEST_STATUS_CONTROL_VALUE_MAX } from './status-controls.ts';
7
9
  export type { GuestStatusControl, GuestStatusControlEvent, GuestStatusControlOption } from './status-controls.ts';
8
10
  export type { OpenChamberManifestApiVersion } from './api-version.ts';
@@ -16,7 +18,7 @@ export type { HostClient, HostClientOptions, HostFrame } from './host.ts';
16
18
  export { GUEST_POPOVER_COORDINATE_MAX, GUEST_POPOVER_DATA_DEPTH_MAX, GUEST_POPOVER_DATA_MAX, GUEST_POPOVER_HEIGHT_MAX, GUEST_POPOVER_HEIGHT_MIN, GUEST_POPOVER_ID, GUEST_POPOVER_SIDES, GUEST_POPOVER_WIDTH_MAX, GUEST_POPOVER_WIDTH_MIN, GUEST_POPOVER_CLOSE_REASONS, isGuestPopoverRequest, } from './popover.ts';
17
19
  export type { GuestPopoverAnchor, GuestPopoverClosedEvent, GuestPopoverContext, GuestPopoverRequest, GuestPopoverSide } from './popover.ts';
18
20
  export { clampAttachRequest, clampBadgeCount, clampFrameHeight, isGuestCommitSha, GUEST_COMMIT_SHA, clampPromptRequest, clampStartSessionRequest, guestFileScope, isAttachData, isJsonValue, isFileListResult, isFileReadResult, isFileStatResult, isGenerateResult, isFileWriteResult, isGuestFilePath, isGuestAttachItem, isGuestMessageItem, isGuestSessionItem, isServiceStatusResult, isGuestRequestPath, isGuestRequestResult, isHostRequestErrorCode, isPromptResult, isStartSessionResult, readHostMessage, resolveHostRequestErrorCode, } from './contract.ts';
19
- export type { BadgeRequest, ActionResultPayload, GuestActionItem, GuestActionResultMessage, HostActionMessage, HostFileOpenMessage, HostFileSnapshotMessage, HostFileSavedMessage, GuestFileSnapshotResultMessage, GuestFileChangeMessage, GuestFileSaveMessage, GuestFileUnsupportedMessage, GuestBadgeMessage, GuestResizeMessage, GuestOpenCommitMessage, GuestStatusControlsMessage, GuestPopoverAnchorMessage, GuestPopoverCloseMessage, GuestPopoverOpenMessage, OpenCommitRequest, ResizeRequest, GuestItem, GuestItemRole, GuestMessageItem, GuestResolveResultMessage, GuestSessionItem, GuestSessionItemMessage, HostResolveMessage, ResolveRequest, ResolveResultPayload, FileListEntry, FileListRequest, FileListResult, FileReadRequest, FileReadResult, FileStatRequest, FileStatResult, FileWriteRequest, FileWriteResult, GenerateRequest, GenerateResult, GuestFileEntryKind, GuestFileListMessage, GuestFileReadMessage, GuestFileScope, GuestFileStatKind, GuestFileStatMessage, GuestGenerateMessage, GuestFileWriteMessage, ServiceStatus, ServiceStatusResult, HostRequestErrorCode, AttachBranches, AttachIssueRequest, AttachThreadKind, JsonValue, HostResultPayload, PromptRequest, PromptResult, SessionLifecycleEvent, SessionLifecyclePhase, StartSessionRequest, StartSessionResult, StartSessionSent, ComposeRequest, GuestServiceRequestMessage, GuestServiceStatusMessage, GuestAttachMessage, GuestClipboardWriteMessage, GuestCloseMessage, GuestComposeMessage, GuestConnection, GuestHelloMessage, GuestMessage, GuestOauthDisconnectMessage, GuestOauthStartMessage, GuestOpenSurfaceMessage, GuestOpenUrlMessage, GuestRequest, GuestRequestMessage, GuestRequestMethod, GuestPromptMessage, GuestSessionLinkMessage, GuestStartSessionMessage, GuestRequestResult, GuestSettings, GuestToastMessage, GuestHostSurface, HostConnectionMessage, HostDirectoryMessage, HostItemMessage, HostStatusControlEventMessage, HostPopoverClosedMessage, HostMessage, HostReadyContext, HostReadyMessage, HostResultMessage, HostSessionLifecycleMessage, HostSessionMessage, HostSettingsMessage, HostTheme, HostThemeMode, HostThemeTokens, SessionSnapshot, ToastKind, ToastRequest, } from './contract.ts';
21
+ export type { BadgeRequest, ActionResultPayload, GuestActionItem, GuestActionResultMessage, HostActionMessage, HostFileOpenMessage, HostFileSnapshotMessage, HostFileSavedMessage, GuestFileSnapshotResultMessage, GuestFileChangeMessage, GuestFileSaveMessage, GuestFileUnsupportedMessage, GuestBadgeMessage, GuestResizeMessage, GuestOpenCommitMessage, GuestStatusControlsMessage, GuestPopoverAnchorMessage, GuestPopoverCloseMessage, GuestPopoverOpenMessage, OpenCommitRequest, ResizeRequest, GuestItem, GuestItemRole, GuestMessageItem, GuestResolveResultMessage, GuestSessionItem, GuestSessionItemMessage, HostResolveMessage, ResolveRequest, ResolveResultPayload, FileListEntry, FileListRequest, FileListResult, FileReadRequest, FileReadResult, FileStatRequest, FileStatResult, FileWriteRequest, FileWriteResult, GenerateRequest, GenerateResult, GuestFileEntryKind, GuestFileListMessage, GuestFileReadMessage, GuestFileScope, GuestFileStatKind, GuestFileStatMessage, GuestGenerateMessage, GuestFileWriteMessage, ServiceStatus, ServiceStatusResult, HostRequestErrorCode, AttachBranches, AttachIssueRequest, AttachThreadKind, JsonValue, HostResultPayload, PromptRequest, PromptResult, SessionLifecycleEvent, SessionLifecyclePhase, StartSessionRequest, StartSessionResult, StartSessionSent, ComposeRequest, GuestServiceRequestMessage, GuestServiceStatusMessage, GuestShellOutputMessage, GuestShellsSubscribeMessage, GuestShellsUnsubscribeMessage, GuestShellStopMessage, GuestAttachMessage, GuestClipboardWriteMessage, GuestCloseMessage, GuestComposeMessage, GuestConnection, GuestHelloMessage, GuestMessage, GuestOauthDisconnectMessage, GuestOauthStartMessage, GuestOpenSurfaceMessage, GuestOpenUrlMessage, GuestRequest, GuestRequestMessage, GuestRequestMethod, GuestPromptMessage, GuestSessionLinkMessage, GuestStartSessionMessage, GuestRequestResult, GuestSettings, GuestToastMessage, GuestHostSurface, HostConnectionMessage, HostDirectoryMessage, HostItemMessage, HostStatusControlEventMessage, HostPopoverClosedMessage, HostMessage, HostReadyContext, HostReadyMessage, HostResultMessage, HostSessionLifecycleMessage, HostSessionMessage, HostSettingsMessage, HostShellsMessage, HostTheme, HostThemeMode, HostThemeTokens, SessionSnapshot, ToastKind, ToastRequest, } from './contract.ts';
20
22
  export { EMPTY_GUEST_CONNECTION, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_FILE_PATH_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_GENERATE_TIMEOUT_MS, GUEST_FILE_STAT_KINDS, SERVICE_STATUS_VALUES, HOST_REQUEST_ERROR_CODES, SESSION_LIFECYCLE_PHASES, START_SESSION_SENT, GUEST_ACCOUNT_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_TOAST_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_REQUEST_TIMEOUT_MS, GUEST_SETTING_VALUE_MAX, } from './contract.ts';
21
23
  export { BROWSER_CONTROL_ACTIONS, BROWSER_PROVIDER_ACTION_TIMEOUT_MS, BROWSER_PROVIDER_IDLE_MS, BROWSER_PROVIDER_OPEN_TIMEOUT_MS, BROWSER_PROVIDER_PATH, BROWSER_PROVIDER_RESPONSE_MAX, BROWSER_SCROLL_DIRECTIONS, BROWSER_VIEWPORT_MODES, isBrowserControlAction, readBrowserProviderRequest, } from './service-providers.ts';
22
24
  export type { BrowserCaptureData, BrowserCaptureParameters, BrowserClickData, BrowserClickParameters, BrowserConsoleProblem, BrowserControlAction, BrowserElementBounds, BrowserInspectData, BrowserInspectParameters, BrowserNavigationData, BrowserOpenData, BrowserOpenParameters, BrowserProviderContext, BrowserProviderData, BrowserProviderRequest, BrowserProviderResult, BrowserResizeData, BrowserResizeParameters, BrowserScrollData, BrowserScrollDirection, BrowserScrollParameters, BrowserSnapshotData, BrowserSnapshotElement, BrowserSnapshotParameters, BrowserTab, BrowserTabTarget, BrowserTypeData, BrowserTypeParameters, BrowserViewportMode, BrowserViewportSummary, } from './service-providers.ts';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL, OPENCHAMBER_SDK_M
2
2
  export { GUEST_SCROLLBAR_CSS, GUEST_SCROLLBAR_SCRIPT } from "./scrollbar-style.js";
3
3
  export { guestFramePolicy } from "./frame-policy.js";
4
4
  export { GUEST_STORAGE_KEY_MAX, GUEST_STORAGE_KEYS_MAX, GUEST_STORAGE_VALUE_BYTES, GUEST_STORAGE_TOTAL_BYTES, GUEST_DEVICE_STORAGE_TOTAL_BYTES } from "./workspace.js";
5
+ export { GUEST_SHELL_END_STATUSES, GUEST_SHELL_ID_MAX, GUEST_SHELLS_MAX, GUEST_SHELL_OUTPUT_TAIL_MAX, } from "./shells.js";
5
6
  export { GUEST_STATUS_CONTROLS_MAX, GUEST_STATUS_CONTROL_ID, GUEST_STATUS_CONTROL_LABEL_MAX, GUEST_STATUS_CONTROL_OPTIONS_MAX, GUEST_STATUS_CONTROL_VALUE_MAX } from "./status-controls.js";
6
7
  export { compareOpenChamberVersions, hostMeetsOpenChamberEngine, openChamberEngineMinimum, OPENCHAMBER_ENGINE_PATTERN, parseOpenChamberVersion, } from "./host-version.js";
7
8
  export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, toPublicService, toPublicIntegration, } from "./manifest.js";
@@ -273,15 +273,15 @@ export declare const serviceProvides: (service: Pick<ServiceContribution, "provi
273
273
  /**
274
274
  * What a guest may do beyond drawing its own panel. The user approves the
275
275
  * full list once, when the package is installed; a later package that asks
276
- * for more is re-approved. `prompt`, `sessions`, `files`, and `model` are
277
- * declared under `contributes.capabilities`; `service`, `network`,
276
+ * for more is re-approved. `prompt`, `sessions`, `files`, `model`, and
277
+ * `shells` are declared under `contributes.capabilities`; `service`, `network`,
278
278
  * `filesystem` and `origins` follow from `contributes.service`,
279
279
  * `contributes.integration`, `contributes.filesystem` and `contributes.origins`. `model` is one-off text generation with the
280
280
  * user's Small Model (`host.generate`), outside any session.
281
281
  */
282
- export declare const GUEST_CAPABILITIES: readonly ["prompt", "sessions", "files", "model", "conversation", "service", "network", "filesystem", "origins"];
282
+ export declare const GUEST_CAPABILITIES: readonly ["prompt", "sessions", "files", "model", "shells", "conversation", "service", "network", "filesystem", "origins"];
283
283
  export type GuestCapability = (typeof GUEST_CAPABILITIES)[number];
284
- export declare const DECLARED_GUEST_CAPABILITIES: readonly ["prompt", "sessions", "files", "model"];
284
+ export declare const DECLARED_GUEST_CAPABILITIES: readonly ["prompt", "sessions", "files", "model", "shells"];
285
285
  /** The capabilities a manifest may ask for directly. */
286
286
  export type DeclaredGuestCapability = (typeof DECLARED_GUEST_CAPABILITIES)[number];
287
287
  /** How many `contributes.origins` a package may declare. */
package/dist/manifest.js CHANGED
@@ -96,14 +96,14 @@ export const serviceProvides = (service, role) => Boolean(service?.provides?.inc
96
96
  /**
97
97
  * What a guest may do beyond drawing its own panel. The user approves the
98
98
  * full list once, when the package is installed; a later package that asks
99
- * for more is re-approved. `prompt`, `sessions`, `files`, and `model` are
100
- * declared under `contributes.capabilities`; `service`, `network`,
99
+ * for more is re-approved. `prompt`, `sessions`, `files`, `model`, and
100
+ * `shells` are declared under `contributes.capabilities`; `service`, `network`,
101
101
  * `filesystem` and `origins` follow from `contributes.service`,
102
102
  * `contributes.integration`, `contributes.filesystem` and `contributes.origins`. `model` is one-off text generation with the
103
103
  * user's Small Model (`host.generate`), outside any session.
104
104
  */
105
- export const GUEST_CAPABILITIES = ['prompt', 'sessions', 'files', 'model', 'conversation', 'service', 'network', 'filesystem', 'origins'];
106
- export const DECLARED_GUEST_CAPABILITIES = ['prompt', 'sessions', 'files', 'model'];
105
+ export const GUEST_CAPABILITIES = ['prompt', 'sessions', 'files', 'model', 'shells', 'conversation', 'service', 'network', 'filesystem', 'origins'];
106
+ export const DECLARED_GUEST_CAPABILITIES = ['prompt', 'sessions', 'files', 'model', 'shells'];
107
107
  /** How many `contributes.origins` a package may declare. */
108
108
  export const GUEST_ORIGINS_MAX = 8;
109
109
  /** How many `contributes.filesystem` patterns a package may declare. */
package/dist/parse.d.ts CHANGED
@@ -44,6 +44,7 @@ export declare const openChamberManifestSchema: z.ZodObject<{
44
44
  requiresProject: z.ZodOptional<z.ZodBoolean>;
45
45
  }, z.core.$strip>]>>;
46
46
  capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
47
+ shells: "shells";
47
48
  sessions: "sessions";
48
49
  prompt: "prompt";
49
50
  files: "files";
@@ -123,8 +124,8 @@ export declare const openChamberManifestSchema: z.ZodObject<{
123
124
  label: z.ZodString;
124
125
  icon: z.ZodOptional<z.ZodString>;
125
126
  where: z.ZodEnum<{
126
- message: "message";
127
127
  session: "session";
128
+ message: "message";
128
129
  }>;
129
130
  mode: z.ZodOptional<z.ZodEnum<{
130
131
  background: "background";
@@ -218,6 +219,7 @@ export declare const packageManifestSchema: z.ZodObject<{
218
219
  requiresProject: z.ZodOptional<z.ZodBoolean>;
219
220
  }, z.core.$strip>]>>;
220
221
  capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
222
+ shells: "shells";
221
223
  sessions: "sessions";
222
224
  prompt: "prompt";
223
225
  files: "files";
@@ -297,8 +299,8 @@ export declare const packageManifestSchema: z.ZodObject<{
297
299
  label: z.ZodString;
298
300
  icon: z.ZodOptional<z.ZodString>;
299
301
  where: z.ZodEnum<{
300
- message: "message";
301
302
  session: "session";
303
+ message: "message";
302
304
  }>;
303
305
  mode: z.ZodOptional<z.ZodEnum<{
304
306
  background: "background";