@openchamber/sdk 2.1.0 → 2.1.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 +33 -6
- package/DOCUMENTATION.md +14 -4
- package/README.md +13 -1
- package/dist/contract.d.ts +41 -3
- package/dist/contract.js +1 -0
- package/dist/host.d.ts +15 -5
- package/dist/host.js +144 -6
- package/dist/index.d.ts +7 -3
- package/dist/index.js +3 -1
- package/dist/manifest.d.ts +2 -0
- package/dist/parse.d.ts +8 -4
- package/dist/parse.js +2 -0
- package/dist/popover-schemas.d.ts +33 -0
- package/dist/popover-schemas.js +74 -0
- package/dist/popover.d.ts +37 -0
- package/dist/popover.js +57 -0
- package/dist/protocol.d.ts +125 -0
- package/dist/protocol.js +32 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +2 -0
- package/dist/status-control-schemas.d.ts +21 -0
- package/dist/status-control-schemas.js +29 -0
- package/dist/status-controls.d.ts +31 -0
- package/dist/status-controls.js +48 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/popover-anchor.d.ts +16 -0
- package/dist/ui/popover-anchor.js +198 -0
- package/dist/ui/theme.js +15 -0
- package/dist/workspace-schemas.d.ts +16 -0
- package/dist/workspace-schemas.js +4 -4
- package/dist/workspace.d.ts +10 -4
- package/dist/workspace.js +2 -0
- package/package.json +1 -1
package/API.md
CHANGED
|
@@ -60,7 +60,7 @@ Throws `HOST_UNAVAILABLE` when there is no `window`. Outside an iframe (`parent
|
|
|
60
60
|
|
|
61
61
|
### 1.1 Subscriptions (host pushes)
|
|
62
62
|
|
|
63
|
-
Each returns an unsubscribe function.
|
|
63
|
+
Each returns an unsubscribe function. State subscriptions replay the last known value from `ready` or the last dedicated push. Control activations and popover-close events are delivered only to current listeners.
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
| Method | Payload | Notes |
|
|
@@ -72,6 +72,8 @@ Each returns an unsubscribe function. Late subscribers get the last known value
|
|
|
72
72
|
| `onConnection(listener)` | `GuestConnection` | `{ connected, account }` |
|
|
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
|
+
| `onStatusControl(listener)` | `{ id, value? }` | Host Work Status control activation. Register synchronously; only status frames with advertised status controls receive events |
|
|
76
|
+
| `onPopoverClosed(listener)` | `{ id, reason }` | Dismissal of this frame's anchored popover. Events go to the owner, are not replayed, and identify the opening being closed |
|
|
75
77
|
| `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
78
|
| `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 |
|
|
77
79
|
| `onFileOpen(listener)` | `FileEditorDocument` | File editors only: `{ path, name, readOnly, encoding: 'text', content }` or `{ ..., encoding: 'binary', bytes }` (a `Uint8Array`) of the file this frame edits. Replays the last file; the same file pushed again is not repeated. Registering also sends Cmd/Ctrl+S inside the frame to the host's save |
|
|
@@ -89,7 +91,8 @@ Each returns an unsubscribe function. Late subscribers get the last known value
|
|
|
89
91
|
| `locale` | `string` | Host language tag |
|
|
90
92
|
| `directory` | `string | null` |
|
|
91
93
|
| `session` | snapshot or `null` | Title falls back to `id`. `busy` is live status. `model` is `providerID/id` when present |
|
|
92
|
-
| `surface` | `'panel' \| 'dialog' \| 'page' \| 'background' \| 'status' \| 'file'` | Where the host mounted this frame |
|
|
94
|
+
| `surface` | `'panel' \| 'dialog' \| 'page' \| 'background' \| 'status' \| 'file' \| 'popover'` | Where the host mounted this frame |
|
|
95
|
+
| `popover` | optional `{ id, data }` | Present in a host-created popover frame. `data` is bounded JSON from the owning frame |
|
|
93
96
|
| `connection` | `{ connected, account }` | Integration link state |
|
|
94
97
|
| `settings` | `Record<string, string>` | Declared keys only |
|
|
95
98
|
| `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` |
|
|
@@ -97,6 +100,8 @@ Each returns an unsubscribe function. Late subscribers get the last known value
|
|
|
97
100
|
|
|
98
101
|
`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.
|
|
99
102
|
|
|
103
|
+
`features` is optional and only advertises implemented host operations: `deviceStorage?: true`, `statusControls?: true`, and `popovers?: true`. Optional syntax tokens are `syntaxKeyword`, `syntaxString`, `syntaxNumber`, `syntaxFunction`, `syntaxType`, `syntaxComment`, `syntaxVariable`, and `syntaxOperator`. `applyHostReady` exposes each as `--oc-syntax-*` and the matching legacy `--syntax-*` name, and clears a missing token on a later snapshot. Use a semantic-token fallback in guest CSS when supporting older hosts.
|
|
104
|
+
|
|
100
105
|
`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.
|
|
101
106
|
|
|
102
107
|
`GuestItem` is `AttachIssueRequest | GuestMessageItem | GuestSessionItem`:
|
|
@@ -139,11 +144,11 @@ Access tokens never appear in `ready` or in request results.
|
|
|
139
144
|
| `openSurface` | `surfaceId: string` | `Promise<void>` | Switch host chrome to that surface |
|
|
140
145
|
| `writeClipboard` | `text: string` | `Promise<void>` | Copy in the host (1–32000 chars) |
|
|
141
146
|
| `compose` | `{ text, mode?: 'append' | 'replace' }` | `Promise<void>` |
|
|
142
|
-
| `attach` | `AttachIssueRequest` | `Promise<void>` | Composer chip
|
|
147
|
+
| `attach` | `AttachIssueRequest` | `Promise<void>` | Composer chip, alongside other attached items |
|
|
143
148
|
| `startSession` | `StartSessionRequest` | `Promise<{ sessionId, sent }>` | Create session (+ optional worktree), write snapshot. `text` can become first message |
|
|
144
149
|
| `prompt` | `{ text, send?: boolean }` | `Promise<{ sent }>` | Current session: omit/`false` = replace-compose; `send: true` = send |
|
|
145
150
|
| `sessionLink` | `AttachIssueRequest` | `Promise<void>` | Write snapshot on **current** session. Does not create one |
|
|
146
|
-
| `close` | — | `Promise<void>` | Dismiss attach dialog. No-op on the rail
|
|
151
|
+
| `close` | — | `Promise<void>` | Dismiss an attach dialog or the calling popover. No-op on the rail |
|
|
147
152
|
| `oauthStart` | — | `Promise<void>` | Open provider authorize URL (or first-party Linear) |
|
|
148
153
|
| `oauthDisconnect` | — | `Promise<void>` | Drop guest tokens / Linear connection |
|
|
149
154
|
| `request` | `{ method, path, query?, body? }` | `Promise<{ status, body }>` | HTTPS call on declared `apiOrigin`. Host attaches auth |
|
|
@@ -156,6 +161,10 @@ Access tokens never appear in `ready` or in request results.
|
|
|
156
161
|
| `setBadge` | `count: number \| null` | `Promise<void>` | Number on this guest's rail icon, 0–999 (clamped); `null` clears. Opening the panel clears it too. In memory only |
|
|
157
162
|
| `openCommit` | `sha: string` | `Promise<void>` | Show that commit of the open project in the host's Diff view (commit scope). 7–64 hex characters; the host reads the commit itself. `NO_DIRECTORY` without a project, `NOT_FOUND` for an unknown commit, `UNSUPPORTED` where the host has no Diff view |
|
|
158
163
|
| `setHeight` | `height: number` | `Promise<void>` | Content height in CSS px. The Work Status section sizes its frame to it, clamped to 24–320; taller content scrolls inside. A page docked to a shared surface grows or shrinks its dock to it (a width for a `left`/`right` dock), from 24 px up to half the panel. Other surfaces ignore it |
|
|
164
|
+
| `setStatusControls` | `GuestStatusControl[]` | `Promise<void>` | Replace host-rendered controls for this mounted status frame. Requires `features.statusControls`; old hosts reject with `UNSUPPORTED` |
|
|
165
|
+
| `openPopover` | `GuestPopoverRequest` | `Promise<void>` | Open a sandboxed frame outside this frame's bounds, anchored to a rectangle in its viewport. Requires `features.popovers` |
|
|
166
|
+
| `closePopover` | `id, reason?: 'closed' \| 'escape'` | `Promise<void>` | Close this frame's matching opening, or the popover frame itself. A stale id cannot close a replacement |
|
|
167
|
+
| `setPopoverAnchorActive` | `id, active: boolean` | `Promise<void>` | Report anchor pointer/focus activity so the host can allow travel between the anchor and popover |
|
|
159
168
|
| `generate` | `{ prompt, system?, maxOutputTokens? }` | `Promise<{ text }>` | One-off text from the user's Small Model (capability `model`). No session, no history; the host picks the model. Waits up to 90 s |
|
|
160
169
|
| `reportFileChange` | `{ dirty, edited }` | `void` | File editors only. `dirty` against the last saved version; `edited` when the document itself changed, which holds autosave back until edits stop |
|
|
161
170
|
| `requestFileSave` | — | `void` | File editors only. Save now, as Cmd/Ctrl+S does |
|
|
@@ -211,17 +220,21 @@ Projects contain `id`, `name`, `directory`. Worktrees contain `directory`, `name
|
|
|
211
220
|
|
|
212
221
|
### Extension storage
|
|
213
222
|
|
|
214
|
-
`host.storage.get(key)` returns JSON or `undefined` for a missing key. JSON `null` is a stored value. `set(key, value)` and `delete(key)` return `Promise<void>`; `keys()` returns `Promise<string[]>` in sorted order.
|
|
223
|
+
`host.storage.get(key, options?)` returns JSON or `undefined` for a missing key. JSON `null` is a stored value. `set(key, value, options?)` and `delete(key, options?)` return `Promise<void>`; `keys(options?)` returns `Promise<string[]>` in sorted order.
|
|
215
224
|
|
|
216
225
|
Storage belongs to the extension on the connected server and needs no extra capability. Keys contain 1 to 128 characters, each serialized value is at most 64 KiB UTF-8, and the complete namespace is at most 2 MiB and 2,000 keys. Use a project ID in your key when data belongs to one project. Concurrent operations serialize on the server, writes are atomic, and read/write failures preserve existing data. Uninstall deletes the namespace, including for folder installs.
|
|
217
226
|
|
|
227
|
+
Omit `options`, or pass `{ scope: 'instance' }`, for this existing server-owned storage. `{ scope: 'device' }` stores data in the current browser profile, scoped by the host runtime, guest ID, and server-issued installation identity. Use it only after `onReady` reports `features.deviceStorage: true`. Older hosts reject device requests with `UNSUPPORTED`; the SDK never changes them into instance requests. Different runtime identities keep separate preferences even when a server copies an installation identity. URL aliases are not guaranteed to share data; saved host and relay connections follow the host's existing runtime identity rules. Device namespaces have the same 2 MiB and 2,000-key limits, and serialized preference content across the browser database has a 16 MiB limit; browser metadata has separate overhead. A successful uninstall makes an installation namespace inaccessible, but an offline profile may retain those bytes until site data is removed. Device storage is for preferences, not secrets, credentials, or SSO state.
|
|
228
|
+
|
|
218
229
|
### Full-screen pages
|
|
219
230
|
|
|
220
231
|
`contributes.page: true` reuses `panel.entry`; `{ entry: 'panel/page.html', title?: 'Board' }` uses separate package HTML. It requires `panel.entry` and the same installed/approved/enabled state as the panel. The sidebar's Extension pages menu is the only page opener; `openSurface` does not open it. `ctx.surface` is `page`, `close()` closes it, and reload or runtime switch returns to chat. Pages use the existing sandbox and capabilities on web/desktop. VS Code and mobile remain unsupported.
|
|
221
232
|
|
|
222
233
|
### Work Status sections
|
|
223
234
|
|
|
224
|
-
`contributes.statusSection: true` reuses `panel.entry`; `{ entry: 'status/index.html', title?: 'Recent commits', height?: 160 }` uses separate package HTML (`.html`, inside the package, built scripts checked at install). The object form needs no `panel.entry`, so an extension can ship only a section and no rail icon. `title` is 1 to 60 characters and replaces `panel.name` on the section header; the icon is `panel.icon`. `height` (24 to 320, default 120) is the frame height before your page calls `setHeight`. The section appears in the chat's Work Status panel and in its section chooser, where the user can hide it or move it. `ctx.surface` is `status`. The frame runs only while the panel is shown and the section is expanded, so keep no state in it that you cannot rebuild. It gets the same sandbox, directory, session, theme, grants, and service as a panel. A status-only package may declare `capabilities`, `service`, `integration`, and `filesystem`; `page`, `attach`, `actions`, and `commands` still need `panel.entry` or `background.entry`. Web and desktop only.
|
|
235
|
+
`contributes.statusSection: true` reuses `panel.entry`; `{ entry: 'status/index.html', title?: 'Recent commits', height?: 160, defaultExpanded?: false, requiresProject?: true }` uses separate package HTML (`.html`, inside the package, built scripts checked at install). The object form needs no `panel.entry`, so an extension can ship only a section and no rail icon. `title` is 1 to 60 characters and replaces `panel.name` on the section header; the icon is `panel.icon`. `height` (24 to 320, default 120) is the frame height before your page calls `setHeight`. Omitted `defaultExpanded` preserves the existing expanded default; a saved user expansion choice still wins. `requiresProject` uses the actual Work Status directory and hides the section when no project is open. The section appears in the chat's Work Status panel and in its section chooser, where the user can hide it or move it. `ctx.surface` is `status`. The frame runs only while the panel is shown and the section is expanded, so keep no state in it that you cannot rebuild. It gets the same sandbox, directory, session, theme, grants, and service as a panel. A status-only package may declare `capabilities`, `service`, `integration`, and `filesystem`; `page`, `attach`, `actions`, and `commands` still need `panel.entry` or `background.entry`. Web and desktop only. `apiVersion` remains 1; VS Code and mobile remain unsupported.
|
|
236
|
+
|
|
237
|
+
`setStatusControls` accepts zero to four declarations. A button is `{ kind: 'button', id, label, disabled? }`; a select is `{ kind: 'select', id, label, value, options, disabled? }`, with one to 16 unique options and a selected value in that list. IDs are kebab-case and at most 40 characters; labels are 1 to 60 characters; option values are 1 to 80 characters. Register `onStatusControl` synchronously after `connectHost`, then feature-gate `setStatusControls` with `features.statusControls`. Buttons receive no `value`; select events carry a declared value. The host rejects stale, unknown, disabled, or invalid controls. It accepts at most 20 changed control publications per second per frame and avoids semantic no-op updates. Controls exist only for the expanded, mounted status frame that registered them. Folding or hiding the section removes controls and listeners. A project change clears header controls; publish them again from `onDirectory`. Workspace subscriptions remain active. Old hosts return `UNSUPPORTED`; keep a body control visible until `setStatusControls` succeeds.
|
|
225
238
|
|
|
226
239
|
### File editors
|
|
227
240
|
|
|
@@ -259,6 +272,20 @@ The 20-second deadline covers loading and execution. At most eight background in
|
|
|
259
272
|
|
|
260
273
|
See [Actions without opening a panel](./README.md#actions-without-opening-a-panel) for a toast example.
|
|
261
274
|
|
|
275
|
+
### Anchored popovers
|
|
276
|
+
|
|
277
|
+
`openPopover` accepts `{ id, anchor: { x, y, width, height }, width, height, side?, focus?, data }`. The anchor uses CSS pixels relative to the calling iframe's viewport. Width is 160..640 pixels, height is 48..480 pixels, and `data` is JSON limited to 16,000 serialized characters. IDs contain 1..80 ASCII letters, digits, underscores or hyphens. Use a new id for each opening. `side` is `left`, `right`, `top` or `bottom`, defaulting to `left`; the host flips or shifts the card to fit the viewport. `focus` defaults to false, so pointer previews do not take keyboard focus.
|
|
278
|
+
|
|
279
|
+
The host loads the caller's existing package entry in a fresh `sandbox="allow-scripts"` iframe with `surface: 'popover'`. Render the card from `ready.popover.data` instead of mounting your ordinary page. Apply subsequent theme snapshots without repeating data loads. The child inherits the extension's approved capabilities and existing host APIs. It cannot open another popover or choose an entry, URL or HTML document.
|
|
280
|
+
|
|
281
|
+
Visible web and desktop panel, page and status frames can advertise this feature. Dialogs, background frames, file editors, popovers, VS Code and mobile do not. Dialogs retain their own modal focus boundary. Older hosts reject opening with `UNSUPPORTED`; retain an inline or click-based detail view for them. Each opening loads a new frame, so cold loading and relay transport can delay the preview.
|
|
282
|
+
|
|
283
|
+
Only one extension popover is active per host document. Opening another retires the previous one. Escape, outside interaction, owner scrolling, resizing, owner removal and changes to project, runtime, installation or authorization dismiss it. A host modal dialog takes precedence and closes previews. Close events name the opening and one of `escape`, `outside`, `anchor`, `owner`, `closed`, or `replaced`. A closing child uses `host.close()` or `closePopover(ready.popover.id)`. The SDK forwards Escape and pointer/focus activity from the child, whose events cannot bubble across the sandbox.
|
|
284
|
+
|
|
285
|
+
A `focus: true` request requires the source frame to have focus. Non-focusing previews are constrained to a visible anchor in the source frame; the host cannot verify pointer activity inside an opaque-origin guest document. The child can use `host.setHeight` to fit its content within the popover height limit; this resizes only that child and includes the host border. Scrolling an unrelated host panel does not dismiss the preview.
|
|
286
|
+
|
|
287
|
+
Use `mountPopoverAnchor` from `@openchamber/sdk/ui` for DOM anchors. It measures the element, handles hover and keyboard opening, reports activity while the pointer crosses between frames, and retires the preview when the anchor scrolls away or is removed. Retain and call its `dispose()` when replacing rows. Low-level callers must perform those lifecycle steps themselves. The host can validate the source iframe and rectangle but cannot inspect an element inside its sandbox.
|
|
288
|
+
|
|
262
289
|
### 1.3 Error codes (`HostRequestError.code`)
|
|
263
290
|
|
|
264
291
|
|
package/DOCUMENTATION.md
CHANGED
|
@@ -27,19 +27,19 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
27
27
|
|
|
28
28
|
- 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`.
|
|
29
29
|
|
|
30
|
-
- `contributes.statusSection` is `true` or `{ entry, title?, height? }` (`StatusSectionContribution`). `true` reuses `panel.entry` and fails as `invalid-status-section` without one; the object form's `entry` is package-local `.html`, `title` 1 to `GUEST_STATUS_SECTION_TITLE_MAX` (60), `height` `GUEST_STATUS_SECTION_HEIGHT_MIN`..`MAX` (24..320). `resolveStatusSectionEntry` resolves shorthand. The object form is a frame of its own: without `panel.entry` or `background.entry` it permits `capabilities`, `service`, `integration`, and `filesystem`, and refuses `page`, `attach`, `actions`, and `commands` as `invalid-panel`, because those are opened or invoked elsewhere. The server validates the HTML/scripts, exposes `statusEntry`/`statusTitle`/`statusHeight`, and counts `statusEntry` as a runtime entry for serving and storage. The host mounts `PluginPane surface="status"` inside the Work Status panel (`components/chat/work-status/WorkStatusExtensionSection.tsx`, see that folder's `DOCUMENTATION.md`): the same sandbox, guest-scoped token, context, grants, approval, and pause gates as a panel. Guest `resize` (`host.setHeight`, `ResizeRequest { height }`, integer 0..`GUEST_FRAME_HEIGHT_MAX`) is a request answered with an empty result on every surface; only `status` sizes its frame from it, through `clampStatusSectionHeight`, and a page docked to a shared surface sizes its dock from it (`DockedGuestPage` in `ContextPanel`: thickness across the dock edge, at least `GUEST_SURFACE_DOCK_SIZE_MIN`, at most half the panel through CSS). Each frame's `setHeight` reaches only its own pane. A status frame never registers a command resolver, clears a badge, or closes rail tabs.
|
|
30
|
+
- `contributes.statusSection` is `true` or `{ entry, title?, height?, defaultExpanded?, requiresProject? }` (`StatusSectionContribution`). `true` reuses `panel.entry` and fails as `invalid-status-section` without one; the object form's `entry` is package-local `.html`, `title` 1 to `GUEST_STATUS_SECTION_TITLE_MAX` (60), `height` `GUEST_STATUS_SECTION_HEIGHT_MIN`..`MAX` (24..320). `defaultExpanded` defaults to true and only applies when the user has no saved expansion choice. `requiresProject` defaults to false; when true, the actual Work Status directory controls both presence and body rendering. `resolveStatusSectionEntry` resolves shorthand. The object form is a frame of its own: without `panel.entry` or `background.entry` it permits `capabilities`, `service`, `integration`, and `filesystem`, and refuses `page`, `attach`, `actions`, and `commands` as `invalid-panel`, because those are opened or invoked elsewhere. The server validates the HTML/scripts, exposes `statusEntry`/`statusTitle`/`statusHeight`, and counts `statusEntry` as a runtime entry for serving and storage. The host mounts `PluginPane surface="status"` inside the Work Status panel (`components/chat/work-status/WorkStatusExtensionSection.tsx`, see that folder's `DOCUMENTATION.md`): the same sandbox, guest-scoped token, context, grants, approval, and pause gates as a panel. Guest `resize` (`host.setHeight`, `ResizeRequest { height }`, integer 0..`GUEST_FRAME_HEIGHT_MAX`) is a request answered with an empty result on every surface; only `status` sizes its frame from it, through `clampStatusSectionHeight`, and a page docked to a shared surface sizes its dock from it (`DockedGuestPage` in `ContextPanel`: thickness across the dock edge, at least `GUEST_SURFACE_DOCK_SIZE_MIN`, at most half the panel through CSS). Each frame's `setHeight` reaches only its own pane. A status frame never registers a command resolver, clears a badge, or closes rail tabs. Host-rendered controls exist only after a mounted, expanded status frame calls `setStatusControls`; folding, hiding, or retiring the frame removes them. A project change clears header controls; publish them again from `onDirectory`. Workspace subscriptions remain active.
|
|
31
31
|
- `openCommit(sha)` posts `open-commit { sha }`. `sha` is 7 to 64 hex characters (`GUEST_COMMIT_SHA`); the client refuses anything else as `HOST_REJECTED` before posting and the host schema drops it. The host (`lib/guests/open-commit.ts`) reads the commit itself with `git.getGitLog(directory, { maxCount: 1, to: sha })` in the frame's current directory, checks the returned hash starts with the requested one (`NOT_FOUND` otherwise), selects it in `useCommitSelectionStore` under the Diff view's key (directory, current branch, git runtime key), and opens the context panel's Diff tab in `commit` scope. No project is `NO_DIRECTORY`. VS Code and mobile answer `UNSUPPORTED` explicitly; they load no guests today, so this is the contract for when they do. No capability is needed: it only shows the user their own history.
|
|
32
32
|
- Scrollbars in guest documents follow the app's overlay behavior: hidden at rest, shown on hover and for one second after the element last scrolled. `GUEST_SCROLLBAR_CSS` does the hiding and hover; `installGuestScrollbarActivity` (one capturing passive `scroll` listener per document) marks the scrolling element with `data-oc-scrolling`. The server appends both to every served guest HTML as `GUEST_SCROLLBAR_SCRIPT` (the function's own source), so already installed bundles get it without a rebuild; the UI kit installs it too for previews. An authored CSP that blocks inline scripts keeps the hover-only behavior. Forced-colors mode keeps system colors.
|
|
33
33
|
- `contributes.fileEditors` is 1 to `GUEST_FILE_EDITORS_MAX` (8) editors, each `{ id, title, match, entry }` (`FileEditorContribution`, `src/file-editor.ts`): unique kebab-case `id`, `title` 1 to `GUEST_FILE_EDITOR_TITLE_MAX` (60), `match` 1 to `GUEST_FILE_EDITOR_PATTERNS_MAX` (16) file-name globs (`isFileEditorPattern`: 1 to 128 characters, no `/`, `\` or NUL, not only wildcards; `matchesFileEditorPattern` compares the name case-insensitively, `*` any run, `?` one character), `entry` package-local `.html`, optional `content` `text` (default) or `binary` (`FileEditorContentKind`); anything else is `invalid-file-editors`. Like the status section, editors are frames of their own: without `panel.entry` or `background.entry` a package may still declare `capabilities`, `service`, `integration`, and `filesystem`, and `page`, `attach`, `actions`, `commands` are `invalid-panel`. The server checks each entry's HTML and built scripts at install, carries `fileEditors` on the catalog row, and counts them as runtime entries for serving and storage (`hasGuestFrame` in `server/lib/guests/catalog.js`). The host owns the file. Wire: host → guest `file-open { path, name, readOnly, encoding: 'text', content } | { ..., encoding: 'binary', bytes }` (a push, sent on `hello` and iframe load, never on other host-state pushes; `connectHost` drops a repeat of the same file), `file-snapshot { id, payload: { purpose: 'save' | 'handoff' } }` answered by guest `file-snapshot-result { id, payload: { snapshot: { content, version } | { bytes, version } } | { error } }` with no `result` back, and `file-saved { version }`; guest → host fire-and-forget `file-change { dirty, edited }`, `file-save`, and `file-unsupported` (no `id`, no answer). Bytes are a `Uint8Array<ArrayBuffer>`, copied by structured clone, so the host schema's `z.instanceof(Uint8Array)` holds in the receiving realm; `sameFileEditorDocument` compares them byte for byte for the repeat-push check. Content on both sides is capped at `GUEST_FILE_EDITOR_CONTENT_MAX` (20 000 000 characters or bytes, `fileEditorPayloadSize`); the guest refuses an oversized snapshot with an error instead of letting the host schema drop it. `onFileOpen` registration installs one capturing `keydown` listener that turns Cmd/Ctrl+S into `file-save`, because key events inside the frame never reach the host. `createFileSaveTracker` is the pure dirty bookkeeping: a written snapshot's version is marked saved, so edits made during the write stay dirty. The host side is `packages/ui/src/lib/guests/file-editor-channel.ts` (one channel per mounted editor; disconnect rejects every outstanding snapshot request), `PluginPane surface="file"`, and `components/views/files/GuestFileEditor.tsx`; the Files view contract is in that folder's `DOCUMENTATION.md`. VS Code and mobile keep the catalog empty, so files there stay with the host's own viewers.
|
|
34
34
|
- `contributes.page` is `true` or `{ entry, title? }`, requires `panel.entry`, and adds a user-opened full-screen page. `resolvePageEntry` resolves shorthand; the server validates its HTML/scripts and exposes `pageEntry`/`pageTitle`. `PluginPane surface="page"` uses the same sandbox. `useUIStore.openGuestPageId` is transient and mutually exclusive with other main pages. The sidebar menu opens it; `close()` or explicit session navigation closes it. Pages are not context-rail surfaces.
|
|
35
35
|
- `workspace.ts` owns project/worktree/session snapshots and storage types; `workspace-schemas.ts` validates their wire contracts. The existing `sessions` grant covers lists, subscriptions, opening sessions, and targeted creation. `lib/guests/workspace.ts` projects shared project, topology, session, and live-status stores; reads perform no network or git discovery. A registered observer is shared per runtime/extension/query and stops with its last listener. `PluginPane` limits each frame to 32 subscriptions and clears them on unmount, grant withdrawal, and runtime switch. Initial snapshots and updates have explicit loading/error coverage. `HostClient.onProjects/onWorktrees/onSessions` return promises of unsubscribe functions, so approval failures reach the caller.
|
|
36
|
-
- `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.
|
|
36
|
+
- `storage` is instance-owned JSON under the guest's namespace, without a new capability. `storage.js` serializes authorization, read-modify-write, and removal per namespace; writes use unique temporary files and rename. Missing keys differ from stored null; corrupt files and failed writes fail without clearing data. Device scope is an explicit SDK opt-in only when ready features advertise `deviceStorage`; it uses the host runtime key plus guest ID and server-issued installation `storageId`, and has no instance fallback. Different runtime identities remain separate even if a server duplicates a storage ID. URL-based runtime keys separate address aliases; saved-host and relay keys retain their existing identity rules. IndexedDB limits are 2 MiB per namespace and 16 MiB of serialized preference data across the database; browser metadata has separate overhead. A confirmed uninstall makes the namespace inaccessible, but an offline profile retains its bytes until site data is removed. Device storage does not promise secret or SSO storage. Limits and public result shapes live in `API.md`. For project data, extensions namespace keys with project IDs.
|
|
37
37
|
|
|
38
38
|
- 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`.
|
|
39
39
|
- `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.
|
|
40
40
|
- 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`.
|
|
41
41
|
- Extra manifest keys are dropped, not forwarded.
|
|
42
|
-
- 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.
|
|
42
|
+
- Theme text fields are required on `apiVersion: 1`: `primaryText`, `successText`, `warningText`, `errorText`, `infoText`. Optional syntax fields are `syntaxKeyword`, `syntaxString`, `syntaxNumber`, `syntaxFunction`, `syntaxType`, `syntaxComment`, `syntaxVariable`, and `syntaxOperator`. Host calculation and UI-kit application are documented in `src/ui/DOCUMENTATION.md`. Keep the wire schema, TypeScript contract and iframe mapping in sync.
|
|
43
43
|
- `connectHost` rejects when `parent === self`. A silent host rejects in-flight RPCs as `HOST_TIMEOUT` after 20s. Dispose rejects the rest as `HOST_UNAVAILABLE`.
|
|
44
44
|
- Failed `result` may carry `code`. Known guest codes are `HOST_UNAVAILABLE`, `HOST_TIMEOUT`, `HOST_REJECTED`, `DISCONNECTED`, `DISABLED`, `BAD_PATH`, `NO_INTEGRATION`, `NO_SESSION`, `SESSION_BUSY`, `NO_SERVICE`, `SERVICE_FAILED`, `NOT_GRANTED`, `NO_DIRECTORY`, `NOT_FOUND`, `FILE_TOO_LARGE`, `DENIED`, `NO_MODEL`, `MODEL_FAILED`, `UNSUPPORTED`. Anything else becomes `HOST_REJECTED`. The `error` string stays.
|
|
45
45
|
- `parseManifest` takes a typed document. File or network bytes go through `parseManifestJson`. Both return a result and do not throw on junk.
|
|
@@ -56,7 +56,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
56
56
|
- `readFile` / `writeFile` / `listDir` / `stat` are `file-read` / `file-write` / `file-list` / `file-stat` on the wire. `guestFileScope` decides the grant: a relative path is `project` (capability `files`, joined to the open project; none open is `NO_DIRECTORY`), `/…` or `~/…` is `filesystem` (must match a declared pattern). The server compares canonical paths, so a symlink out of either tree is `BAD_PATH`, as is any `..` segment. Results are `{ content }`, `{ written: true }`, `{ entries: { name, kind }[] }`, and `{ kind, size, mtime }` where `kind` may be `missing`. `GUEST_FILE_PATH_MAX` (1024), `GUEST_FILE_CONTENT_MAX` (2 000 000 characters, read and write), `GUEST_FILE_LIST_MAX` (2000 entries). The client refuses an empty, oversized, NUL, or backslash path as `BAD_PATH` and oversized content as `FILE_TOO_LARGE` before posting; the host schema accepts only what `isGuestFilePath` does, so a dropped message cannot turn into `HOST_TIMEOUT`. Nothing about the write is logged, not even the path.
|
|
57
57
|
- `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.
|
|
58
58
|
- `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.
|
|
59
|
-
- `attach` puts `{ providerId, id, title, url }` on the composer as a chip,
|
|
59
|
+
- `attach` puts `{ providerId, id, title, url }` on the composer as a chip, next to any other attached issues, PRs and guest items; attaching the same `providerId` + `id` again replaces that chip. `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.
|
|
60
60
|
- `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.
|
|
61
61
|
- `prompt` is `{ text, send? }`. `send` omitted or `false` replace-composes on the current session. `send: true` sends on that session with the same model path as `startSession`. No session is `NO_SESSION`. A busy session on send is `SESSION_BUSY`. Compose still works while busy. The result is `{ sent }`. The guest does not pick a model or agent.
|
|
62
62
|
- `sessionLink` is the same attach fields on the current session. It does not create a session. No project or no session is a refusal.
|
|
@@ -98,6 +98,16 @@ Add a type when the host grows that hole, not before. Any new `connectHost` meth
|
|
|
98
98
|
|
|
99
99
|
Rebuild every example bundle after an SDK change (`bunx openchamber-guest-bundle` per entry, see `examples/README.md`); the built files are checked in.
|
|
100
100
|
|
|
101
|
+
## Anchored popovers
|
|
102
|
+
|
|
103
|
+
`popover.ts` owns bounded rectangles, JSON context, requests and close events; `popover-schemas.ts` validates the host boundary. `connectHost` exposes `openPopover`, `closePopover`, `setPopoverAnchorActive` and `onPopoverClosed`, gated by `ready.features.popovers`. `src/ui/popover-anchor.ts` owns DOM-anchor hover/focus events and active-only observers. Public limits and behavior live in `API.md#anchored-popovers`.
|
|
104
|
+
|
|
105
|
+
The UI host owns geometry and lifecycle in `lib/guests/popovers.ts` and `components/layout/GuestPopover.tsx`; `PluginPane` remains the message and authorization boundary. An overlay reuses its owner's approved package entry and existing direct/relay loader. It receives `surface: 'popover'`, bounded `ready.popover` data and the owner's directory. Its grants are the extension's existing grants. It cannot choose a different entry or open nested popovers. The host verifies child authority against the active owner before handling messages, including the interval before React removes a retired frame.
|
|
106
|
+
|
|
107
|
+
Project changes dismiss the overlay without resetting the owner's workspace subscriptions or unrelated pending replies. Frame, runtime, origin, grant and installation changes retire it. One fresh child exists per opening; there is no hidden warm frame. Owner-only close events identify the opening so delayed activity cannot close its replacement. The host focuses the source frame for keyboard dismissal, and the guest helper restores the actual anchor, which the host cannot access across the sandbox.
|
|
108
|
+
|
|
109
|
+
Child pointer/focus activity travels through the existing popover activity message, restricted to that child's own opening. Parent iframe `:hover` and pointer events are not reliable for opaque-origin content. The host verifies geometry but cannot prove a guest's hover intent; focus transfer requires existing owner focus. Host modal dialogs suppress previews. `setHeight` resizes the child within its bounds without changing the activation or interrupting pending replies. Dialog-owned previews remain unsupported to preserve their modal focus boundary.
|
|
110
|
+
|
|
101
111
|
## Guest toasts
|
|
102
112
|
|
|
103
113
|
`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`.
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Full guide: [Build an extension](https://openchamber.dev/docs/sdk/). Reference:
|
|
|
6
6
|
|
|
7
7
|
Extensions load in OpenChamber web and desktop. VS Code and mobile do not load them yet.
|
|
8
8
|
|
|
9
|
+
Device-scoped extension storage is local browser preference data. It stays partitioned by the host's runtime identity and the extension installation. URL aliases are not guaranteed to share preferences; saved host and relay connections follow the host's existing runtime identity rules.
|
|
10
|
+
|
|
9
11
|
## Install
|
|
10
12
|
|
|
11
13
|
```bash
|
|
@@ -157,12 +159,14 @@ Copy keeps the toast open and shows Copied on success. A failed copy shows an er
|
|
|
157
159
|
|
|
158
160
|
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.
|
|
159
161
|
|
|
160
|
-
For a small readout in the chat's Work Status panel, add `"statusSection": { "entry": "status/index.html", "title": "Recent commits" }`. It needs no `panel.entry`, so a section-only extension has no rail icon. `ctx.surface` is `"status"`. Call `host.setHeight(px)` when your content changes size; the host stops growing the frame at 320 px and your page scrolls after that. `examples/git-graph-status` is a complete section with a local service.
|
|
162
|
+
For a small readout in the chat's Work Status panel, add `"statusSection": { "entry": "status/index.html", "title": "Recent commits" }`. It needs no `panel.entry`, so a section-only extension has no rail icon. `ctx.surface` is `"status"`. Call `host.setHeight(px)` when your content changes size; the host stops growing the frame at 320 px and your page scrolls after that. Object-form sections can opt into `defaultExpanded: false` or `requiresProject: true`; omitted fields keep the existing expanded, directory-independent behavior. `examples/git-graph-status` is a complete section with a local service.
|
|
161
163
|
|
|
162
164
|
With `sessions` approved, use `listProjects()`, `listWorktrees(projectId)`, and `listSessions(projectId)`. Subscribe through `await onProjects(listener)`, `await onWorktrees(projectId, listener)`, or `await onSessions(projectId, listener)` and retain the returned unsubscribe function. Snapshots distinguish loading, ready, and error; session activity and observed turn outcomes are separate from your task status.
|
|
163
165
|
|
|
164
166
|
`startSession` accepts `projectId` and `worktree: { kind: "new", name: "fix-login", baseBranch: "main" }` or `{ kind: "existing", directory }`. It preserves the current screen by default. `openSession(sessionId)` explicitly opens the chat. `host.storage.get/set/delete/keys` stores your own JSON on the connected server without a file-access grant. See [API.md](./API.md) for limits and partial results. The `tasks-demo` page exercises these methods together.
|
|
165
167
|
|
|
168
|
+
Pass `{ scope: 'device' }` only after `onReady` reports `features.deviceStorage: true`. Device storage stays in this browser profile and installation namespace instead of the connected server's instance storage. Older hosts reject that request with `UNSUPPORTED`; the SDK does not fall back to instance storage. It is for ordinary preferences, not secrets or single-sign-on state.
|
|
169
|
+
|
|
166
170
|
```ts
|
|
167
171
|
import { connectHost, HostRequestError } from '@openchamber/sdk';
|
|
168
172
|
|
|
@@ -220,10 +224,18 @@ host.onItem((item) => {
|
|
|
220
224
|
|
|
221
225
|
Every method, its limits, and the error codes are on the [Host API](https://openchamber.dev/docs/sdk/host/) page.
|
|
222
226
|
|
|
227
|
+
## Popovers outside the extension frame
|
|
228
|
+
|
|
229
|
+
When `ready.features.popovers` is true, `host.openPopover` can show a bounded card outside your iframe. The host loads the same package entry in a second sandboxed frame, with `ready.surface === 'popover'` and `ready.popover.data` carrying your JSON. Branch on that surface before mounting your normal page. The popover has the same approved capabilities as your extension.
|
|
230
|
+
|
|
231
|
+
Use `mountPopoverAnchor` from `@openchamber/sdk/ui` to attach the preview to a DOM element. It handles hover, keyboard access, anchor removal and scrolling; dispose the binding when replacing the element. Keep your existing click action as a fallback for older hosts. The Git Graph example shows this with commit metadata, statistics and copy/link actions. See [Anchored popovers](./API.md#anchored-popovers) for the request, limits and lifecycle.
|
|
232
|
+
|
|
223
233
|
## UI kit
|
|
224
234
|
|
|
225
235
|
`@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.
|
|
226
236
|
|
|
237
|
+
Theme snapshots may include optional syntax colors for keywords, strings, numbers, functions, types, comments, variables, and operators. `applyHostReady` publishes them as `--oc-syntax-*` variables and clears an optional variable when a later snapshot omits it. Existing semantic theme variables remain available for older hosts.
|
|
238
|
+
|
|
227
239
|
```ts
|
|
228
240
|
import { applyHostReady, mountList } from '@openchamber/sdk/ui';
|
|
229
241
|
|
package/dist/contract.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from './api-version.ts';
|
|
2
2
|
import type { FileEditorChange, FileEditorDocument, FileSnapshotRequest, FileSnapshotResultPayload } from './file-editor.ts';
|
|
3
3
|
import type { GuestSessionWorktree, GuestStorageRequest, GuestStorageResult, GuestWorkspaceQuery, GuestWorkspaceSnapshot, GuestWorkspaceSubscription, GuestWorkspaceUpdate, GuestWorktree } from './workspace.ts';
|
|
4
|
+
import type { GuestStatusControl, GuestStatusControlEvent } from './status-controls.ts';
|
|
5
|
+
import type { GuestPopoverClosedEvent, GuestPopoverContext, GuestPopoverRequest } from './popover.ts';
|
|
4
6
|
export type HostThemeMode = 'light' | 'dark';
|
|
5
7
|
export type HostThemeTokens = {
|
|
6
8
|
background: string;
|
|
@@ -37,6 +39,14 @@ export type HostThemeTokens = {
|
|
|
37
39
|
/** Monospace stack for code and identifiers. */
|
|
38
40
|
mono: string;
|
|
39
41
|
radius: string;
|
|
42
|
+
syntaxKeyword?: string;
|
|
43
|
+
syntaxString?: string;
|
|
44
|
+
syntaxNumber?: string;
|
|
45
|
+
syntaxFunction?: string;
|
|
46
|
+
syntaxType?: string;
|
|
47
|
+
syntaxComment?: string;
|
|
48
|
+
syntaxVariable?: string;
|
|
49
|
+
syntaxOperator?: string;
|
|
40
50
|
};
|
|
41
51
|
export type HostTheme = {
|
|
42
52
|
mode: HostThemeMode;
|
|
@@ -55,7 +65,7 @@ export type SessionSnapshot = {
|
|
|
55
65
|
* Which host chrome mounted this iframe. Not `openSurface`. `status` is the
|
|
56
66
|
* extension's section in the chat's Work Status panel.
|
|
57
67
|
*/
|
|
58
|
-
export type GuestHostSurface = 'panel' | 'dialog' | 'page' | 'background' | 'status' | 'file';
|
|
68
|
+
export type GuestHostSurface = 'panel' | 'dialog' | 'page' | 'background' | 'status' | 'file' | 'popover';
|
|
59
69
|
export type GuestConnection = {
|
|
60
70
|
connected: boolean;
|
|
61
71
|
account: string;
|
|
@@ -175,6 +185,14 @@ export type HostReadyContext = {
|
|
|
175
185
|
* opened from the rail icon or the composer + menu.
|
|
176
186
|
*/
|
|
177
187
|
item: GuestItem | null;
|
|
188
|
+
/** Present only in the host-created child frame for an active popover. */
|
|
189
|
+
popover?: GuestPopoverContext;
|
|
190
|
+
/** Optional capabilities advertised by a newer host. Absent remains compatible with older hosts. */
|
|
191
|
+
features?: {
|
|
192
|
+
deviceStorage?: true;
|
|
193
|
+
statusControls?: true;
|
|
194
|
+
popovers?: true;
|
|
195
|
+
};
|
|
178
196
|
};
|
|
179
197
|
export type GuestItemRole = 'user' | 'assistant';
|
|
180
198
|
/** A `contributes.actions` entry with `where: "message"` ran on this message. */
|
|
@@ -424,6 +442,14 @@ export type HostItemMessage = Envelope & {
|
|
|
424
442
|
item: GuestItem | null;
|
|
425
443
|
};
|
|
426
444
|
};
|
|
445
|
+
export type HostStatusControlEventMessage = Envelope & {
|
|
446
|
+
type: 'status-control-event';
|
|
447
|
+
payload: GuestStatusControlEvent;
|
|
448
|
+
};
|
|
449
|
+
export type HostPopoverClosedMessage = Envelope & {
|
|
450
|
+
type: 'popover-closed';
|
|
451
|
+
payload: GuestPopoverClosedEvent;
|
|
452
|
+
};
|
|
427
453
|
/** Host → guest request. The guest answers with `resolve-result` carrying the same `id`. */
|
|
428
454
|
export type HostResolveMessage = Envelope & {
|
|
429
455
|
type: 'resolve';
|
|
@@ -467,7 +493,7 @@ export type HostResultMessage = Envelope & {
|
|
|
467
493
|
export type HostMessage = (Envelope & {
|
|
468
494
|
type: 'workspace';
|
|
469
495
|
payload: GuestWorkspaceUpdate;
|
|
470
|
-
}) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostResolveMessage | HostActionMessage | HostFileOpenMessage | HostFileSnapshotMessage | HostFileSavedMessage | HostResultMessage;
|
|
496
|
+
}) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostStatusControlEventMessage | HostPopoverClosedMessage | HostResolveMessage | HostActionMessage | HostFileOpenMessage | HostFileSnapshotMessage | HostFileSavedMessage | HostResultMessage;
|
|
471
497
|
type GuestCall<Type extends string, Payload = never> = Envelope & {
|
|
472
498
|
type: Type;
|
|
473
499
|
id: string;
|
|
@@ -508,6 +534,18 @@ export type GuestGenerateMessage = GuestCall<'generate', GenerateRequest>;
|
|
|
508
534
|
export type GuestBadgeMessage = GuestCall<'badge', BadgeRequest>;
|
|
509
535
|
export type GuestResizeMessage = GuestCall<'resize', ResizeRequest>;
|
|
510
536
|
export type GuestOpenCommitMessage = GuestCall<'open-commit', OpenCommitRequest>;
|
|
537
|
+
export type GuestStatusControlsMessage = GuestCall<'status-controls', {
|
|
538
|
+
controls: GuestStatusControl[];
|
|
539
|
+
}>;
|
|
540
|
+
export type GuestPopoverOpenMessage = GuestCall<'popover-open', GuestPopoverRequest>;
|
|
541
|
+
export type GuestPopoverCloseMessage = GuestCall<'popover-close', {
|
|
542
|
+
id: string;
|
|
543
|
+
reason?: 'closed' | 'escape';
|
|
544
|
+
}>;
|
|
545
|
+
export type GuestPopoverAnchorMessage = GuestCall<'popover-anchor', {
|
|
546
|
+
id: string;
|
|
547
|
+
active: boolean;
|
|
548
|
+
}>;
|
|
511
549
|
/** Answers a host `resolve` by `id`. The host sends no `result` back for it. */
|
|
512
550
|
export type GuestResolveResultMessage = Envelope & {
|
|
513
551
|
type: 'resolve-result';
|
|
@@ -543,7 +581,7 @@ export type GuestMessage = GuestCall<'workspace-read', GuestWorkspaceQuery> | Gu
|
|
|
543
581
|
subscriptionId: string;
|
|
544
582
|
}> | GuestCall<'storage', GuestStorageRequest> | GuestCall<'open-session', {
|
|
545
583
|
sessionId: string;
|
|
546
|
-
}> | GuestHelloMessage | GuestToastMessage | GuestOpenUrlMessage | GuestOpenSurfaceMessage | GuestClipboardWriteMessage | GuestComposeMessage | GuestAttachMessage | GuestStartSessionMessage | GuestPromptMessage | GuestSessionLinkMessage | GuestCloseMessage | GuestOauthStartMessage | GuestOauthDisconnectMessage | GuestRequestMessage | GuestServiceRequestMessage | GuestServiceStatusMessage | GuestFileReadMessage | GuestFileWriteMessage | GuestFileListMessage | GuestFileStatMessage | GuestGenerateMessage | GuestBadgeMessage | GuestResizeMessage | GuestOpenCommitMessage | GuestActionResultMessage | GuestResolveResultMessage | GuestFileSnapshotResultMessage | GuestFileChangeMessage | GuestFileSaveMessage | GuestFileUnsupportedMessage;
|
|
584
|
+
}> | GuestHelloMessage | GuestToastMessage | GuestOpenUrlMessage | GuestOpenSurfaceMessage | GuestClipboardWriteMessage | GuestComposeMessage | GuestAttachMessage | GuestStartSessionMessage | GuestPromptMessage | GuestSessionLinkMessage | GuestCloseMessage | GuestOauthStartMessage | GuestOauthDisconnectMessage | GuestRequestMessage | GuestServiceRequestMessage | GuestServiceStatusMessage | GuestFileReadMessage | GuestFileWriteMessage | GuestFileListMessage | GuestFileStatMessage | GuestGenerateMessage | GuestBadgeMessage | GuestResizeMessage | GuestOpenCommitMessage | GuestStatusControlsMessage | GuestPopoverOpenMessage | GuestPopoverCloseMessage | GuestPopoverAnchorMessage | GuestActionResultMessage | GuestResolveResultMessage | GuestFileSnapshotResultMessage | GuestFileChangeMessage | GuestFileSaveMessage | GuestFileUnsupportedMessage;
|
|
547
585
|
export declare const isServiceStatusResult: (value: HostResultPayload | undefined) => value is ServiceStatusResult;
|
|
548
586
|
export declare const isGuestRequestResult: (value: HostResultPayload | undefined) => value is GuestRequestResult;
|
|
549
587
|
export declare const isFileReadResult: (value: HostResultPayload | undefined) => value is FileReadResult;
|
package/dist/contract.js
CHANGED
|
@@ -211,6 +211,7 @@ export const isGenerateResult = (value) => Boolean(value && 'text' in value && S
|
|
|
211
211
|
const HOST_PUSH_TYPES = new Set([
|
|
212
212
|
'workspace',
|
|
213
213
|
'ready', 'directory', 'session', 'connection', 'settings', 'session-lifecycle', 'item', 'resolve', 'action',
|
|
214
|
+
'status-control-event', 'popover-closed',
|
|
214
215
|
'file-open', 'file-snapshot', 'file-saved',
|
|
215
216
|
]);
|
|
216
217
|
const asWireRecord = (data) => (Object(data) === data ? data : null);
|
package/dist/host.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type GuestProjectsSnapshot, type GuestWorktreesSnapshot, type GuestSessionsSnapshot } from './workspace.ts';
|
|
1
|
+
import { type GuestProjectsSnapshot, type GuestWorktreesSnapshot, type GuestSessionsSnapshot, type GuestStorageOptions } from './workspace.ts';
|
|
2
|
+
import type { GuestStatusControl, GuestStatusControlEvent } from './status-controls.ts';
|
|
2
3
|
import type { JsonValue } from './contract.ts';
|
|
4
|
+
import { type GuestPopoverClosedEvent, type GuestPopoverRequest } from './popover.ts';
|
|
3
5
|
import { type FileEditorChange, type FileEditorDocument, type FileEditorSnapshot, type FileSnapshotPurpose } from './file-editor.ts';
|
|
4
6
|
import { type AttachIssueRequest, type GuestActionItem, type ComposeRequest, type PromptRequest, type PromptResult, type SessionLifecycleEvent, type StartSessionRequest, type GuestConnection, type GuestItem, type GuestMessage, type GuestRequest, type GuestRequestResult, type GuestSettings, type HostReadyContext, type HostRequestErrorCode, type ResolveRequest, type SessionSnapshot, type StartSessionResult, type ToastRequest, type ServiceStatusResult, type FileListResult, type FileReadResult, type FileStatResult, type FileWriteResult, type GenerateRequest, type GenerateResult } from './contract.ts';
|
|
5
7
|
export type HostFrame = {
|
|
@@ -26,10 +28,10 @@ export type HostClient = {
|
|
|
26
28
|
onSessions: (projectId: string, listener: (snapshot: GuestSessionsSnapshot) => void) => Promise<() => void>;
|
|
27
29
|
openSession: (sessionId: string) => Promise<void>;
|
|
28
30
|
storage: {
|
|
29
|
-
get: (key: string) => Promise<JsonValue | undefined>;
|
|
30
|
-
set: (key: string, value: JsonValue) => Promise<void>;
|
|
31
|
-
delete: (key: string) => Promise<void>;
|
|
32
|
-
keys: () => Promise<string[]>;
|
|
31
|
+
get: (key: string, options?: GuestStorageOptions) => Promise<JsonValue | undefined>;
|
|
32
|
+
set: (key: string, value: JsonValue, options?: GuestStorageOptions) => Promise<void>;
|
|
33
|
+
delete: (key: string, options?: GuestStorageOptions) => Promise<void>;
|
|
34
|
+
keys: (options?: GuestStorageOptions) => Promise<string[]>;
|
|
33
35
|
};
|
|
34
36
|
onReady: (listener: (context: HostReadyContext) => void) => () => void;
|
|
35
37
|
onDirectory: (listener: (directory: string | null) => void) => () => void;
|
|
@@ -43,6 +45,14 @@ export type HostClient = {
|
|
|
43
45
|
* the last value; `null` when there is none.
|
|
44
46
|
*/
|
|
45
47
|
onItem: (listener: (item: GuestItem | null) => void) => () => void;
|
|
48
|
+
/** Receive activation of a host-rendered Work Status control. */
|
|
49
|
+
onStatusControl: (listener: (event: GuestStatusControlEvent) => void) => () => void;
|
|
50
|
+
onPopoverClosed: (listener: (event: GuestPopoverClosedEvent) => void) => () => void;
|
|
51
|
+
/** Replace this status frame's host-rendered controls. Pass [] to clear them. */
|
|
52
|
+
setStatusControls: (controls: GuestStatusControl[]) => Promise<void>;
|
|
53
|
+
openPopover: (request: GuestPopoverRequest) => Promise<void>;
|
|
54
|
+
closePopover: (id: string, reason?: 'closed' | 'escape') => Promise<void>;
|
|
55
|
+
setPopoverAnchorActive: (id: string, active: boolean) => Promise<void>;
|
|
46
56
|
/**
|
|
47
57
|
* Answer the host when the user submits one of this package's
|
|
48
58
|
* `contributes.commands`. Return the chip to attach, or `null` for nothing
|