@openchamber/sdk 2.1.1 → 2.2.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.
@@ -449,7 +465,7 @@ Used by the OpenChamber host and by tools that validate packages. Guests rarely
449
465
  "entry": "panel/index.html"
450
466
  },
451
467
  "attach": "dialog",
452
- "capabilities": ["prompt", "sessions", "files"],
468
+ "capabilities": ["prompt", "sessions", "files", "shells"],
453
469
  "filesystem": ["~/.config/opencode/opencode.json", "/tmp/acme/**"],
454
470
  "actions": [
455
471
  { "id": "create-task", "label": "Create task from message", "icon": "add-circle", "where": "message", "roles": ["assistant"] },
@@ -475,7 +491,7 @@ Used by the OpenChamber host and by tools that validate packages. Guests rarely
475
491
  | `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
492
  | `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
493
  | `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 |
494
+ | `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
495
  | `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
496
  | `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
497
  | `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
@@ -33,6 +33,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
33
33
  - `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
34
  - `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
35
  - `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.
36
+ - `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.stopBackgroundShell` writes the agent's cancellation note before it kills the command and kills nothing when the note fails.
36
37
  - `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
38
 
38
39
  - 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`.
@@ -51,7 +52,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
51
52
  - 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
53
  - `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
54
  - `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).
55
+ - `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
56
  - `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
57
  - `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
58
  - `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.
@@ -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;
@@ -490,10 +491,17 @@ export type HostResultMessage = Envelope & {
490
491
  error: string;
491
492
  code: HostRequestErrorCode;
492
493
  });
