@openchamber/sdk 1.24.0 → 1.24.1
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 +34 -7
- package/DOCUMENTATION.md +29 -3
- package/README.md +59 -3
- package/dist/contract.d.ts +30 -3
- package/dist/contract.js +1 -1
- package/dist/host.d.ts +7 -1
- package/dist/host.js +41 -8
- package/dist/index.d.ts +2 -2
- package/dist/manifest.d.ts +13 -8
- package/dist/manifest.js +2 -4
- package/dist/parse.d.ts +14 -0
- package/dist/parse.js +27 -8
- package/dist/protocol.d.ts +52 -1
- package/dist/protocol.js +19 -1
- package/dist/ui/menu.js +5 -1
- package/dist/ui/select.js +5 -1
- package/package.json +1 -1
package/API.md
CHANGED
|
@@ -25,8 +25,8 @@ Two entrypoints:
|
|
|
25
25
|
| Must exist | When | Failure code |
|
|
26
26
|
| -------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------ |
|
|
27
27
|
| Semver `version` on `package.json` (`1.0.0`) | Always on install | `invalid-manifest` |
|
|
28
|
-
| `panel.entry`
|
|
29
|
-
| Every relative `<script src="…">` `.js` from that HTML
|
|
28
|
+
| `panel.entry` or `background.entry` HTML file | Every declared entry | `invalid-manifest` |
|
|
29
|
+
| Every relative `<script src="…">` `.js` from that HTML | Every declared entry | `missing-build` |
|
|
30
30
|
| File named by `panel.icon` | Only when icon ends in `.svg` (e.g. `icon.svg`) | `invalid-manifest` |
|
|
31
31
|
| File named by `service.entry` (e.g. `service/main.js`) | When `contributes.service` is set | `missing-build` |
|
|
32
32
|
|
|
@@ -73,6 +73,7 @@ Each returns an unsubscribe function. Late subscribers get the last known value
|
|
|
73
73
|
| `onSettings(listener)` | `GuestSettings` | Declared integration fields only (`Record<string, string>`) |
|
|
74
74
|
| `onItem(listener)` | `GuestItem | null` | The item this surface was opened for: the chip (`AttachIssueRequest`), a message (`GuestMessageItem`), or a session (`GuestSessionItem`); `null` from the rail icon or + menu |
|
|
75
75
|
| `onResolve(handler)` | `{ command, args }` → `Promise<AttachIssueRequest \| null>` | Answers a `contributes.commands` slash command. Return the chip to attach, `null` for nothing (the user sees a short notice), or throw (the message reaches the user). One handler at a time |
|
|
76
|
+
| `onAction(handler)` | `GuestActionItem` → `void \| Promise<void>` | Runs a `mode: "background"` message or session action. Register synchronously after `connectHost`. Await every operation; the frame is removed when the handler settles. Throw to report an error. One handler at a time; returns an unsubscribe function |
|
|
76
77
|
|
|
77
78
|
|
|
78
79
|
`HostReadyContext`
|
|
@@ -85,7 +86,7 @@ Each returns an unsubscribe function. Late subscribers get the last known value
|
|
|
85
86
|
| `locale` | `string` | Host language tag |
|
|
86
87
|
| `directory` | `string | null` |
|
|
87
88
|
| `session` | snapshot or `null` | Title falls back to `id`. `busy` is live status. `model` is `providerID/id` when present |
|
|
88
|
-
| `surface`
|
|
89
|
+
| `surface` | `'panel' \| 'dialog' \| 'page' \| 'background'` | Where the host mounted this frame |
|
|
89
90
|
| `connection` | `{ connected, account }` | Integration link state |
|
|
90
91
|
| `settings` | `Record<string, string>` | Declared keys only |
|
|
91
92
|
| `item` | `GuestItem | null` | Set when the user clicked this guest's chip on the composer, or ran one of this guest's `contributes.actions`. Narrow with `isGuestMessageItem` / `isGuestSessionItem` / `isGuestAttachItem` |
|
|
@@ -130,7 +131,7 @@ Access tokens never appear in `ready` or in request results.
|
|
|
130
131
|
|
|
131
132
|
| Method | Arguments | Returns | Behavior |
|
|
132
133
|
| ----------------- | --------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
133
|
-
| `toast`
|
|
134
|
+
| `toast` | `{ kind: 'info' \| 'success' \| 'error', message, copy?, dismiss?, persistent? }` | `Promise<void>` | Show a toast, optionally with host-owned Copy and OK buttons; see Toast buttons below |
|
|
134
135
|
| `openUrl` | `url: string` | `Promise<void>` | Open URL in the host |
|
|
135
136
|
| `openSurface` | `surfaceId: string` | `Promise<void>` | Switch host chrome to that surface |
|
|
136
137
|
| `writeClipboard` | `text: string` | `Promise<void>` | Copy in the host (1–32000 chars) |
|
|
@@ -216,6 +217,30 @@ Storage belongs to the extension on the connected server and needs no extra capa
|
|
|
216
217
|
|
|
217
218
|
`request` **/** `serviceRequest` **rules:** `method` is `GET` | `POST` | `PUT` | `PATCH` | `DELETE`. `path` must start with `/`, no scheme, stay on the declared origin (cloud API or service loopback). Guest parses `body` as JSON when needed.
|
|
218
219
|
|
|
220
|
+
### Toast buttons
|
|
221
|
+
|
|
222
|
+
`ToastRequest.copy` is an optional boolean or `{ text: string }`. `true` copies the displayed message; an object supplies the clipboard value. `false` or omission adds no Copy button. The custom text must contain 1 to `GUEST_CLIPBOARD_TEXT_MAX` characters, with whitespace preserved. The displayed message is trimmed and must contain 1 to `GUEST_TOAST_MAX` characters. The client rejects invalid text lengths as `HOST_REJECTED` before sending; the host independently validates the wire payload.
|
|
223
|
+
|
|
224
|
+
`dismiss: true` adds OK. `persistent: true` disables automatic expiry and always adds OK regardless of `dismiss`, so every persistent toast can be closed. Omitted or false `persistent` retains the host's usual duration.
|
|
225
|
+
|
|
226
|
+
Copy leaves the toast open, reports success on the button, and shows a retryable error when the clipboard fails. OK dismisses only its own toast. Buttons use the host's locale and clipboard helper. They retain only the supplied text and toast identity, work after guest disposal or a runtime switch, and never call back into the extension. The `toast` promise resolves after display acknowledgement, without waiting for a click. Web and desktop use the same behavior; extension support on other runtimes is unchanged.
|
|
227
|
+
|
|
228
|
+
### Background actions
|
|
229
|
+
|
|
230
|
+
`contributes.actions[].mode` accepts `"open"` or `"background"`. Omitted mode keeps the existing panel/dialog routing and `onItem` delivery. Background actions load `background.entry` when declared, otherwise `panel.entry`. They never use separate attach-dialog HTML. They run on web and desktop, including connections through the private relay; VS Code and mobile still do not load extensions.
|
|
231
|
+
|
|
232
|
+
`contributes.background` is `{ entry: "<package-local .html>" }`. It starts on demand for an action or command and adds no visible UI. With a background entry, `panel.entry` may be omitted: there is no rail icon, attach picker, or full-screen page, but background actions, slash commands, storage and granted APIs work. Identity remains in `panel.id/name/icon`. Open-mode actions, `page`, and enabled `attach` require `panel.entry`; invalid combinations fail as `invalid-panel`. Without either entry, the extension remains tools-only. Invalid background paths or a missing entry field fail as `invalid-background`.
|
|
233
|
+
|
|
234
|
+
When both entries exist, the visible panel cannot register the slash-command resolver: the hidden background entry handles `onResolve` and receives `surface: "background"`. Existing panel-only commands retain their current behavior. Clicking an attached chip from a background-only extension shows a no-panel notice; its browser action remains available.
|
|
235
|
+
|
|
236
|
+
Every background click mounts a fresh sandboxed iframe and sends one `action` request with an invocation `id` and a `GuestActionItem` payload. `host.onAction` receives that message or session item and sends `action-result` with `{ ok: true }` when the handler finishes, or `{ ok: false, error }` when it throws. The error is limited to `GUEST_RESOLVE_ERROR_MAX` characters. The host validates the reply and accepts only the matching invocation from that frame. It sends no acknowledgement for `action-result`.
|
|
237
|
+
|
|
238
|
+
`ready.surface` is `"background"` and `ready.item` is `null`. Context updates never replay the action. The session and directory context remain scoped to the clicked target; the item itself is the snapshot captured at click time. Calls that explicitly write the composer still write the currently visible composer. `close()` is a no-op in a background frame; returning from the handler completes the action.
|
|
239
|
+
|
|
240
|
+
The 20-second deadline covers loading and execution. At most eight background invocations may run concurrently. Completion, failure, timeout, disabling, lost approval, extension update/removal, runtime switch, and host unmount release the frame and its listeners. Late messages are ignored. Already completed or server-accepted effects are not undone, and the host does not automatically retry actions. A background action has the same declared capabilities as its extension. It does not hide the extension's rail icon.
|
|
241
|
+
|
|
242
|
+
See [Actions without opening a panel](./README.md#actions-without-opening-a-panel) for a toast example.
|
|
243
|
+
|
|
219
244
|
### 1.3 Error codes (`HostRequestError.code`)
|
|
220
245
|
|
|
221
246
|
|
|
@@ -259,6 +284,7 @@ Storage belongs to the extension on the connected server and needs no extra capa
|
|
|
259
284
|
| Request response | 256 000 |
|
|
260
285
|
| Request timeout | 20 000 ms |
|
|
261
286
|
| `resolve` answer (host waits) | 20 000 ms |
|
|
287
|
+
| Background action loading and execution | 20 000 ms |
|
|
262
288
|
| Badge count | 999 |
|
|
263
289
|
| Message item `text` | 200 000 |
|
|
264
290
|
| Session item (serialized) | 2 000 000 |
|
|
@@ -400,10 +426,11 @@ Used by the OpenChamber host and by tools that validate packages. Guests rarely
|
|
|
400
426
|
| `engines.openchamber` | Optional. Only `1.22.0` or `>=1.22.0`. Older host → `host-too-old` |
|
|
401
427
|
| `panel.id` | kebab-case |
|
|
402
428
|
| `panel.icon` | Remixicon kebab name (`window`) **or** package `.svg` path. Remixicon needs no file. An `.svg` path must exist on disk or install fails (`invalid-manifest`). No URLs/absolute paths |
|
|
403
|
-
| `panel.entry`
|
|
429
|
+
| `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 |
|
|
430
|
+
| `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` |
|
|
404
431
|
| `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` |
|
|
405
432
|
| `capabilities` | Optional list of `prompt`, `sessions`, `files`, `model`. `files` is read **and** write inside the open project; `model` is `generate`. Approved once at install |
|
|
406
|
-
| `actions`
|
|
433
|
+
| `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 |
|
|
407
434
|
| `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 |
|
|
408
435
|
| `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 |
|
|
409
436
|
| `filesystem` | Optional, 1–16 globs, each 1–256 chars, starting with `/` or `~/`; `**` spans folders, `*` / `?` stay in one segment; no `..`, empty segment, or backslash (`invalid-filesystem`). Declaring it adds the `filesystem` capability and the dialog lists the globs |
|
|
@@ -422,7 +449,7 @@ Extra keys are dropped, not forwarded.
|
|
|
422
449
|
| `parseManifestJson(json)` (`@openchamber/sdk/schemas`) | String → same result |
|
|
423
450
|
| `resolveAttachMode(attach)` | Normalize to `'panel' |
|
|
424
451
|
| `resolveAttachEntry(contributes)` | Dialog page from the object form, or `null` when the dialog reuses `panel.entry` |
|
|
425
|
-
| `hasGuestPage(contributes)`
|
|
452
|
+
| `hasGuestPage(contributes)` | `true` when `panel.entry` is set; background-only and tools-only packages return `false` |
|
|
426
453
|
| `resolveIntegrationAuth` / `resolveIntegrationApi` | Auth kind and API origin |
|
|
427
454
|
| `toPublicIntegration` / `toPublicService` | Catalog-safe public slices |
|
|
428
455
|
| `isGuestPackageSvgIcon` | Whether icon is a package SVG path |
|
package/DOCUMENTATION.md
CHANGED
|
@@ -20,6 +20,8 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
20
20
|
|
|
21
21
|
## Invariants
|
|
22
22
|
|
|
23
|
+
- `scripts/bump-version.mjs` refreshes `bun.lock` after bumping package versions; include both in the release commit. Bun resolves `workspace:*` in published archives from lockfile versions, and a frozen install can succeed with stale versions. `scripts/bump-version.test.mjs` covers the bump, frozen install, and packed dependency versions together.
|
|
24
|
+
|
|
23
25
|
- Extension activity uses `useConfigStore.isConnected` plus live status records and directory `sessionStatusReady` authority. `useUIStore.eventStreamStatus` is an unmaintained diagnostic field, not a connection gate; its default `idle` must not hide known running or idle sessions as `unknown`.
|
|
24
26
|
|
|
25
27
|
- `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.
|
|
@@ -27,7 +29,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
27
29
|
- `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. Limits and public result shapes live in `API.md`. For project data, extensions namespace keys with project IDs.
|
|
28
30
|
|
|
29
31
|
- 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`.
|
|
30
|
-
- `panel.entry` is optional
|
|
32
|
+
- `panel.entry` is optional; `panel.id/name/icon` remain the identity on Settings and approval cards. `hasGuestPage` means a visible panel, not an execution runtime. `background.entry` provides independent package-local `.html` for on-demand code without a rail icon. Without a panel, actions must explicitly use `mode: "background"`; `page` and enabled `attach` fail as `invalid-panel`. A background-only extension can declare commands, services, integrations and capabilities, with the existing grants. Without either entry, only tools are allowed, HTML/JS serving is refused, and `examples/tools-only` remains the reference. Settings shows "No panel" whenever the visible entry is absent.
|
|
31
33
|
- Panel `id` is kebab-case. `icon` is a Remixicon kebab-case name (`RiWindowLine` → `window`, `RiGitMergeLine` → `git-merge`) or a package `.svg` path (`icon.svg`). URLs and absolute paths fail parse. `entry` stays inside the package (no `..`, no absolute, no URL). The rail paints a Remixicon from the host sprite, or a `currentColor` mask for a package SVG. Package SVGs often fill the 24×24 viewBox; the rail scales that mask so it matches Remixicon optical size. A Remixicon name the sprite does not carry falls back to `apps`.
|
|
32
34
|
- Extra manifest keys are dropped, not forwarded.
|
|
33
35
|
- Theme text fields are required on `apiVersion: 1`: `primaryText`, `successText`, `warningText`, `errorText`, `infoText`. Host calculation and UI-kit application are documented in `src/ui/DOCUMENTATION.md`. Keep the wire schema, TypeScript contract and iframe mapping in sync.
|
|
@@ -46,7 +48,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
46
48
|
- `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.
|
|
47
49
|
- `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.
|
|
48
50
|
- `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.
|
|
49
|
-
- `contributes.integration`
|
|
51
|
+
- `contributes.integration` needs either a panel or background entry. Exactly one of `oauth`, `token`, or `host` is required. The public catalog slice is name, description, `auth`, `apiOrigin` (the one origin `request` may call; the approval dialog shows it, it is not a secret), token scheme, and setting field defs. OAuth URLs stay on the server. Client id, secret, and pasted tokens are not in the package. `host.provider: "linear"` binds the first-party Linear connection. `apiOrigin` is `https://api.linear.app` only.
|
|
50
52
|
- `attach` puts `{ providerId, id, title, url }` on the composer as a chip, exclusive with GitHub and Linear. `kind` is `issue` or `pull` and defaults to `issue`. `author` is a login. `branches` is `{ head, base }` on a pull. Avatar URLs stay off this payload. It does not invent a GitHub `number`. `text` is optional model context, 1–16000 after trim. `data` is optional plain JSON the guest chose (`JsonValue`); the host stores it on the chip, the queued context part, and the session snapshot, and hands it back byte-identical as `ready.item.data` / `onItem`. It never reaches the model or the chip text. Serialized size is capped at `GUEST_ATTACH_DATA_MAX` (16 000 characters): `clampAttachRequest` drops a larger value, the host schema refuses it. `connectHost.attach` clamps id/title/url/text/author/branches to the protocol max so the host schema does not drop the message. The rail and the attach window both write `pendingGuestIssue` on the composer store. Send writes `guest-issue` or `guest-pr` and a session snapshot. `close` dismisses the attach window and is a no-op on the rail.
|
|
51
53
|
- `startSession` extends the existing operation with `projectId`, root/existing/new worktree selection, optional new name/base, and `navigation` defaulting to `preserve`. It calls the owning session creation action with that navigation policy, so registration/message initialization/global cache updates still happen while selection stays unchanged. Worktree creation reuses `createWorktreeWithDefaults` and the configured bootstrap wait. It preserves a created worktree on bootstrap/session failure and returns that partial result. It captures send selection before async creation, rechecks runtime and grants before subsequent mutations, and distinguishes session creation from first-message and linked-item persistence outcomes. See `API.md` for the exact result. Web and desktop only.
|
|
52
54
|
- `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.
|
|
@@ -81,12 +83,36 @@ File calls go through `POST /api/guests/:id/files` with the `x-opencode-director
|
|
|
81
83
|
|
|
82
84
|
Clipboard writes run in the host (`copyTextToClipboard`). Compose uses `useInputStore.setPendingInputText`. Attach writes the composer chip. The iframe has no clipboard, no composer, and no chip. The + menu and the attach window chrome are host. Guest JS is not in that React tree.
|
|
83
85
|
|
|
84
|
-
Actions, commands, and the badge are host chrome too. `useGuestActions` (`packages/ui/src/hooks/useGuestSurfaces.ts`) lists
|
|
86
|
+
Actions, commands, and the badge are host chrome too. `useGuestActions` (`packages/ui/src/hooks/useGuestSurfaces.ts`) lists active guests' actions for message and session menus, empty on VS Code and mobile. A click builds the item with `buildGuestMessageItem` / `buildGuestSessionItem`; failed conversation loading is a toast, never an empty conversation. `runGuestAction` dispatches the declared mode. In default `open` mode, `openGuestWithItem` routes like a chip click: the attach window for `attach: "dialog"` through `useGuestDialogStore`, otherwise the rail through `useGuestItemStore`. Background mode is documented below.
|
|
87
|
+
|
|
88
|
+
`useGuestCommands` lists commands minus reserved names. The composer routes `/name args` through `submit/guestCommands.ts` to `runGuestCommand`, which asks a mounted pane's registered resolver or mounts a hidden `PluginPane` through `GuestHosts`. The pane registers after hello/load, posts `resolve`, and settles on `resolve-result` or the 20s deadline. A returned chip uses `useInputStore.setPendingGuestIssue`. `badge` writes the memory-only `useGuestBadgeStore`; the rail paints the count and opening the panel clears it. A paused or unapproved guest contributes no actions, commands, tool presentations, or badge.
|
|
85
89
|
|
|
86
90
|
Add a type when the host grows that hole, not before. Any new `connectHost` method ships with a `PluginPane` / `answerGuestMessage` branch in the same change.
|
|
87
91
|
|
|
88
92
|
Rebuild every example bundle after an SDK change (`bunx openchamber-guest-bundle` per entry, see `examples/README.md`); the built files are checked in.
|
|
89
93
|
|
|
94
|
+
## Guest toasts
|
|
95
|
+
|
|
96
|
+
`ToastRequest` adds optional `copy: boolean | { text: string }`, `dismiss: boolean`, and `persistent: boolean`. The client checks message/copy length before sending; the host schema validates the complete payload. `HostBridgeEffects.toast` receives the request intact. `lib/guests/toast.tsx` owns toast creation and per-toast dismissal; `components/layout/GuestToastActions.tsx` renders the buttons and clipboard success/failure state. Copy keeps the toast open. Persistent toasts always include OK, including when the guest passes `dismiss: false`.
|
|
97
|
+
|
|
98
|
+
Toast acknowledgement means displayed, not clicked. Button closures retain only text and toast identity, not the guest's lifetime or runtime. They survive action completion, iframe teardown, and runtime switches. Labels resolve through the host's reactive i18n context. The SDK and schema limits use the existing `GUEST_TOAST_MAX` and `GUEST_CLIPBOARD_TEXT_MAX`; custom copy text is not trimmed. See `API.md` for the author contract.
|
|
99
|
+
|
|
100
|
+
## Background actions
|
|
101
|
+
|
|
102
|
+
`contributes.actions[].mode` is `open` or `background`, defaulting to `open`. The manifest parser and UI catalog parser preserve it; server catalog projection already carries actions unchanged. Existing `onItem` routing stays with open-mode actions. `runGuestAction` revalidates the captured declaration, active grants, target role, and runtime before dispatch. Session actions also discard history loaded across a runtime switch.
|
|
103
|
+
|
|
104
|
+
`lib/guests/run-action.ts` owns temporary invocation state, a 20-second loading/execution deadline, and an eight-frame limit. Each click gets a distinct `PluginPane` from `GuestHosts`, preferring `backgroundEntry` and falling back to `entry`, with `surface: 'background'` and `headless`. It never registers as a slash-command resolver, consumes a parked item, clears a badge, opens a tab, or changes session selection. The pane pins session/directory context to the clicked target. Composer-writing methods retain their existing current-composer behavior.
|
|
105
|
+
|
|
106
|
+
The catalog carries `backgroundEntry` independently of the visible `entry`. Both HTML files and their built scripts are checked at install. The serving route's `hasRuntime` gate permits code when either exists; storage applies that same entry check plus enabled/approval gates. Background-only chips cannot reopen a rail panel, so the composer shows a no-panel notice while retaining the existing browser action.
|
|
107
|
+
|
|
108
|
+
Slash commands use the background entry whenever it is declared. A visible pane then does not register a resolver; only the temporary command host does, with `ready.surface: 'background'`. Panel-only extensions retain resolver reuse. Resolver registrations are retired when the frame URL or entry changes and refuse calls after runtime/grant/version changes. `examples/hello-kit/background` is the reference for a separate entry; omit that example's visible entry and page to make it background-only.
|
|
109
|
+
|
|
110
|
+
Both `MainLayout` and the separate-window `MiniChatLayout` mount `GuestHosts`; a message action must have an execution host in the window that owns its catalog and session state.
|
|
111
|
+
|
|
112
|
+
The SDK adds `HostActionMessage`, `GuestActionResultMessage`, `GuestActionItem`, `ActionResultPayload`, and `HostClient.onAction`. Wire `action` carries one message/session item and an invocation ID; `action-result` carries that ID and a discriminated success/error payload. Error text shares `GUEST_RESOLVE_ERROR_MAX`. `ready.item` stays null; a one-shot `takeMessage` prevents hello/load/context updates from executing an action twice. The handler must be registered synchronously and await its work before returning. Host schema validation plus the frame's source and invocation ID reject unrelated replies.
|
|
113
|
+
|
|
114
|
+
The runner cancels on runtime/catalog invalidation and marks the operation inactive synchronously, so queued messages cannot mutate host state during React teardown. Timeout, completion, frame-load failure, or host unmount clears the invocation and subscriptions. Unmount cleanup yields one microtask for Strict Mode's effect reattachment. Already accepted remote effects are not rolled back; no automatic retry. Web/desktop use the existing direct or relay frame loader; VS Code and mobile remain unsupported. `examples/hello-kit` demonstrates a background message-length toast.
|
|
115
|
+
|
|
90
116
|
## Not here yet
|
|
91
117
|
|
|
92
118
|
`issues.search` / `issues.get` on the host, a public OAuth broker, a second `host.provider`, file watching or globbing from the guest (it gets `listDir` and `stat`, not a search). `attach` is the chip. `startSession` creates a session and writes that snapshot. `sessionLink` writes that snapshot on the current session. `prompt` writes or sends on that session. `onSessionLifecycle` is the live phase. Search and the task list live in the guest. `@openchamber/sdk/ui` is the drawing kit: buttons, fields, select, list, tabs, badges, banners, menu, and `mountText`. No issue page, card, attach picker, or PR window; the guest composes those from the blocks. `mountText` lifts `` and `[label](https://…)` and hands link clicks to `onOpenUrl`. `ready.theme.tokens` carries the host surface and interactive colors. Call `applyHostReady` before mount.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @openchamber/sdk
|
|
2
2
|
|
|
3
|
-
Build extensions for [OpenChamber](https://openchamber.dev). An extension
|
|
3
|
+
Build extensions for [OpenChamber](https://openchamber.dev). An extension can show a panel on the right-hand rail or run actions in the background without a panel. It can read the current project and session, show toasts, put text in the chat box, attach a task to a session, and, once the user approves it, start sessions and send prompts. This package is the contract between the extension and the app.
|
|
4
4
|
|
|
5
5
|
Full guide: [Build an extension](https://openchamber.dev/docs/sdk/). Reference: [Host API](https://openchamber.dev/docs/sdk/host/) and [UI kit](https://openchamber.dev/docs/sdk/ui/). Extensions with a local process: [GUEST_SERVICES.md](./GUEST_SERVICES.md).
|
|
6
6
|
|
|
@@ -88,15 +88,71 @@ Git installs also accept SSH addresses such as `git@github.com:owner/extension.g
|
|
|
88
88
|
- `version` is required semver. Settings → Extensions shows it on the card.
|
|
89
89
|
- `apiVersion` is `1`. Anything else is refused.
|
|
90
90
|
- `engines.openchamber` is optional (`1.24.0` or `>=1.24.0`). Older OpenChamber builds refuse the install.
|
|
91
|
-
- `panel.id` is kebab-case and unique. `icon` is a Remixicon name (`RiWindowLine` becomes `window`) or an SVG inside the folder. `entry` is the HTML file
|
|
91
|
+
- `panel.id` is kebab-case and unique. `icon` is a Remixicon name (`RiWindowLine` becomes `window`) or an SVG inside the folder. `entry` is the visible panel's HTML file. Leave `entry` out to have no rail icon or panel. Add `background.entry` for executable actions and commands; with neither entry, the extension can only declare `tools`, as in `examples/tools-only`.
|
|
92
|
+
- `background: { "entry": "background/index.html" }` supplies separate sandboxed HTML for background actions and slash commands. Its scripts must be built. It runs on demand, not continuously or at installation. A panel can coexist with it.
|
|
92
93
|
- `attach` is optional. `"dialog"` opens the page in a window from the + menu next to the chat box; `true` or `"panel"` opens the rail panel instead. `ctx.surface` tells the page which one it is in. The object form `{ "mode": "dialog", "entry": "panel/attach.html" }` gives the window its own page. When the user clicks the attached chip, the page opens again with that item in `ctx.item` (`null` from the + menu), so it can show the item instead of the list.
|
|
93
|
-
- `actions` is optional: menu entries on messages (`where: "message"`, optionally only `roles: ["assistant"]`) and on sessions (`where: "session"`).
|
|
94
|
+
- `actions` is optional: menu entries on messages (`where: "message"`, optionally only `roles: ["assistant"]`) and on sessions (`where: "session"`). By default, picking one opens your page with that message or session in `ctx.item` (`kind: "message"` with the text, or `kind: "session"`; add `payload: ["messages"]` to get the conversation too). Set `mode: "background"` to call `onAction` without opening UI, as shown below. Up to 8.
|
|
94
95
|
- `commands` is optional: slash commands for the chat box, up to 8. `/task DEMO-2` calls your `host.onResolve` handler instead of the model; return a chip to attach it, or `null` for nothing. A name the app already has is ignored.
|
|
95
96
|
- `tools` is optional: how your tool calls look in the chat, up to 16, no code. `match` is the tool name OpenCode reports (`mcp.tasks.*` matches every tool under that prefix); `name` and `icon` (a Remixicon name or an SVG inside the folder, like `panel.icon`) set the header, `title` and `subtitle` are templates like `{input.id}` or `{output.total} open`, and `output` picks the body: `text`, `json`, `markdown`, `code` (with `language`), or `table` (with `columns`, rows from the output array or `output.items`). Leave `output` out to keep the app's own detection.
|
|
96
97
|
- `capabilities` lists what needs the user's approval: `prompt` to send messages, `sessions` to create sessions and worktrees, `files` to read and write inside the open project, `model` for one-off text generation with the user's Small Model (`host.generate`, no session involved). An `integration` adds `network`, a `service` adds `service`, `filesystem` patterns (like `["~/.config/opencode/opencode.json"]`) add `filesystem`, which lets `readFile`, `writeFile`, `listDir`, and `stat` reach those paths outside the project, and a session action with `payload: ["messages"]` adds `conversation`. The user approves the whole list once at install. Calls outside it fail with `NOT_GRANTED`.
|
|
97
98
|
- `integration` is optional. It adds a card at Settings → Integrations. `token` takes a pasted API token (`scheme: "bearer"` for `Authorization: Bearer`, `"basic"` for a username and token pair as Jira Cloud wants), `oauth` runs an authorize flow with a pasted client id, and `host: { "provider": "linear" }` reuses the Linear account already connected in OpenChamber. The page never sees the token; OpenChamber makes the calls through `host.request`.
|
|
98
99
|
- `service` is optional. It declares a local process OpenChamber starts next to the extension. It runs with the user's full access and no sandbox, so declare one only when the page cannot do the job. See [GUEST_SERVICES.md](./GUEST_SERVICES.md).
|
|
99
100
|
|
|
101
|
+
## Actions without opening a panel
|
|
102
|
+
|
|
103
|
+
Set `mode: "background"` on a message or session action to run it without opening the rail or a dialog:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "id": "message-length", "label": "Show message length", "where": "message", "mode": "background" }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Register `onAction` immediately after `connectHost` in your background script, or your panel script when no background entry is declared:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const host = connectHost();
|
|
113
|
+
host.onAction(async (item) => {
|
|
114
|
+
if (item.kind === 'message' && item.action === 'message-length') {
|
|
115
|
+
await host.toast({
|
|
116
|
+
kind: 'info',
|
|
117
|
+
message: `Message length: ${item.text.length} characters.`,
|
|
118
|
+
copy: true,
|
|
119
|
+
dismiss: true,
|
|
120
|
+
persistent: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Each click runs in a fresh hidden iframe. Await all work inside the handler, including the toast. OpenChamber removes the iframe when the handler finishes and reports thrown errors as toasts. Loading and execution together have a 20-second limit. A runtime switch, disabling the extension, or withdrawing approval also ends the invocation. Completed side effects are not rolled back or retried.
|
|
127
|
+
|
|
128
|
+
`ctx.surface` is `"background"`; skip drawing your UI in that case. `ctx.item` stays `null`, so the action runs only through `onAction`, without repeated `onItem` snapshots. The session and directory context stay with the clicked target even if the user changes chats. OpenChamber loads `background.entry` when declared and falls back to `panel.entry` for existing extensions. Omit `mode`, or use `"open"`, to open a visible panel or dialog. See `examples/hello-kit` for separate panel and background scripts.
|
|
129
|
+
|
|
130
|
+
To remove the panel and its rail icon entirely, use this `contributes` block:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"panel": { "id": "message-tools", "name": "Message Tools", "icon": "apps" },
|
|
135
|
+
"background": { "entry": "background/index.html" },
|
|
136
|
+
"actions": [
|
|
137
|
+
{ "id": "message-length", "label": "Show message length", "where": "message", "mode": "background" }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The `panel` object retains the extension's identity for Settings and approval dialogs; only `panel.entry` creates a visible panel. Without it, every action must use `mode: "background"`, and `attach` and `page` cannot open a view. Slash commands call `onResolve` in the background entry, with `ctx.surface` also set to `"background"`. Capabilities, services, integrations, storage, and file access use the same approval checks. An attached chip can still be sent to the model or opened with its browser button; clicking to reopen the extension shows a no-panel notice.
|
|
143
|
+
|
|
144
|
+
This addition requires a matching OpenChamber build. For unreleased SDK preview builds, use the app built from the same revision.
|
|
145
|
+
|
|
146
|
+
## Toast buttons
|
|
147
|
+
|
|
148
|
+
`host.toast` accepts three optional fields:
|
|
149
|
+
|
|
150
|
+
- `copy: true` adds Copy for the displayed message. Use `copy: { text: "..." }` to copy a different value, up to 32,000 characters. Whitespace in that value is preserved.
|
|
151
|
+
- `dismiss: true` adds OK to close the toast.
|
|
152
|
+
- `persistent: true` keeps the toast on screen until dismissed and always adds OK, even if `dismiss` is false.
|
|
153
|
+
|
|
154
|
+
Copy keeps the toast open and shows Copied on success. A failed copy shows an error beside the button so the user can retry. OpenChamber translates the buttons and handles clicks itself, so they work after a background action's iframe has closed. `await host.toast(...)` waits only for the host to show the toast, not for a button click. Omit these fields for an ordinary timed toast.
|
|
155
|
+
|
|
100
156
|
## In the page
|
|
101
157
|
|
|
102
158
|
For a full-screen board, add `"page": true` under `contributes`, or `"page": { "entry": "panel/page.html", "title": "Board" }` for separate HTML. Users open it from the Extension pages menu above the session list. `ctx.surface` is `"page"`. The extension cannot open the page itself.
|
package/dist/contract.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type SessionSnapshot = {
|
|
|
51
51
|
agent?: string;
|
|
52
52
|
};
|
|
53
53
|
/** Which host chrome mounted this iframe. Not `openSurface`. */
|
|
54
|
-
export type GuestHostSurface = 'panel' | 'dialog' | 'page';
|
|
54
|
+
export type GuestHostSurface = 'panel' | 'dialog' | 'page' | 'background';
|
|
55
55
|
export type GuestConnection = {
|
|
56
56
|
connected: boolean;
|
|
57
57
|
account: string;
|
|
@@ -208,6 +208,14 @@ export type GuestSessionItem = {
|
|
|
208
208
|
truncated?: boolean;
|
|
209
209
|
};
|
|
210
210
|
export type GuestItem = AttachIssueRequest | GuestMessageItem | GuestSessionItem;
|
|
211
|
+
/** The captured target of a background action. Delivered once through `onAction`, not `onItem`. */
|
|
212
|
+
export type GuestActionItem = GuestMessageItem | GuestSessionItem;
|
|
213
|
+
export type ActionResultPayload = {
|
|
214
|
+
ok: true;
|
|
215
|
+
} | {
|
|
216
|
+
ok: false;
|
|
217
|
+
error: string;
|
|
218
|
+
};
|
|
211
219
|
export declare const isGuestMessageItem: (item: GuestItem | null) => item is GuestMessageItem;
|
|
212
220
|
export declare const isGuestSessionItem: (item: GuestItem | null) => item is GuestSessionItem;
|
|
213
221
|
/** The composer chip: what the guest handed to `attach`, with `text` filled from the chip. */
|
|
@@ -231,6 +239,14 @@ export type ToastKind = 'info' | 'success' | 'error';
|
|
|
231
239
|
export type ToastRequest = {
|
|
232
240
|
kind: ToastKind;
|
|
233
241
|
message: string;
|
|
242
|
+
/** Show Copy. `true` copies the message; an object supplies different text, up to 32,000 characters. */
|
|
243
|
+
copy?: boolean | {
|
|
244
|
+
text: string;
|
|
245
|
+
};
|
|
246
|
+
/** Show an OK button that dismisses the toast. */
|
|
247
|
+
dismiss?: boolean;
|
|
248
|
+
/** Keep the toast until dismissed. Always includes OK, even when `dismiss` is false. */
|
|
249
|
+
persistent?: boolean;
|
|
234
250
|
};
|
|
235
251
|
export type ComposeRequest = {
|
|
236
252
|
text: string;
|
|
@@ -395,6 +411,11 @@ export type HostResolveMessage = Envelope & {
|
|
|
395
411
|
id: string;
|
|
396
412
|
payload: ResolveRequest;
|
|
397
413
|
};
|
|
414
|
+
export type HostActionMessage = Envelope & {
|
|
415
|
+
type: 'action';
|
|
416
|
+
id: string;
|
|
417
|
+
payload: GuestActionItem;
|
|
418
|
+
};
|
|
398
419
|
export type HostResultMessage = Envelope & {
|
|
399
420
|
type: 'result';
|
|
400
421
|
id: string;
|
|
@@ -409,7 +430,7 @@ export type HostResultMessage = Envelope & {
|
|
|
409
430
|
export type HostMessage = (Envelope & {
|
|
410
431
|
type: 'workspace';
|
|
411
432
|
payload: GuestWorkspaceUpdate;
|
|
412
|
-
}) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostResolveMessage | HostResultMessage;
|
|
433
|
+
}) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostResolveMessage | HostActionMessage | HostResultMessage;
|
|
413
434
|
type GuestCall<Type extends string, Payload = never> = Envelope & {
|
|
414
435
|
type: Type;
|
|
415
436
|
id: string;
|
|
@@ -454,11 +475,17 @@ export type GuestResolveResultMessage = Envelope & {
|
|
|
454
475
|
id: string;
|
|
455
476
|
payload: ResolveResultPayload;
|
|
456
477
|
};
|
|
478
|
+
/** Completes a host `action`. The host sends no `result` back. */
|
|
479
|
+
export type GuestActionResultMessage = Envelope & {
|
|
480
|
+
type: 'action-result';
|
|
481
|
+
id: string;
|
|
482
|
+
payload: ActionResultPayload;
|
|
483
|
+
};
|
|
457
484
|
export type GuestMessage = GuestCall<'workspace-read', GuestWorkspaceQuery> | GuestCall<'workspace-subscribe', GuestWorkspaceSubscription> | GuestCall<'workspace-unsubscribe', {
|
|
458
485
|
subscriptionId: string;
|
|
459
486
|
}> | GuestCall<'storage', GuestStorageRequest> | GuestCall<'open-session', {
|
|
460
487
|
sessionId: string;
|
|
461
|
-
}> | GuestHelloMessage | GuestToastMessage | GuestOpenUrlMessage | GuestOpenSurfaceMessage | GuestClipboardWriteMessage | GuestComposeMessage | GuestAttachMessage | GuestStartSessionMessage | GuestPromptMessage | GuestSessionLinkMessage | GuestCloseMessage | GuestOauthStartMessage | GuestOauthDisconnectMessage | GuestRequestMessage | GuestServiceRequestMessage | GuestServiceStatusMessage | GuestFileReadMessage | GuestFileWriteMessage | GuestFileListMessage | GuestFileStatMessage | GuestGenerateMessage | GuestBadgeMessage | GuestResolveResultMessage;
|
|
488
|
+
}> | GuestHelloMessage | GuestToastMessage | GuestOpenUrlMessage | GuestOpenSurfaceMessage | GuestClipboardWriteMessage | GuestComposeMessage | GuestAttachMessage | GuestStartSessionMessage | GuestPromptMessage | GuestSessionLinkMessage | GuestCloseMessage | GuestOauthStartMessage | GuestOauthDisconnectMessage | GuestRequestMessage | GuestServiceRequestMessage | GuestServiceStatusMessage | GuestFileReadMessage | GuestFileWriteMessage | GuestFileListMessage | GuestFileStatMessage | GuestGenerateMessage | GuestBadgeMessage | GuestActionResultMessage | GuestResolveResultMessage;
|
|
462
489
|
export declare const isServiceStatusResult: (value: HostResultPayload | undefined) => value is ServiceStatusResult;
|
|
463
490
|
export declare const isGuestRequestResult: (value: HostResultPayload | undefined) => value is GuestRequestResult;
|
|
464
491
|
export declare const isFileReadResult: (value: HostResultPayload | undefined) => value is FileReadResult;
|
package/dist/contract.js
CHANGED
|
@@ -198,7 +198,7 @@ export const isFileStatResult = (value) => Boolean(value && 'kind' in value && '
|
|
|
198
198
|
export const isGenerateResult = (value) => Boolean(value && 'text' in value && String(value.text) === value.text && !('status' in value));
|
|
199
199
|
const HOST_PUSH_TYPES = new Set([
|
|
200
200
|
'workspace',
|
|
201
|
-
'ready', 'directory', 'session', 'connection', 'settings', 'session-lifecycle', 'item', 'resolve',
|
|
201
|
+
'ready', 'directory', 'session', 'connection', 'settings', 'session-lifecycle', 'item', 'resolve', 'action',
|
|
202
202
|
]);
|
|
203
203
|
const asWireRecord = (data) => (Object(data) === data ? data : null);
|
|
204
204
|
const isNonEmptyString = (value) => (String(value) === value && value.length > 0);
|
package/dist/host.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type GuestProjectsSnapshot, type GuestWorktreesSnapshot, type GuestSessionsSnapshot } from './workspace.ts';
|
|
2
2
|
import type { JsonValue } from './contract.ts';
|
|
3
|
-
import { type AttachIssueRequest, 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';
|
|
3
|
+
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';
|
|
4
4
|
export type HostFrame = {
|
|
5
5
|
addEventListener: Window['addEventListener'];
|
|
6
6
|
removeEventListener: Window['removeEventListener'];
|
|
@@ -49,6 +49,12 @@ export type HostClient = {
|
|
|
49
49
|
* toast. One handler at a time; the returned function removes it.
|
|
50
50
|
*/
|
|
51
51
|
onResolve: (handler: (request: ResolveRequest) => Promise<AttachIssueRequest | null> | AttachIssueRequest | null) => () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Run a `mode: "background"` action. Register synchronously after connectHost.
|
|
54
|
+
* Await all work, including toast calls: the hidden frame is removed when
|
|
55
|
+
* this handler settles. Throw to report failure. One handler at a time.
|
|
56
|
+
*/
|
|
57
|
+
onAction: (handler: (item: GuestActionItem) => void | Promise<void>) => () => void;
|
|
52
58
|
toast: (request: ToastRequest) => Promise<void>;
|
|
53
59
|
openUrl: (url: string) => Promise<void>;
|
|
54
60
|
openSurface: (surfaceId: string) => Promise<void>;
|
package/dist/host.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from "./api-version.js";
|
|
2
2
|
import { GUEST_STORAGE_KEY_MAX, GUEST_STORAGE_VALUE_BYTES } from "./workspace.js";
|
|
3
|
-
import { GUEST_FILE_CONTENT_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, clampPromptRequest, clampStartSessionRequest, readHostMessage, isGenerateResult, isFileListResult, isFileReadResult, isFileStatResult, isJsonValue, isFileWriteResult, isServiceStatusResult, isGuestRequestResult, isPromptResult, isStartSessionResult, } from "./contract.js";
|
|
3
|
+
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, clampPromptRequest, clampStartSessionRequest, readHostMessage, isGenerateResult, isFileListResult, isFileReadResult, isFileStatResult, isJsonValue, isFileWriteResult, isServiceStatusResult, isGuestRequestResult, isPromptResult, isStartSessionResult, } from "./contract.js";
|
|
4
4
|
export class HostRequestError extends Error {
|
|
5
5
|
code;
|
|
6
6
|
constructor(code, message) {
|
|
@@ -32,6 +32,7 @@ export const connectHost = (options = {}) => {
|
|
|
32
32
|
const settingsListeners = new Set();
|
|
33
33
|
const itemListeners = new Set();
|
|
34
34
|
let resolveHandler = null;
|
|
35
|
+
let actionHandler = null;
|
|
35
36
|
const pending = new Map();
|
|
36
37
|
const workspaceListeners = new Map();
|
|
37
38
|
let disposed = false;
|
|
@@ -135,6 +136,23 @@ export const connectHost = (options = {}) => {
|
|
|
135
136
|
emit(itemListeners, message.payload.item);
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
139
|
+
if (message.type === 'action') {
|
|
140
|
+
const answer = (payload) => {
|
|
141
|
+
if (!disposed)
|
|
142
|
+
post({ channel: OPENCHAMBER_SDK_CHANNEL, v: OPENCHAMBER_SDK_API_VERSION,
|
|
143
|
+
type: 'action-result', id: message.id, payload });
|
|
144
|
+
};
|
|
145
|
+
const handler = actionHandler;
|
|
146
|
+
if (!handler) {
|
|
147
|
+
answer({ ok: false, error: 'This extension does not handle background actions.' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
Promise.resolve().then(() => handler(message.payload)).then(() => answer({ ok: true }), (error) => {
|
|
151
|
+
const text = (error instanceof Error ? error.message : String(error)).trim();
|
|
152
|
+
answer({ ok: false, error: (text || 'Action failed.').slice(0, GUEST_RESOLVE_ERROR_MAX) });
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
138
156
|
if (message.type === 'resolve') {
|
|
139
157
|
const answer = (payload) => {
|
|
140
158
|
post({
|
|
@@ -239,6 +257,11 @@ export const connectHost = (options = {}) => {
|
|
|
239
257
|
return result;
|
|
240
258
|
};
|
|
241
259
|
return {
|
|
260
|
+
onAction: (handler) => {
|
|
261
|
+
actionHandler = handler;
|
|
262
|
+
return () => { if (actionHandler === handler)
|
|
263
|
+
actionHandler = null; };
|
|
264
|
+
},
|
|
242
265
|
listProjects: async () => {
|
|
243
266
|
const result = await readWorkspace({ kind: 'projects' });
|
|
244
267
|
if (result.kind !== 'projects')
|
|
@@ -344,13 +367,22 @@ export const connectHost = (options = {}) => {
|
|
|
344
367
|
resolveHandler = null;
|
|
345
368
|
};
|
|
346
369
|
},
|
|
347
|
-
toast: (payload) =>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
payload
|
|
353
|
-
|
|
370
|
+
toast: (payload) => {
|
|
371
|
+
const message = payload.message.trim();
|
|
372
|
+
if (!message || message.length > GUEST_TOAST_MAX) {
|
|
373
|
+
return Promise.reject(new HostRequestError('HOST_REJECTED', `Toast message must contain 1 to ${GUEST_TOAST_MAX} characters.`));
|
|
374
|
+
}
|
|
375
|
+
if (payload.copy && payload.copy !== true && (!payload.copy.text.length || payload.copy.text.length > GUEST_CLIPBOARD_TEXT_MAX)) {
|
|
376
|
+
return Promise.reject(new HostRequestError('HOST_REJECTED', `Toast copy text must contain 1 to ${GUEST_CLIPBOARD_TEXT_MAX} characters.`));
|
|
377
|
+
}
|
|
378
|
+
return request({
|
|
379
|
+
channel: OPENCHAMBER_SDK_CHANNEL,
|
|
380
|
+
v: OPENCHAMBER_SDK_API_VERSION,
|
|
381
|
+
type: 'toast',
|
|
382
|
+
id: nextId(ids),
|
|
383
|
+
payload: { ...payload, message },
|
|
384
|
+
});
|
|
385
|
+
},
|
|
354
386
|
openUrl: (url) => request({
|
|
355
387
|
channel: OPENCHAMBER_SDK_CHANNEL,
|
|
356
388
|
v: OPENCHAMBER_SDK_API_VERSION,
|
|
@@ -588,6 +620,7 @@ export const connectHost = (options = {}) => {
|
|
|
588
620
|
workspaceListeners.clear();
|
|
589
621
|
disposed = true;
|
|
590
622
|
resolveHandler = null;
|
|
623
|
+
actionHandler = null;
|
|
591
624
|
target.removeEventListener('message', onMessage);
|
|
592
625
|
for (const waiter of pending.values()) {
|
|
593
626
|
clearTimeout(waiter.timer);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ export { GUEST_STORAGE_KEY_MAX, GUEST_STORAGE_KEYS_MAX, GUEST_STORAGE_VALUE_BYTE
|
|
|
5
5
|
export type { OpenChamberManifestApiVersion } from './api-version.ts';
|
|
6
6
|
export { compareOpenChamberVersions, hostMeetsOpenChamberEngine, openChamberEngineMinimum, OPENCHAMBER_ENGINE_PATTERN, parseOpenChamberVersion, } from './host-version.ts';
|
|
7
7
|
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, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, toPublicService, toPublicIntegration, } from './manifest.ts';
|
|
8
|
-
export type { ServiceContribution, ServicePermissions, GuestActionContribution, GuestActionPayload, GuestActionRole, GuestActionWhere, GuestCommandContribution, GuestToolContribution, GuestToolOutput, AttachContribution, AttachContributionObject, AttachMode, DeclaredGuestCapability, GuestAuthorization, GuestCapability, IntegrationAuth, IntegrationContribution, IntegrationHost, IntegrationHostProvider, IntegrationOAuth, IntegrationOAuthAccount, IntegrationSettingField, IntegrationToken, IntegrationTokenScheme, OpenChamberContributes, OpenChamberEngines, OpenChamberManifest, PanelContribution, PageContribution, ParseManifestErrorCode, ParseManifestFailure, ParseManifestResult, ParseManifestSuccess, PublicService, PublicServicePermissions, PublicGuestCapabilities, PublicIntegration, PublicIntegrationToken, PublicSocketBinding, ResolvedGuestApi, SocketBinding, SocketPlatform, } from './manifest.ts';
|
|
8
|
+
export type { ServiceContribution, ServicePermissions, GuestActionContribution, GuestActionPayload, GuestActionRole, GuestActionWhere, GuestCommandContribution, GuestToolContribution, GuestToolOutput, AttachContribution, AttachContributionObject, AttachMode, DeclaredGuestCapability, GuestAuthorization, GuestCapability, IntegrationAuth, IntegrationContribution, IntegrationHost, IntegrationHostProvider, IntegrationOAuth, IntegrationOAuthAccount, IntegrationSettingField, IntegrationToken, IntegrationTokenScheme, OpenChamberContributes, OpenChamberEngines, OpenChamberManifest, PanelContribution, BackgroundContribution, PageContribution, ParseManifestErrorCode, ParseManifestFailure, ParseManifestResult, ParseManifestSuccess, PublicService, PublicServicePermissions, PublicGuestCapabilities, PublicIntegration, PublicIntegrationToken, PublicSocketBinding, ResolvedGuestApi, SocketBinding, SocketPlatform, } from './manifest.ts';
|
|
9
9
|
export { connectHost, HostRequestError } from './host.ts';
|
|
10
10
|
export type { HostClient, HostClientOptions, HostFrame } from './host.ts';
|
|
11
11
|
export { clampAttachRequest, clampBadgeCount, 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';
|
|
12
|
-
export type { BadgeRequest, GuestBadgeMessage, 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, HostMessage, HostReadyContext, HostReadyMessage, HostResultMessage, HostSessionLifecycleMessage, HostSessionMessage, HostSettingsMessage, HostTheme, HostThemeMode, HostThemeTokens, SessionSnapshot, ToastKind, ToastRequest, } from './contract.ts';
|
|
12
|
+
export type { BadgeRequest, ActionResultPayload, GuestActionItem, GuestActionResultMessage, HostActionMessage, GuestBadgeMessage, 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, HostMessage, HostReadyContext, HostReadyMessage, HostResultMessage, HostSessionLifecycleMessage, HostSessionMessage, HostSettingsMessage, HostTheme, HostThemeMode, HostThemeTokens, SessionSnapshot, ToastKind, ToastRequest, } from './contract.ts';
|
|
13
13
|
export { EMPTY_GUEST_CONNECTION, GUEST_BADGE_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';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type { OpenChamberManifestApiVersion } from './api-version.ts';
|
|
|
2
2
|
export declare const PANEL_ID: RegExp;
|
|
3
3
|
/**
|
|
4
4
|
* The extension's identity on the rail, the Extensions card, and the approval
|
|
5
|
-
* dialog. `entry` is the panel page
|
|
6
|
-
*
|
|
5
|
+
* dialog. `entry` is the optional visible panel page. Code can instead run
|
|
6
|
+
* from `background.entry` without adding a rail icon.
|
|
7
7
|
*/
|
|
8
8
|
export type PanelContribution = {
|
|
9
9
|
id: string;
|
|
@@ -11,6 +11,10 @@ export type PanelContribution = {
|
|
|
11
11
|
icon: string;
|
|
12
12
|
entry?: string;
|
|
13
13
|
};
|
|
14
|
+
/** Sandboxed HTML loaded on demand for background actions and slash commands. */
|
|
15
|
+
export type BackgroundContribution = {
|
|
16
|
+
entry: string;
|
|
17
|
+
};
|
|
14
18
|
export type AttachMode = 'panel' | 'dialog';
|
|
15
19
|
/**
|
|
16
20
|
* Object form of `contributes.attach`. `entry` is an HTML page inside the
|
|
@@ -42,7 +46,7 @@ export declare const GUEST_COMMAND_DESCRIPTION_MAX = 80;
|
|
|
42
46
|
/** A slash command name: lower-case, digits and dashes, up to 24 characters. */
|
|
43
47
|
export declare const GUEST_COMMAND_NAME: RegExp;
|
|
44
48
|
/**
|
|
45
|
-
* A menu entry on a message or a session.
|
|
49
|
+
* A menu entry on a message or a session. By default it opens the guest with
|
|
46
50
|
* the message or session as `ready.item`. `roles` narrows a message action
|
|
47
51
|
* to user or assistant messages (default both); `payload: ["messages"]` on
|
|
48
52
|
* a session action asks for the conversation and needs the `conversation`
|
|
@@ -54,6 +58,8 @@ export type GuestActionContribution = {
|
|
|
54
58
|
/** Remixicon name or package `.svg` path, same as `panel.icon`. Falls back to the panel icon. */
|
|
55
59
|
icon?: string;
|
|
56
60
|
where: GuestActionWhere;
|
|
61
|
+
/** `background` calls `host.onAction` in a temporary hidden frame. Default: `open`. */
|
|
62
|
+
mode?: 'open' | 'background';
|
|
57
63
|
roles?: GuestActionRole[];
|
|
58
64
|
payload?: GuestActionPayload[];
|
|
59
65
|
};
|
|
@@ -231,6 +237,7 @@ export declare const GUEST_FILESYSTEM_PATTERN_MAX = 256;
|
|
|
231
237
|
export declare const isGuestFilesystemPattern: (value: string) => boolean;
|
|
232
238
|
export type OpenChamberContributes = {
|
|
233
239
|
panel: PanelContribution;
|
|
240
|
+
background?: BackgroundContribution;
|
|
234
241
|
attach?: AttachContribution;
|
|
235
242
|
page?: PageContribution;
|
|
236
243
|
capabilities?: DeclaredGuestCapability[];
|
|
@@ -254,10 +261,8 @@ export type PublicGuestCapabilities = {
|
|
|
254
261
|
};
|
|
255
262
|
export declare const requestedGuestCapabilities: (contributes: Pick<OpenChamberContributes, "capabilities" | "integration" | "service" | "filesystem" | "actions">) => GuestCapability[];
|
|
256
263
|
/**
|
|
257
|
-
* Whether the package ships a panel
|
|
258
|
-
*
|
|
259
|
-
* command, service, integration, or capability; parse refuses those without
|
|
260
|
-
* an entry. `tools` is the only contribution it may carry.
|
|
264
|
+
* Whether the package ships a visible panel. Background-only extensions can
|
|
265
|
+
* execute code but have no rail surface, attach picker, or full-screen page.
|
|
261
266
|
*/
|
|
262
267
|
export declare const hasGuestPage: (contributes: Pick<OpenChamberContributes, "panel">) => boolean;
|
|
263
268
|
export declare const isGuestApproved: (capabilities: PublicGuestCapabilities) => boolean;
|
|
@@ -277,7 +282,7 @@ export type OpenChamberManifest = {
|
|
|
277
282
|
engines?: OpenChamberEngines;
|
|
278
283
|
contributes: OpenChamberContributes;
|
|
279
284
|
};
|
|
280
|
-
export type ParseManifestErrorCode = 'not-object' | 'missing-openchamber' | 'unsupported-api-version' | 'invalid-engines' | 'invalid-version' | 'missing-panel' | 'invalid-panel' | 'invalid-panel-id' | 'invalid-panel-name' | 'invalid-panel-icon' | 'invalid-panel-entry' | 'invalid-attach' | 'invalid-page' | 'invalid-capabilities' | 'invalid-integration' | 'invalid-service' | 'invalid-filesystem' | 'invalid-actions' | 'invalid-commands' | 'invalid-tools';
|
|
285
|
+
export type ParseManifestErrorCode = 'not-object' | 'missing-openchamber' | 'unsupported-api-version' | 'invalid-engines' | 'invalid-version' | 'missing-panel' | 'invalid-panel' | 'invalid-panel-id' | 'invalid-panel-name' | 'invalid-panel-icon' | 'invalid-panel-entry' | 'invalid-background' | 'invalid-attach' | 'invalid-page' | 'invalid-capabilities' | 'invalid-integration' | 'invalid-service' | 'invalid-filesystem' | 'invalid-actions' | 'invalid-commands' | 'invalid-tools';
|
|
281
286
|
export type ParseManifestFailure = {
|
|
282
287
|
ok: false;
|
|
283
288
|
code: ParseManifestErrorCode;
|
package/dist/manifest.js
CHANGED
|
@@ -119,10 +119,8 @@ export const requestedGuestCapabilities = (contributes) => {
|
|
|
119
119
|
return GUEST_CAPABILITIES.filter((capability) => declared.has(capability));
|
|
120
120
|
};
|
|
121
121
|
/**
|
|
122
|
-
* Whether the package ships a panel
|
|
123
|
-
*
|
|
124
|
-
* command, service, integration, or capability; parse refuses those without
|
|
125
|
-
* an entry. `tools` is the only contribution it may carry.
|
|
122
|
+
* Whether the package ships a visible panel. Background-only extensions can
|
|
123
|
+
* execute code but have no rail surface, attach picker, or full-screen page.
|
|
126
124
|
*/
|
|
127
125
|
export const hasGuestPage = (contributes) => Boolean(contributes.panel.entry);
|
|
128
126
|
export const isGuestApproved = (capabilities) => (capabilities.requested.every((capability) => capabilities.granted.includes(capability)));
|
package/dist/parse.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export declare const openChamberManifestSchema: z.ZodObject<{
|
|
|
12
12
|
icon: z.ZodString;
|
|
13
13
|
entry: z.ZodOptional<z.ZodString>;
|
|
14
14
|
}, z.core.$strip>;
|
|
15
|
+
background: z.ZodOptional<z.ZodObject<{
|
|
16
|
+
entry: z.ZodString;
|
|
17
|
+
}, z.core.$strip>>;
|
|
15
18
|
attach: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodEnum<{
|
|
16
19
|
panel: "panel";
|
|
17
20
|
dialog: "dialog";
|
|
@@ -104,6 +107,10 @@ export declare const openChamberManifestSchema: z.ZodObject<{
|
|
|
104
107
|
message: "message";
|
|
105
108
|
session: "session";
|
|
106
109
|
}>;
|
|
110
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
111
|
+
background: "background";
|
|
112
|
+
open: "open";
|
|
113
|
+
}>>;
|
|
107
114
|
roles: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
108
115
|
user: "user";
|
|
109
116
|
assistant: "assistant";
|
|
@@ -150,6 +157,9 @@ export declare const packageManifestSchema: z.ZodObject<{
|
|
|
150
157
|
icon: z.ZodString;
|
|
151
158
|
entry: z.ZodOptional<z.ZodString>;
|
|
152
159
|
}, z.core.$strip>;
|
|
160
|
+
background: z.ZodOptional<z.ZodObject<{
|
|
161
|
+
entry: z.ZodString;
|
|
162
|
+
}, z.core.$strip>>;
|
|
153
163
|
attach: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodEnum<{
|
|
154
164
|
panel: "panel";
|
|
155
165
|
dialog: "dialog";
|
|
@@ -242,6 +252,10 @@ export declare const packageManifestSchema: z.ZodObject<{
|
|
|
242
252
|
message: "message";
|
|
243
253
|
session: "session";
|
|
244
254
|
}>;
|
|
255
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
256
|
+
background: "background";
|
|
257
|
+
open: "open";
|
|
258
|
+
}>>;
|
|
245
259
|
roles: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
246
260
|
user: "user";
|
|
247
261
|
assistant: "assistant";
|
package/dist/parse.js
CHANGED
|
@@ -149,6 +149,7 @@ const actionSchema = z.object({
|
|
|
149
149
|
label: z.string().trim().min(1).max(GUEST_ACTION_LABEL_MAX),
|
|
150
150
|
icon: z.string().trim().refine(isPanelIcon).optional(),
|
|
151
151
|
where: z.enum(['message', 'session']),
|
|
152
|
+
mode: z.enum(['open', 'background']).optional(),
|
|
152
153
|
roles: z.array(z.enum(['user', 'assistant'])).min(1).max(2).optional(),
|
|
153
154
|
payload: z.array(z.enum(['messages'])).max(1).optional(),
|
|
154
155
|
}).refine((value) => value.where === 'message' || value.roles === undefined, { path: ['roles'] })
|
|
@@ -178,6 +179,9 @@ const toolSchema = z.object({
|
|
|
178
179
|
const toolsSchema = z.array(toolSchema).min(1).max(GUEST_TOOLS_MAX);
|
|
179
180
|
const contributesSchema = z.object({
|
|
180
181
|
panel: panelSchema,
|
|
182
|
+
background: z.object({
|
|
183
|
+
entry: z.string().trim().refine((value) => isSafeAssetPath(value) && value.toLowerCase().endsWith('.html')),
|
|
184
|
+
}).optional(),
|
|
181
185
|
attach: attachSchema.optional(),
|
|
182
186
|
page: z.union([z.literal(true), z.object({
|
|
183
187
|
entry: z.string().trim().refine(isSafeAssetPath),
|
|
@@ -192,12 +196,9 @@ const contributesSchema = z.object({
|
|
|
192
196
|
tools: toolsSchema.optional(),
|
|
193
197
|
});
|
|
194
198
|
/**
|
|
195
|
-
*
|
|
196
|
-
* `panel.entry` there is nothing to open from the rail, the + menu, a menu
|
|
197
|
-
* action, or a slash command, and nothing to hand a capability, a service,
|
|
198
|
-
* an integration, or a filesystem grant to; only `tools` stays meaningful.
|
|
199
|
+
* Contributions that need either the panel or background execution frame.
|
|
199
200
|
*/
|
|
200
|
-
const
|
|
201
|
+
const runtimeContributions = (contributes) => {
|
|
201
202
|
const declared = [];
|
|
202
203
|
if (contributes.page !== undefined)
|
|
203
204
|
declared.push('page');
|
|
@@ -225,13 +226,28 @@ export const openChamberManifestSchema = z.object({
|
|
|
225
226
|
contributes: contributesSchema.superRefine((contributes, ctx) => {
|
|
226
227
|
if (hasGuestPage(contributes))
|
|
227
228
|
return;
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
if (contributes.background) {
|
|
230
|
+
const needsPanel = [];
|
|
231
|
+
if (contributes.page !== undefined)
|
|
232
|
+
needsPanel.push('page');
|
|
233
|
+
if (contributes.attach !== undefined && contributes.attach !== false)
|
|
234
|
+
needsPanel.push('attach');
|
|
235
|
+
if (contributes.actions?.some((action) => action.mode !== 'background'))
|
|
236
|
+
needsPanel.push('actions with mode "open"');
|
|
237
|
+
if (needsPanel.length > 0)
|
|
238
|
+
ctx.addIssue({
|
|
239
|
+
code: 'custom', path: ['panel'],
|
|
240
|
+
message: `${needsPanel.join(', ')} needs panel.entry; background-only actions must declare mode "background".`,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const needsRuntime = runtimeContributions(contributes);
|
|
245
|
+
if (needsRuntime.length === 0)
|
|
230
246
|
return;
|
|
231
247
|
ctx.addIssue({
|
|
232
248
|
code: 'custom',
|
|
233
249
|
path: ['panel'],
|
|
234
|
-
message: `${
|
|
250
|
+
message: `${needsRuntime.map((key) => `contributes.${key}`).join(', ')} needs panel.entry or background.entry; an extension without either may only declare tools.`,
|
|
235
251
|
});
|
|
236
252
|
}),
|
|
237
253
|
});
|
|
@@ -293,6 +309,9 @@ const failureFromIssue = (issue) => {
|
|
|
293
309
|
if (path === 'contributes.page' || path.startsWith('contributes.page.')) {
|
|
294
310
|
return fail('invalid-page', 'contributes.page must be true or { entry: "<package HTML>", title?: "Page title" }.');
|
|
295
311
|
}
|
|
312
|
+
if (path === 'contributes.background' || path.startsWith('contributes.background.')) {
|
|
313
|
+
return fail('invalid-background', 'contributes.background needs an entry ending in .html inside the package.');
|
|
314
|
+
}
|
|
296
315
|
if (path === 'contributes.attach' || path.startsWith('contributes.attach.')) {
|
|
297
316
|
return fail('invalid-attach', 'contributes.attach must be true, false, "panel", "dialog", or { "mode": "panel" | "dialog", "entry"?: "<html inside the package, dialog only>" }.');
|
|
298
317
|
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -143,6 +143,7 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
143
143
|
panel: "panel";
|
|
144
144
|
dialog: "dialog";
|
|
145
145
|
page: "page";
|
|
146
|
+
background: "background";
|
|
146
147
|
}>;
|
|
147
148
|
connection: z.ZodObject<{
|
|
148
149
|
connected: z.ZodBoolean;
|
|
@@ -751,8 +752,53 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
751
752
|
} | {
|
|
752
753
|
text: string;
|
|
753
754
|
} | undefined;
|
|
754
|
-
}
|
|
755
|
+
}>>, z.ZodObject<{
|
|
756
|
+
type: z.ZodLiteral<"action">;
|
|
757
|
+
id: z.ZodString;
|
|
758
|
+
payload: z.ZodUnion<readonly [z.ZodObject<{
|
|
759
|
+
kind: z.ZodLiteral<"message">;
|
|
760
|
+
action: z.ZodString;
|
|
761
|
+
sessionId: z.ZodString;
|
|
762
|
+
sessionTitle: z.ZodString;
|
|
763
|
+
directory: z.ZodNullable<z.ZodString>;
|
|
764
|
+
messageId: z.ZodString;
|
|
765
|
+
role: z.ZodEnum<{
|
|
766
|
+
user: "user";
|
|
767
|
+
assistant: "assistant";
|
|
768
|
+
}>;
|
|
769
|
+
text: z.ZodString;
|
|
770
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
771
|
+
kind: z.ZodLiteral<"session">;
|
|
772
|
+
action: z.ZodString;
|
|
773
|
+
sessionId: z.ZodString;
|
|
774
|
+
sessionTitle: z.ZodString;
|
|
775
|
+
directory: z.ZodNullable<z.ZodString>;
|
|
776
|
+
messages: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
777
|
+
id: z.ZodString;
|
|
778
|
+
role: z.ZodEnum<{
|
|
779
|
+
user: "user";
|
|
780
|
+
assistant: "assistant";
|
|
781
|
+
}>;
|
|
782
|
+
text: z.ZodString;
|
|
783
|
+
createdAt: z.ZodNumber;
|
|
784
|
+
}, z.core.$strip>>>;
|
|
785
|
+
truncated: z.ZodOptional<z.ZodBoolean>;
|
|
786
|
+
}, z.core.$strip>]>;
|
|
787
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
788
|
+
v: z.ZodLiteral<1>;
|
|
789
|
+
}, z.core.$strip>]>;
|
|
755
790
|
export declare const guestMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
791
|
+
type: z.ZodLiteral<"action-result">;
|
|
792
|
+
id: z.ZodString;
|
|
793
|
+
payload: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
794
|
+
ok: z.ZodLiteral<true>;
|
|
795
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
796
|
+
ok: z.ZodLiteral<false>;
|
|
797
|
+
error: z.ZodString;
|
|
798
|
+
}, z.core.$strip>], "ok">;
|
|
799
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
800
|
+
v: z.ZodLiteral<1>;
|
|
801
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
756
802
|
type: z.ZodLiteral<"workspace-read">;
|
|
757
803
|
id: z.ZodString;
|
|
758
804
|
payload: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -831,6 +877,11 @@ export declare const guestMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
831
877
|
success: "success";
|
|
832
878
|
}>;
|
|
833
879
|
message: z.ZodString;
|
|
880
|
+
copy: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
881
|
+
text: z.ZodString;
|
|
882
|
+
}, z.core.$strip>]>>;
|
|
883
|
+
dismiss: z.ZodOptional<z.ZodBoolean>;
|
|
884
|
+
persistent: z.ZodOptional<z.ZodBoolean>;
|
|
834
885
|
}, z.core.$strip>;
|
|
835
886
|
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
836
887
|
v: z.ZodLiteral<1>;
|
package/dist/protocol.js
CHANGED
|
@@ -146,7 +146,7 @@ const readyPayloadSchema = z.object({
|
|
|
146
146
|
locale: z.string().min(1),
|
|
147
147
|
directory: z.string().nullable(),
|
|
148
148
|
session: sessionSnapshotSchema,
|
|
149
|
-
surface: z.enum(['panel', 'dialog', 'page']),
|
|
149
|
+
surface: z.enum(['panel', 'dialog', 'page', 'background']),
|
|
150
150
|
connection: guestConnectionSchema,
|
|
151
151
|
settings: guestSettingsSchema,
|
|
152
152
|
item: guestItemSchema,
|
|
@@ -253,9 +253,24 @@ export const hostMessageSchema = z.union([
|
|
|
253
253
|
}),
|
|
254
254
|
}),
|
|
255
255
|
hostResultSchema,
|
|
256
|
+
z.object({
|
|
257
|
+
...envelope,
|
|
258
|
+
type: z.literal('action'),
|
|
259
|
+
id: z.string().min(1),
|
|
260
|
+
payload: z.union([messageItemSchema, sessionItemSchema]),
|
|
261
|
+
}),
|
|
256
262
|
]);
|
|
257
263
|
const filePathSchema = z.string().min(1).max(GUEST_FILE_PATH_MAX).refine(isGuestFilePath);
|
|
258
264
|
export const guestMessageSchema = z.discriminatedUnion('type', [
|
|
265
|
+
z.object({
|
|
266
|
+
...envelope,
|
|
267
|
+
type: z.literal('action-result'),
|
|
268
|
+
id: z.string().min(1),
|
|
269
|
+
payload: z.discriminatedUnion('ok', [
|
|
270
|
+
z.object({ ok: z.literal(true) }),
|
|
271
|
+
z.object({ ok: z.literal(false), error: z.string().trim().min(1).max(GUEST_RESOLVE_ERROR_MAX) }),
|
|
272
|
+
]),
|
|
273
|
+
}),
|
|
259
274
|
z.object({ ...envelope, type: z.literal('workspace-read'), id: z.string().min(1), payload: guestWorkspaceQuerySchema }),
|
|
260
275
|
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 }) }),
|
|
261
276
|
z.object({ ...envelope, type: z.literal('workspace-unsubscribe'), id: z.string().min(1), payload: z.object({ subscriptionId: z.string().min(1).max(128) }) }),
|
|
@@ -272,6 +287,9 @@ export const guestMessageSchema = z.discriminatedUnion('type', [
|
|
|
272
287
|
payload: z.object({
|
|
273
288
|
kind: z.enum(['info', 'success', 'error']),
|
|
274
289
|
message: z.string().trim().min(1).max(GUEST_TOAST_MAX),
|
|
290
|
+
copy: z.union([z.boolean(), z.object({ text: z.string().min(1).max(GUEST_CLIPBOARD_TEXT_MAX) })]).optional(),
|
|
291
|
+
dismiss: z.boolean().optional(),
|
|
292
|
+
persistent: z.boolean().optional(),
|
|
275
293
|
}),
|
|
276
294
|
}),
|
|
277
295
|
z.object({
|
package/dist/ui/menu.js
CHANGED
|
@@ -51,8 +51,12 @@ export const mountMenu = (root, initial) => {
|
|
|
51
51
|
setActive(actions(props.items).some((item) => item.id === activeId) ? activeId : null);
|
|
52
52
|
};
|
|
53
53
|
const close = () => {
|
|
54
|
-
|
|
54
|
+
// Removing a focused row fires `focusout` synchronously from inside the disposer, and
|
|
55
|
+
// that handler calls `close` again. Clearing the slot first makes the re-entry a no-op
|
|
56
|
+
// instead of a second `popup.remove()` that throws before the pick reaches `onChange`.
|
|
57
|
+
const dispose = closePopup;
|
|
55
58
|
closePopup = null;
|
|
59
|
+
dispose?.();
|
|
56
60
|
activeId = null;
|
|
57
61
|
triggerNode?.setAttribute('aria-expanded', 'false');
|
|
58
62
|
};
|
package/dist/ui/select.js
CHANGED
|
@@ -63,8 +63,12 @@ export const mountSelect = (root, initial) => {
|
|
|
63
63
|
setActive(options.some((option) => option.id === activeId) ? activeId : options[0]?.id ?? null);
|
|
64
64
|
};
|
|
65
65
|
const close = () => {
|
|
66
|
-
|
|
66
|
+
// Removing a focused row fires `focusout` synchronously from inside the disposer, and
|
|
67
|
+
// that handler calls `close` again. Clearing the slot first makes the re-entry a no-op
|
|
68
|
+
// instead of a second `popup.remove()` that throws before the pick reaches `onChange`.
|
|
69
|
+
const dispose = closePopup;
|
|
67
70
|
closePopup = null;
|
|
71
|
+
dispose?.();
|
|
68
72
|
query = '';
|
|
69
73
|
search.value = '';
|
|
70
74
|
trigger.setAttribute('aria-expanded', 'false');
|