@openchamber/sdk 1.23.2-preview.1 → 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 +73 -27
- package/DOCUMENTATION.md +43 -4
- package/README.md +68 -5
- package/dist/contract.d.ts +36 -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 +3 -2
- package/dist/index.js +1 -0
- 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 +57 -1
- package/dist/protocol.js +24 -1
- package/dist/scrollbar-style.d.ts +2 -0
- package/dist/scrollbar-style.js +32 -0
- package/dist/ui/menu.js +5 -1
- package/dist/ui/select.js +5 -1
- package/dist/ui/style.js +33 -21
- package/dist/ui/theme.js +10 -0
- package/package.json +2 -2
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,12 +86,16 @@ 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` |
|
|
92
93
|
|
|
93
94
|
|
|
95
|
+
`theme.tokens` includes `primaryText`, `successText`, `warningText`, `errorText`, and `infoText`. The host computes these for text on neutral surfaces and the UI kit's tinted controls. Keep using the base colors for fills and `primaryForeground` for text on a solid primary fill.
|
|
96
|
+
|
|
97
|
+
`applyHostReady` exposes the computed colors as `--primary-text`, `--success-text`, `--warning-text`, `--error-text`, and `--info-text`, with matching `--oc-*-text` aliases. These are required theme fields. Apply each `onReady` snapshot to update them when the theme changes.
|
|
98
|
+
|
|
94
99
|
`GuestItem` is `AttachIssueRequest | GuestMessageItem | GuestSessionItem`:
|
|
95
100
|
|
|
96
101
|
```ts
|
|
@@ -126,7 +131,7 @@ Access tokens never appear in `ready` or in request results.
|
|
|
126
131
|
|
|
127
132
|
| Method | Arguments | Returns | Behavior |
|
|
128
133
|
| ----------------- | --------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
129
|
-
| `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 |
|
|
130
135
|
| `openUrl` | `url: string` | `Promise<void>` | Open URL in the host |
|
|
131
136
|
| `openSurface` | `surfaceId: string` | `Promise<void>` | Switch host chrome to that surface |
|
|
132
137
|
| `writeClipboard` | `text: string` | `Promise<void>` | Copy in the host (1–32000 chars) |
|
|
@@ -212,6 +217,30 @@ Storage belongs to the extension on the connected server and needs no extra capa
|
|
|
212
217
|
|
|
213
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.
|
|
214
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
|
+
|
|
215
244
|
### 1.3 Error codes (`HostRequestError.code`)
|
|
216
245
|
|
|
217
246
|
|
|
@@ -255,6 +284,7 @@ Storage belongs to the extension on the connected server and needs no extra capa
|
|
|
255
284
|
| Request response | 256 000 |
|
|
256
285
|
| Request timeout | 20 000 ms |
|
|
257
286
|
| `resolve` answer (host waits) | 20 000 ms |
|
|
287
|
+
| Background action loading and execution | 20 000 ms |
|
|
258
288
|
| Badge count | 999 |
|
|
259
289
|
| Message item `text` | 200 000 |
|
|
260
290
|
| Session item (serialized) | 2 000 000 |
|
|
@@ -278,14 +308,34 @@ Always call theme first:
|
|
|
278
308
|
```ts
|
|
279
309
|
import { applyHostReady, mountList } from '@openchamber/sdk/ui';
|
|
280
310
|
|
|
311
|
+
let mounted = false;
|
|
281
312
|
host.onReady((ctx) => {
|
|
282
313
|
applyHostReady(ctx, document.documentElement);
|
|
314
|
+
if (mounted) return;
|
|
315
|
+
mounted = true;
|
|
283
316
|
// then mount…
|
|
284
317
|
});
|
|
285
318
|
```
|
|
286
319
|
|
|
287
320
|
Every mount returns `{ update(partial), dispose() }`. `update` merges the fields you pass and repaints; `dispose` removes the node and its listeners.
|
|
288
321
|
|
|
322
|
+
Selection controls report a proposed value; the caller commits it with `update`. Use `activeId` for tabs, `value` for selects, and `checked` for checkboxes and switches. Text and search inputs display typing immediately, but call `update({ value })` too so later updates do not restore stale props. Button clicks do not change `variant`; use tabs for mode selection. Preserve input state across tab changes instead of remounting empty fields.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { mountTabs } from '@openchamber/sdk/ui';
|
|
326
|
+
|
|
327
|
+
let activeId = 'convert';
|
|
328
|
+
const tabs = mountTabs(root, {
|
|
329
|
+
items: [{ id: 'convert', label: 'Convert' }, { id: 'format', label: 'Format' }],
|
|
330
|
+
activeId,
|
|
331
|
+
onChange: (next) => {
|
|
332
|
+
activeId = next;
|
|
333
|
+
tabs.update({ activeId });
|
|
334
|
+
// Show the matching panel while preserving its draft values.
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
289
339
|
### 2.1 Theme
|
|
290
340
|
|
|
291
341
|
|
|
@@ -376,10 +426,11 @@ Used by the OpenChamber host and by tools that validate packages. Guests rarely
|
|
|
376
426
|
| `engines.openchamber` | Optional. Only `1.22.0` or `>=1.22.0`. Older host → `host-too-old` |
|
|
377
427
|
| `panel.id` | kebab-case |
|
|
378
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 |
|
|
379
|
-
| `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` |
|
|
380
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` |
|
|
381
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 |
|
|
382
|
-
| `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 |
|
|
383
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 |
|
|
384
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 |
|
|
385
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 |
|
|
@@ -398,7 +449,7 @@ Extra keys are dropped, not forwarded.
|
|
|
398
449
|
| `parseManifestJson(json)` (`@openchamber/sdk/schemas`) | String → same result |
|
|
399
450
|
| `resolveAttachMode(attach)` | Normalize to `'panel' |
|
|
400
451
|
| `resolveAttachEntry(contributes)` | Dialog page from the object form, or `null` when the dialog reuses `panel.entry` |
|
|
401
|
-
| `hasGuestPage(contributes)`
|
|
452
|
+
| `hasGuestPage(contributes)` | `true` when `panel.entry` is set; background-only and tools-only packages return `false` |
|
|
402
453
|
| `resolveIntegrationAuth` / `resolveIntegrationApi` | Auth kind and API origin |
|
|
403
454
|
| `toPublicIntegration` / `toPublicService` | Catalog-safe public slices |
|
|
404
455
|
| `isGuestPackageSvgIcon` | Whether icon is a package SVG path |
|
|
@@ -470,25 +521,29 @@ Do not go around the guest contract through `RuntimeAPIs`.
|
|
|
470
521
|
|
|
471
522
|
## 6. Minimal panel sketch
|
|
472
523
|
|
|
524
|
+
Mount once and keep the handles. `onReady` can repeat when the session or theme changes. Field listeners replay their current values too; compare relevant fields before fetching data again. For a complete request example with stale-response handling, see [the extension example](https://docs.openchamber.dev/sdk/example/).
|
|
525
|
+
|
|
473
526
|
```ts
|
|
474
|
-
import { connectHost
|
|
527
|
+
import { connectHost } from '@openchamber/sdk';
|
|
475
528
|
import { applyHostReady, mountList, mountEmpty } from '@openchamber/sdk/ui';
|
|
476
529
|
|
|
477
530
|
const host = connectHost();
|
|
478
531
|
const root = document.querySelector('#root')!;
|
|
479
532
|
|
|
533
|
+
let mounted = false;
|
|
480
534
|
host.onReady((ctx) => {
|
|
481
535
|
applyHostReady(ctx, document.documentElement);
|
|
536
|
+
if (mounted) return;
|
|
537
|
+
mounted = true;
|
|
482
538
|
|
|
483
|
-
|
|
484
|
-
|
|
539
|
+
const signInRoot = root.appendChild(document.createElement('div'));
|
|
540
|
+
const listRoot = root.appendChild(document.createElement('div'));
|
|
541
|
+
mountEmpty(signInRoot, {
|
|
485
542
|
title: 'Connect Acme',
|
|
486
543
|
action: { label: 'Sign in', onClick: () => { void host.oauthStart(); } },
|
|
487
|
-
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
544
|
+
});
|
|
490
545
|
|
|
491
|
-
mountList(
|
|
546
|
+
mountList(listRoot, {
|
|
492
547
|
items: [], // fill from host.request
|
|
493
548
|
onSelect: (id) => {
|
|
494
549
|
void host.attach({
|
|
@@ -499,18 +554,10 @@ host.onReady((ctx) => {
|
|
|
499
554
|
});
|
|
500
555
|
},
|
|
501
556
|
});
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
try {
|
|
507
|
-
const res = await host.request({ method: 'GET', path: '/api/v2/tasks' });
|
|
508
|
-
// parse res.body, then remount / update the list
|
|
509
|
-
} catch (error) {
|
|
510
|
-
if (error instanceof HostRequestError && error.code === 'DISCONNECTED') {
|
|
511
|
-
await host.oauthStart();
|
|
512
|
-
}
|
|
513
|
-
}
|
|
557
|
+
host.onConnection((connection) => {
|
|
558
|
+
signInRoot.hidden = connection.connected;
|
|
559
|
+
listRoot.hidden = !connection.connected;
|
|
560
|
+
});
|
|
514
561
|
});
|
|
515
562
|
```
|
|
516
563
|
|
|
@@ -526,4 +573,3 @@ host.onConnection(async (connection) => {
|
|
|
526
573
|
| [GUEST_SERVICES.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/GUEST_SERVICES.md) | Local service contract |
|
|
527
574
|
| [src/ui/DOCUMENTATION.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/src/ui/DOCUMENTATION.md) | UI kit invariants |
|
|
528
575
|
| [sdk.mdx](https://github.com/openchamber/openchamber/blob/main/packages/docs/content/docs/sdk.mdx) / [sdk/host.mdx](https://github.com/openchamber/openchamber/blob/main/packages/docs/content/docs/sdk/host.mdx) / [sdk/ui.mdx](https://github.com/openchamber/openchamber/blob/main/packages/docs/content/docs/sdk/ui.mdx) | Author-facing website pages |
|
|
529
|
-
|
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,9 +29,10 @@ 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
|
|
31
|
-
- 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 `
|
|
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.
|
|
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.
|
|
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.
|
|
33
36
|
- `connectHost` rejects when `parent === self`. A silent host rejects in-flight RPCs as `HOST_TIMEOUT` after 20s. Dispose rejects the rest as `HOST_UNAVAILABLE`.
|
|
34
37
|
- Failed `result` may carry `code`. Known guest codes are `HOST_UNAVAILABLE`, `HOST_TIMEOUT`, `HOST_REJECTED`, `DISCONNECTED`, `DISABLED`, `BAD_PATH`, `NO_INTEGRATION`, `NO_SESSION`, `SESSION_BUSY`, `NO_SERVICE`, `SERVICE_FAILED`, `NOT_GRANTED`, `NO_DIRECTORY`, `NOT_FOUND`, `FILE_TOO_LARGE`, `DENIED`, `NO_MODEL`, `MODEL_FAILED`. Anything else becomes `HOST_REJECTED`. The `error` string stays.
|
|
35
38
|
- `parseManifest` takes a typed document. File or network bytes go through `parseManifestJson`. Both return a result and do not throw on junk.
|
|
@@ -45,7 +48,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
45
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.
|
|
46
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.
|
|
47
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.
|
|
48
|
-
- `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.
|
|
49
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.
|
|
50
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.
|
|
51
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.
|
|
@@ -56,6 +59,18 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
56
59
|
|
|
57
60
|
## Host hole
|
|
58
61
|
|
|
62
|
+
Built-in extension source and registration rules live in `packages/extensions/DOCUMENTATION.md`. The server assigns `source: 'bundled'` only from its app-owned registry and automatically grants the package's declared capabilities. Built-ins use the same SDK, sandbox and enabled-state checks as user extensions; the `openchamber-builtin-` namespace is reserved. Their account cards appear under Built-in integrations, and their persisted disabled state and data survive app updates.
|
|
63
|
+
|
|
64
|
+
Relay panels use `lib/guests/relay-document.ts`: fetch package HTML and its statically referenced scripts, styles, CSS imports/URLs, images, fonts, and nested documents through `runtimeFetch`, then render self-contained `srcDoc` with the same `sandbox="allow-scripts"`. A normal iframe URL bypasses the client-side tunnel and can load the local host UI instead. Local resource paths stay under that guest's package; missing files, import cycles, or resource limits fail the load. The loader allows 500 referenced files, 40 MiB of fetched bytes and 64 MiB of embedded text. Runtime-created relative URLs are not intercepted: bundle those assets into the IIFE or use explicit external URLs/SDK operations. Existing authored CSP is preserved. Direct iframe URLs retain their existing path.
|
|
65
|
+
|
|
66
|
+
`useGuestIconSource` loads package SVGs through authenticated `runtimeFetch` in direct and relay modes, then exposes revocable blob URLs to CSS masks. It uses the package path rather than the original URL token, so cold-start token readiness and token expiry cannot strand an icon. Cache identity follows runtime, package path and extension version; late responses are discarded and old blobs revoked. Unknown icons and SVGs that are loading or failed use the shared `FALLBACK_GUEST_ICON`, the `apps` glyph.
|
|
67
|
+
|
|
68
|
+
Git installation accepts HTTPS, `ssh://` and scp-style SSH addresses. `POST /api/guests` accepts optional `gitIdentityId`; the server stores only that ID in the git origin, and resolves the profile again for clones and update fetches. Global/omitted identity uses the server process's Git/SSH configuration and agent. The Settings picker loads identities from the active runtime, discards stale loads, and never reuses a desktop's cached profiles on a remote server. SSH parsing, public-host validation and pinned connection options belong to `packages/web/server/lib/guests/clone.js`.
|
|
69
|
+
|
|
70
|
+
Catalog loading and installation retain typed request diagnostics in `lib/guests/request-failure.ts`: fixed API route, method, HTTP status, or a transport/invalid-response classification. Settings shows catalog diagnostics and passes install diagnostics to the toast description so Copy includes them. Response bodies, headers, credentials, and arbitrary transport error messages are excluded. Catalog failures preserve a previously loaded list; diagnostics clear on success or runtime switch, and late failures from a retired runtime are ignored.
|
|
71
|
+
|
|
72
|
+
`lib/guests/useGuestFrameUrl.ts` owns iframe URL lifetime. It retains the scoped token's expiry, renews on an expired navigation, and remints after enable/version/runtime changes. Expiry alone never reloads a healthy document. Recovery is bounded until the guest's next `hello`; mint failure leaves the existing load-failed UI rather than mounting an unauthenticated URL. Late mint responses from a retired owner are discarded. Guest capability grants and provider OAuth are separate from this host file-loading authorization.
|
|
73
|
+
|
|
59
74
|
`GET /api/guests` plus `PluginPane` on the desktop/web rail. The iframe URL carries a token minted for `guest:<id>` scope (`resolveGuestFrameUrl`), valid only for `GET /api/guests/<id>/*`; the session-wide URL token never enters a guest frame. Settings → Extensions installs a folder, a local `.zip`, or an https git / zip URL into that OpenChamber instance's data dir (`extensions.json`). Settings → Integrations shows a host card for each installed guest that declared `integration`. OAuth and pasted tokens live in `guest-auth.json`. Host Linear uses `linear-auth.json`. VS Code and mobile set the catalog to `unsupported`. That is not an empty success. A guest must ship a classic IIFE (`panel/main.js`). No runtime compiles guest TypeScript. The iframe cannot load ESM.
|
|
60
75
|
|
|
61
76
|
**Folder install** stores the realpath in `extensions.json` with `source: "path"`. Uninstall drops the catalog row and extension-owned storage. The user's folder stays.
|
|
@@ -68,12 +83,36 @@ File calls go through `POST /api/guests/:id/files` with the `x-opencode-director
|
|
|
68
83
|
|
|
69
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.
|
|
70
85
|
|
|
71
|
-
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.
|
|
72
89
|
|
|
73
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.
|
|
74
91
|
|
|
75
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.
|
|
76
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
|
+
|
|
77
116
|
## Not here yet
|
|
78
117
|
|
|
79
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
|
|
|
@@ -42,10 +42,12 @@ bunx openchamber-guest-bundle panel/main.ts panel/main.js
|
|
|
42
42
|
|
|
43
43
|
Then install the folder from Settings → Extensions → Add. Folder installs run from your folder, so edit, rebuild, and reload. A `.zip` or an https git or zip link is copied into OpenChamber's data folder instead; ship the built files only. Git installs can update from Settings → Extensions when the repository's `version` is newer than the installed one, so bump `version` to ship an update; `https://…/panel.git#v1` pins a tag or branch.
|
|
44
44
|
|
|
45
|
-
A complete three-file example is on the [Build an extension](https://openchamber.dev/docs/sdk/) page.
|
|
45
|
+
A complete three-file example is on the [Build an extension](https://openchamber.dev/docs/sdk/) page. Six examples are at [github.com/openchamber/openchamber/tree/main/packages/sdk/examples](https://github.com/openchamber/openchamber/tree/main/packages/sdk/examples).
|
|
46
46
|
|
|
47
47
|
## Manifest
|
|
48
48
|
|
|
49
|
+
Git installs also accept SSH addresses such as `git@github.com:owner/extension.git` and `ssh://git@github.com/owner/extension.git`. The fingerprint menu in Settings → Extensions selects Global Identity or a Git identity on the active server. That identity is reused for update checks and updates. On a remote instance, SSH keys and any unlocked SSH agent must be available to the server process, not just your desktop. See [Extensions](https://docs.openchamber.dev/extensions/) for details.
|
|
50
|
+
|
|
49
51
|
```json
|
|
50
52
|
{
|
|
51
53
|
"name": "@acme/hello",
|
|
@@ -86,15 +88,71 @@ A complete three-file example is on the [Build an extension](https://openchamber
|
|
|
86
88
|
- `version` is required semver. Settings → Extensions shows it on the card.
|
|
87
89
|
- `apiVersion` is `1`. Anything else is refused.
|
|
88
90
|
- `engines.openchamber` is optional (`1.24.0` or `>=1.24.0`). Older OpenChamber builds refuse the install.
|
|
89
|
-
- `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.
|
|
90
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.
|
|
91
|
-
- `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.
|
|
92
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.
|
|
93
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.
|
|
94
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`.
|
|
95
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`.
|
|
96
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).
|
|
97
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
|
+
|
|
98
156
|
## In the page
|
|
99
157
|
|
|
100
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.
|
|
@@ -160,13 +218,16 @@ Every method, its limits, and the error codes are on the [Host API](https://open
|
|
|
160
218
|
|
|
161
219
|
## UI kit
|
|
162
220
|
|
|
163
|
-
`@openchamber/sdk/ui` has buttons, fields, a searchable dropdown, checkboxes, tabs, badges, lists, empty states, spinners, banners, separators, progress bars, menus, and safe text, all drawn with the app's colours and fonts.
|
|
221
|
+
`@openchamber/sdk/ui` has buttons, fields, a searchable dropdown, checkboxes, tabs, badges, lists, empty states, spinners, banners, separators, progress bars, menus, and safe text, all drawn with the app's colours and fonts. Apply `applyHostReady` on every `onReady`, but mount controls and register listeners once. Repeated snapshots must not erase inputs or drafts. Every mount returns `{ update, dispose }`. Use `update` to pass changed values back to controls, including `tabs.update({ activeId })` and `select.update({ value })` inside `onChange`. See the [UI kit examples](https://docs.openchamber.dev/sdk/ui/) for input state and tab switching.
|
|
164
222
|
|
|
165
223
|
```ts
|
|
166
224
|
import { applyHostReady, mountList } from '@openchamber/sdk/ui';
|
|
167
225
|
|
|
226
|
+
let mounted = false;
|
|
168
227
|
host.onReady((ctx) => {
|
|
169
228
|
applyHostReady(ctx, document.documentElement);
|
|
229
|
+
if (mounted) return;
|
|
230
|
+
mounted = true;
|
|
170
231
|
mountList(document.querySelector('#root')!, {
|
|
171
232
|
items: tasks.map((task) => ({ id: task.id, leading: task.key, title: task.title })),
|
|
172
233
|
onSelect: (id) => {
|
|
@@ -177,6 +238,8 @@ host.onReady((ctx) => {
|
|
|
177
238
|
});
|
|
178
239
|
```
|
|
179
240
|
|
|
241
|
+
OpenChamber supplies thin, theme-aware native scrollbars inside extension documents, including nested lists, tabs, and textareas. The UI kit includes the same defaults for development previews. Existing installed bundles get the host stylesheet without rebuilding. Custom rendering hosts can use `GUEST_SCROLLBAR_CSS` from `@openchamber/sdk`. Authors can override these default rules; an extension's CSP still applies.
|
|
242
|
+
|
|
180
243
|
## Schemas
|
|
181
244
|
|
|
182
245
|
`@openchamber/sdk/schemas` exports the zod schemas for the manifest and the messages, for tools that validate extensions. The main entry has no zod dependency, so a page bundle stays small.
|
package/dist/contract.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export type HostThemeTokens = {
|
|
|
22
22
|
selectionForeground: string;
|
|
23
23
|
/** Text on `primary`. */
|
|
24
24
|
primaryForeground: string;
|
|
25
|
+
/** Host-computed text on neutral or tinted surfaces, not on a solid fill. */
|
|
26
|
+
primaryText: string;
|
|
27
|
+
successText: string;
|
|
28
|
+
warningText: string;
|
|
29
|
+
errorText: string;
|
|
30
|
+
infoText: string;
|
|
25
31
|
success: string;
|
|
26
32
|
warning: string;
|
|
27
33
|
error: string;
|
|
@@ -45,7 +51,7 @@ export type SessionSnapshot = {
|
|
|
45
51
|
agent?: string;
|
|
46
52
|
};
|
|
47
53
|
/** Which host chrome mounted this iframe. Not `openSurface`. */
|
|
48
|
-
export type GuestHostSurface = 'panel' | 'dialog' | 'page';
|
|
54
|
+
export type GuestHostSurface = 'panel' | 'dialog' | 'page' | 'background';
|
|
49
55
|
export type GuestConnection = {
|
|
50
56
|
connected: boolean;
|
|
51
57
|
account: string;
|
|
@@ -202,6 +208,14 @@ export type GuestSessionItem = {
|
|
|
202
208
|
truncated?: boolean;
|
|
203
209
|
};
|
|
204
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
|
+
};
|
|
205
219
|
export declare const isGuestMessageItem: (item: GuestItem | null) => item is GuestMessageItem;
|
|
206
220
|
export declare const isGuestSessionItem: (item: GuestItem | null) => item is GuestSessionItem;
|
|
207
221
|
/** The composer chip: what the guest handed to `attach`, with `text` filled from the chip. */
|
|
@@ -225,6 +239,14 @@ export type ToastKind = 'info' | 'success' | 'error';
|
|
|
225
239
|
export type ToastRequest = {
|
|
226
240
|
kind: ToastKind;
|
|
227
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;
|
|
228
250
|
};
|
|
229
251
|
export type ComposeRequest = {
|
|
230
252
|
text: string;
|
|
@@ -389,6 +411,11 @@ export type HostResolveMessage = Envelope & {
|
|
|
389
411
|
id: string;
|
|
390
412
|
payload: ResolveRequest;
|
|
391
413
|
};
|
|
414
|
+
export type HostActionMessage = Envelope & {
|
|
415
|
+
type: 'action';
|
|
416
|
+
id: string;
|
|
417
|
+
payload: GuestActionItem;
|
|
418
|
+
};
|
|
392
419
|
export type HostResultMessage = Envelope & {
|
|
393
420
|
type: 'result';
|
|
394
421
|
id: string;
|
|
@@ -403,7 +430,7 @@ export type HostResultMessage = Envelope & {
|
|
|
403
430
|
export type HostMessage = (Envelope & {
|
|
404
431
|
type: 'workspace';
|
|
405
432
|
payload: GuestWorkspaceUpdate;
|
|
406
|
-
}) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostResolveMessage | HostResultMessage;
|
|
433
|
+
}) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostResolveMessage | HostActionMessage | HostResultMessage;
|
|
407
434
|
type GuestCall<Type extends string, Payload = never> = Envelope & {
|
|
408
435
|
type: Type;
|
|
409
436
|
id: string;
|
|
@@ -448,11 +475,17 @@ export type GuestResolveResultMessage = Envelope & {
|
|
|
448
475
|
id: string;
|
|
449
476
|
payload: ResolveResultPayload;
|
|
450
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
|
+
};
|
|
451
484
|
export type GuestMessage = GuestCall<'workspace-read', GuestWorkspaceQuery> | GuestCall<'workspace-subscribe', GuestWorkspaceSubscription> | GuestCall<'workspace-unsubscribe', {
|
|
452
485
|
subscriptionId: string;
|
|
453
486
|
}> | GuestCall<'storage', GuestStorageRequest> | GuestCall<'open-session', {
|
|
454
487
|
sessionId: string;
|
|
455
|
-
}> | 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;
|
|
456
489
|
export declare const isServiceStatusResult: (value: HostResultPayload | undefined) => value is ServiceStatusResult;
|
|
457
490
|
export declare const isGuestRequestResult: (value: HostResultPayload | undefined) => value is GuestRequestResult;
|
|
458
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>;
|