494
+ export type HostShellsMessage = Envelope & {
495
+ type: 'shells';
496
+ payload: {
497
+ subscriptionId: string;
498
+ snapshot: GuestRunningShellsSnapshot;
499
+ };
500
+ };
493
501
  export type HostMessage = (Envelope & {
494
502
  type: 'workspace';
495
503
  payload: GuestWorkspaceUpdate;
496
- }) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostStatusControlEventMessage | HostPopoverClosedMessage | HostResolveMessage | HostActionMessage | HostFileOpenMessage | HostFileSnapshotMessage | HostFileSavedMessage | HostResultMessage;
504
+ }) | HostShellsMessage | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostStatusControlEventMessage | HostPopoverClosedMessage | HostResolveMessage | HostActionMessage | HostFileOpenMessage | HostFileSnapshotMessage | HostFileSavedMessage | HostResultMessage;
497
505
  type GuestCall<Type extends string, Payload = never> = Envelope & {
498
506
  type: Type;
499
507
  id: string;
@@ -577,9 +585,17 @@ export type GuestFileSaveMessage = Envelope & {
577
585
  export type GuestFileUnsupportedMessage = Envelope & {
578
586
  type: 'file-unsupported';
579
587
  };
588
+ export type GuestShellsSubscribeMessage = GuestCall<'shells-subscribe', GuestShellsSubscription>;
589
+ export type GuestShellsUnsubscribeMessage = GuestCall<'shells-unsubscribe', {
590
+ subscriptionId: string;
591
+ }>;
592
+ export type GuestShellOutputMessage = GuestCall<'shell-output', GuestShellOutputRequest>;
593
+ export type GuestShellStopMessage = GuestCall<'shell-stop', {
594
+ shellId: string;
595
+ }>;
580
596
  export type GuestMessage = GuestCall<'workspace-read', GuestWorkspaceQuery> | GuestCall<'workspace-subscribe', GuestWorkspaceSubscription> | GuestCall<'workspace-unsubscribe', {
581
597
  subscriptionId: string;
582
- }> | GuestCall<'storage', GuestStorageRequest> | GuestCall<'open-session', {
598
+ }> | GuestShellsSubscribeMessage | GuestShellsUnsubscribeMessage | GuestShellOutputMessage | GuestShellStopMessage | GuestCall<'storage', GuestStorageRequest> | GuestCall<'open-session', {
583
599
  sessionId: string;
584
600
  }> | 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
601
  export declare const isServiceStatusResult: (value: HostResultPayload | undefined) => value is ServiceStatusResult;
package/dist/contract.js CHANGED
@@ -210,6 +210,7 @@ export const isFileStatResult = (value) => Boolean(value && 'kind' in value && '
210
210
  export const isGenerateResult = (value) => Boolean(value && 'text' in value && String(value.text) === value.text && !('status' in value));
211
211
  const HOST_PUSH_TYPES = new Set([
212
212
  'workspace',
213
+ 'shells',
213
214
  'ready', 'directory', 'session', 'connection', 'settings', 'session-lifecycle', 'item', 'resolve', 'action',
214
215
  'status-control-event', 'popover-closed',
215
216
  '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";
@@ -92,6 +92,48 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
92
92
  }, z.core.$strip>;
93
93
  channel: z.ZodLiteral<"openchamber.sdk">;
94
94
  v: z.ZodLiteral<1>;
95
+ }, z.core.$strip>, z.ZodObject<{
96
+ type: z.ZodLiteral<"shells">;
97
+ payload: z.ZodObject<{
98
+ subscriptionId: z.ZodString;
99
+ snapshot: z.ZodObject<{
100
+ kind: z.ZodLiteral<"shells">;
101
+ scope: z.ZodDiscriminatedUnion<[z.ZodObject<{
102
+ kind: z.ZodLiteral<"session">;
103
+ sessionId: z.ZodString;
104
+ }, z.core.$strict>, z.ZodObject<{
105
+ kind: z.ZodLiteral<"project">;
106
+ projectId: z.ZodString;
107
+ }, z.core.$strict>, z.ZodObject<{
108
+ kind: z.ZodLiteral<"global">;
109
+ }, z.core.$strict>], "kind">;
110
+ shells: z.ZodArray<z.ZodObject<{
111
+ id: z.ZodString;
112
+ sessionID: z.ZodString;
113
+ command: z.ZodString;
114
+ startedAt: z.ZodNumber;
115
+ background: z.ZodBoolean;
116
+ }, z.core.$strict>>;
117
+ ended: z.ZodArray<z.ZodObject<{
118
+ id: z.ZodString;
119
+ sessionID: z.ZodString;
120
+ command: z.ZodString;
121
+ startedAt: z.ZodNumber;
122
+ background: z.ZodBoolean;
123
+ status: z.ZodEnum<{
124
+ exited: "exited";
125
+ timeout: "timeout";
126
+ killed: "killed";
127
+ stopped: "stopped";
128
+ unknown: "unknown";
129
+ }>;
130
+ exit: z.ZodOptional<z.ZodNumber>;
131
+ endedAt: z.ZodNumber;
132
+ }, z.core.$strict>>;
133
+ }, z.core.$strict>;
134
+ }, z.core.$strip>;
135
+ channel: z.ZodLiteral<"openchamber.sdk">;
136
+ v: z.ZodLiteral<1>;
95
137
  }, z.core.$strip>, z.ZodObject<{
96
138
  type: z.ZodLiteral<"ready">;
97
139
  payload: z.ZodObject<{
@@ -459,7 +501,13 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
459
501
  data: z.ZodOptional<z.ZodJSONSchema>;
460
502
  }, z.core.$strip>>;
461
503
  }, z.core.$strip>>;
462
- }, z.core.$strip>], "kind">, z.ZodUnion<readonly [z.ZodObject<{
504
+ }, z.core.$strip>], "kind">, z.ZodObject<{
505
+ output: z.ZodString;
506
+ cursor: z.ZodNumber;
507
+ skipped: z.ZodBoolean;
508
+ }, z.core.$strip>, z.ZodObject<{
509
+ stopped: z.ZodLiteral<true>;
510
+ }, z.core.$strip>, z.ZodUnion<readonly [z.ZodObject<{
463
511
  sessionId: z.ZodString;
464
512
  sent: z.ZodEnum<{
465
513
  failed: "failed";
@@ -511,9 +559,9 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
511
559
  }>;
512
560
  }, z.core.$strip>, z.ZodObject<{
513
561
  status: z.ZodEnum<{
562
+ stopped: "stopped";
514
563
  ready: "ready";
515
564
  failed: "failed";
516
- stopped: "stopped";
517
565
  starting: "starting";
518
566
  }>;
519
567
  }, z.core.$strip>, z.ZodObject<{
@@ -641,7 +689,7 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
641
689
  } | {
642
690
  sent: "failed" | "sent" | "no-model" | "skipped";
643
691
  } | {
644
- status: "ready" | "failed" | "stopped" | "starting";
692
+ status: "stopped" | "ready" | "failed" | "starting";
645
693
  } | {
646
694
  content: string;
647
695
  } | {
@@ -657,6 +705,12 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
657
705
  mtime: number;
658
706
  } | {
659
707
  text: string;
708
+ } | {
709
+ output: string;
710
+ cursor: number;
711
+ skipped: boolean;
712
+ } | {
713
+ stopped: true;
660
714
  };
661
715
  error?: undefined;
662
716
  code?: undefined;
@@ -778,7 +832,7 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
778
832
  } | {
779
833
  sent: "failed" | "sent" | "no-model" | "skipped";
780
834
  } | {
781
- status: "ready" | "failed" | "stopped" | "starting";
835
+ status: "stopped" | "ready" | "failed" | "starting";
782
836
  } | {
783
837
  content: string;
784
838
  } | {
@@ -794,6 +848,12 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
794
848
  mtime: number;
795
849
  } | {
796
850
  text: string;
851
+ } | {
852
+ output: string;
853
+ cursor: number;
854
+ skipped: boolean;
855
+ } | {
856
+ stopped: true;
797
857
  } | undefined;
798
858
  }>>, z.ZodObject<{
799
859
  type: z.ZodLiteral<"action">;
@@ -915,6 +975,49 @@ export declare const guestMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
915
975
  }, z.core.$strip>;
916
976
  channel: z.ZodLiteral<"openchamber.sdk">;
917
977
  v: z.ZodLiteral<1>;
978
+ }, z.core.$strip>, z.ZodObject<{
979
+ type: z.ZodLiteral<"shells-subscribe">;
980
+ id: z.ZodString;
981
+ payload: z.ZodObject<{
982
+ subscriptionId: z.ZodString;
983
+ scope: z.ZodDiscriminatedUnion<[z.ZodObject<{
984
+ kind: z.ZodLiteral<"session">;
985
+ sessionId: z.ZodString;
986
+ }, z.core.$strict>, z.ZodObject<{
987
+ kind: z.ZodLiteral<"project">;
988
+ projectId: z.ZodString;
989
+ }, z.core.$strict>, z.ZodObject<{
990
+ kind: z.ZodLiteral<"global">;
991
+ }, z.core.$strict>], "kind">;
992
+ }, z.core.$strict>;
993
+ channel: z.ZodLiteral<"openchamber.sdk">;
994
+ v: z.ZodLiteral<1>;
995
+ }, z.core.$strip>, z.ZodObject<{
996
+ type: z.ZodLiteral<"shells-unsubscribe">;
997
+ id: z.ZodString;
998
+ payload: z.ZodObject<{
999
+ subscriptionId: z.ZodString;
1000
+ }, z.core.$strict>;
1001
+ channel: z.ZodLiteral<"openchamber.sdk">;
1002
+ v: z.ZodLiteral<1>;
1003
+ }, z.core.$strip>, z.ZodObject<{
1004
+ type: z.ZodLiteral<"shell-output">;
1005
+ id: z.ZodString;
1006
+ payload: z.ZodObject<{
1007
+ shellId: z.ZodString;
1008
+ cursor: z.ZodOptional<z.ZodNumber>;
1009
+ tailBytes: z.ZodOptional<z.ZodNumber>;
1010
+ }, z.core.$strict>;
1011
+ channel: z.ZodLiteral<"openchamber.sdk">;
1012
+ v: z.ZodLiteral<1>;
1013
+ }, z.core.$strip>, z.ZodObject<{
1014
+ type: z.ZodLiteral<"shell-stop">;
1015
+ id: z.ZodString;
1016
+ payload: z.ZodObject<{
1017
+ shellId: z.ZodString;
1018
+ }, z.core.$strict>;
1019
+ channel: z.ZodLiteral<"openchamber.sdk">;
1020
+ v: z.ZodLiteral<1>;
918
1021
  }, z.core.$strip>, z.ZodObject<{
919
1022
  type: z.ZodLiteral<"storage">;
920
1023
  id: z.ZodString;
package/dist/protocol.js CHANGED
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { guestSessionWorktreeSchema, guestStorageRequestSchema, guestStorageResultSchema, guestWorkspaceQuerySchema, guestWorkspaceSnapshotSchema } from "./workspace-schemas.js";
3
3
  import { guestStatusControlEventSchema, guestStatusControlsSchema } from "./status-control-schemas.js";
4
4
  import { guestPopoverClosedEventSchema, guestPopoverDataSchema, guestPopoverRequestSchema } from "./popover-schemas.js";
5
+ import { GUEST_SHELL_END_STATUSES, GUEST_SHELL_ID_MAX, GUEST_SHELLS_MAX, GUEST_SHELL_OUTPUT_TAIL_MAX } from "./shells.js";
5
6
  import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from "./api-version.js";
6
7
  import { SERVICE_STATUS_VALUES, ATTACH_PROVIDER_ID, GUEST_ACCOUNT_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_COMMIT_SHA, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_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_COMPOSE_TEXT_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_FILE_PATH_MAX, GUEST_FILE_STAT_KINDS, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_SETTING_VALUE_MAX, GUEST_TOAST_MAX, SESSION_LIFECYCLE_PHASES, SETTING_KEY, START_SESSION_SENT, isGuestFilePath, isGuestRequestPath, resolveHostRequestErrorCode, } from "./contract.js";
7
8
  import { GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_VERSION_MAX } from "./file-editor.js";
@@ -98,9 +99,19 @@ const fileStatResultPayloadSchema = z.object({
98
99
  const generateResultPayloadSchema = z.object({
99
100
  text: z.string().max(GUEST_GENERATE_TEXT_MAX),
100
101
  });
102
+ const shellOutputResultPayloadSchema = z.object({
103
+ output: z.string(),
104
+ cursor: z.number(),
105
+ skipped: z.boolean(),
106
+ });
107
+ const shellStopResultPayloadSchema = z.object({
108
+ stopped: z.literal(true),
109
+ });
101
110
  const hostResultPayloadSchema = z.union([
102
111
  guestStorageResultSchema,
103
112
  guestWorkspaceSnapshotSchema,
113
+ shellOutputResultPayloadSchema,
114
+ shellStopResultPayloadSchema,
104
115
  startSessionResultPayloadSchema,
105
116
  requestResultPayloadSchema,
106
117
  promptResultPayloadSchema,
@@ -214,8 +225,36 @@ const hostResultSchema = z.object({
214
225
  code: resolveHostRequestErrorCode(message.code),
215
226
  };
216
227
  });
228
+ const guestRunningShellSchema = z.object({
229
+ id: z.string().min(1).max(GUEST_SHELL_ID_MAX),
230
+ sessionID: z.string().min(1).max(1024),
231
+ command: z.string(),
232
+ startedAt: z.number().nonnegative(),
233
+ background: z.boolean(),
234
+ }).strict();
235
+ const guestEndedShellSchema = guestRunningShellSchema.extend({
236
+ status: z.enum(GUEST_SHELL_END_STATUSES),
237
+ exit: z.number().int().optional(),
238
+ endedAt: z.number().nonnegative(),
239
+ }).strict();
240
+ const guestShellsScopeSchema = z.discriminatedUnion('kind', [
241
+ z.object({ kind: z.literal('session'), sessionId: z.string().min(1).max(1024) }).strict(),
242
+ z.object({ kind: z.literal('project'), projectId: z.string().min(1).max(1024) }).strict(),
243
+ z.object({ kind: z.literal('global') }).strict(),
244
+ ]);
245
+ const guestRunningShellsSnapshotSchema = z.object({
246
+ kind: z.literal('shells'),
247
+ scope: guestShellsScopeSchema,
248
+ shells: z.array(guestRunningShellSchema).max(GUEST_SHELLS_MAX),
249
+ ended: z.array(guestEndedShellSchema).max(GUEST_SHELLS_MAX),
250
+ }).strict();
217
251
  export const hostMessageSchema = z.union([
218
252
  z.object({ ...envelope, type: z.literal('workspace'), payload: z.object({ subscriptionId: z.string().min(1).max(128), snapshot: guestWorkspaceSnapshotSchema }) }),
253
+ z.object({
254
+ ...envelope,
255
+ type: z.literal('shells'),
256
+ payload: z.object({ subscriptionId: z.string().min(1).max(128), snapshot: guestRunningShellsSnapshotSchema }),
257
+ }),
219
258
  z.object({
220
259
  ...envelope,
221
260
  type: z.literal('ready'),
@@ -332,6 +371,10 @@ export const guestMessageSchema = z.discriminatedUnion('type', [
332
371
  z.object({ ...envelope, type: z.literal('workspace-read'), id: z.string().min(1), payload: guestWorkspaceQuerySchema }),
333
372
  z.object({ ...envelope, type: z.literal('workspace-subscribe'), id: z.string().min(1), payload: z.object({ subscriptionId: z.string().min(1).max(128), query: guestWorkspaceQuerySchema }) }),
334
373
  z.object({ ...envelope, type: z.literal('workspace-unsubscribe'), id: z.string().min(1), payload: z.object({ subscriptionId: z.string().min(1).max(128) }) }),
374
+ z.object({ ...envelope, type: z.literal('shells-subscribe'), id: z.string().min(1), payload: z.object({ subscriptionId: z.string().min(1).max(128), scope: guestShellsScopeSchema }).strict() }),
375
+ z.object({ ...envelope, type: z.literal('shells-unsubscribe'), id: z.string().min(1), payload: z.object({ subscriptionId: z.string().min(1).max(128) }).strict() }),
376
+ z.object({ ...envelope, type: z.literal('shell-output'), id: z.string().min(1), payload: z.object({ shellId: z.string().min(1).max(GUEST_SHELL_ID_MAX), cursor: z.number().int().nonnegative().optional(), tailBytes: z.number().int().positive().max(GUEST_SHELL_OUTPUT_TAIL_MAX).optional() }).strict() }),
377
+ z.object({ ...envelope, type: z.literal('shell-stop'), id: z.string().min(1), payload: z.object({ shellId: z.string().min(1).max(GUEST_SHELL_ID_MAX) }).strict() }),
335
378
  z.object({ ...envelope, type: z.literal('storage'), id: z.string().min(1), payload: guestStorageRequestSchema }),
336
379
  z.object({
337
380
  ...envelope,
@@ -0,0 +1,69 @@
1
+ export declare const GUEST_SHELL_ID_MAX = 128;
2
+ export declare const GUEST_SHELLS_MAX = 200;
3
+ export declare const GUEST_SHELL_OUTPUT_TAIL_MAX = 65536;
4
+ /** A shell command still running on behalf of a session. */
5
+ export type GuestRunningShell = {
6
+ id: string;
7
+ sessionID: string;
8
+ command: string;
9
+ startedAt: number;
10
+ /** True for a job the turn does not wait for; false while the turn that ran it is blocked on it. */
11
+ background: boolean;
12
+ };
13
+ export declare const GUEST_SHELL_END_STATUSES: readonly ["exited", "timeout", "killed", "stopped", "unknown"];
14
+ /**
15
+ * How a shell command ended: `exited` on its own (see `exit`), `timeout`,
16
+ * `killed` by a signal, `stopped` by a user (Stop, or aborting the turn that
17
+ * waited on it), or `unknown` when
18
+ * OpenChamber missed the end (a reconnect) and only saw the command gone.
19
+ */
20
+ export type GuestShellEndStatus = (typeof GUEST_SHELL_END_STATUSES)[number];
21
+ /** A shell command OpenChamber saw running that has since ended. */
22
+ export type GuestEndedShell = GuestRunningShell & {
23
+ status: GuestShellEndStatus;
24
+ /** Exit code, when the command exited and OpenCode reported one. */
25
+ exit?: number;
26
+ endedAt: number;
27
+ };
28
+ /**
29
+ * Which running shells a subscription covers: one session and its subagents,
30
+ * every session of a registered project, or every session the app sees.
31
+ */
32
+ export type GuestShellsScope = {
33
+ kind: 'session';
34
+ sessionId: string;
35
+ } | {
36
+ kind: 'project';
37
+ projectId: string;
38
+ } | {
39
+ kind: 'global';
40
+ };
41
+ export type GuestRunningShellsSnapshot = {
42
+ kind: 'shells';
43
+ /** Echo of the scope the subscription asked for. */
44
+ scope: GuestShellsScope;
45
+ shells: GuestRunningShell[];
46
+ /**
47
+ * Commands in scope that ended since OpenChamber started watching, oldest
48
+ * end first. Kept in memory only: empty after an app reload or runtime
49
+ * switch, and capped at `GUEST_SHELLS_MAX`, newest kept.
50
+ */
51
+ ended: GuestEndedShell[];
52
+ };
53
+ export type GuestShellsSubscription = {
54
+ subscriptionId: string;
55
+ scope: GuestShellsScope;
56
+ };
57
+ export type GuestShellOutputRequest = {
58
+ shellId: string;
59
+ cursor?: number;
60
+ tailBytes?: number;
61
+ };
62
+ export type GuestShellOutputResult = {
63
+ output: string;
64
+ cursor: number;
65
+ skipped: boolean;
66
+ };
67
+ export type GuestShellStopResult = {
68
+ stopped: true;
69
+ };
package/dist/shells.js ADDED
@@ -0,0 +1,4 @@
1
+ export const GUEST_SHELL_ID_MAX = 128;
2
+ export const GUEST_SHELLS_MAX = 200;
3
+ export const GUEST_SHELL_OUTPUT_TAIL_MAX = 65536;
4
+ export const GUEST_SHELL_END_STATUSES = ['exited', 'timeout', 'killed', 'stopped', 'unknown'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchamber/sdk",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Contract for OpenChamber guests: manifest parse and iframe host bridge.",
5
5
  "private": false,
6
6
  "license": "MIT",