@openchamber/sdk 2.0.2 → 2.0.4
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 +17 -1
- package/DOCUMENTATION.md +6 -4
- package/dist/contract.d.ts +41 -3
- package/dist/contract.js +1 -0
- package/dist/file-editor.d.ts +109 -0
- package/dist/file-editor.js +91 -0
- package/dist/frame-policy.d.ts +20 -0
- package/dist/frame-policy.js +37 -0
- package/dist/host.d.ts +22 -0
- package/dist/host.js +99 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/manifest.d.ts +16 -6
- package/dist/manifest.js +8 -4
- package/dist/parse.d.ts +24 -2
- package/dist/parse.js +36 -6
- package/dist/protocol.d.ts +68 -0
- package/dist/protocol.js +58 -1
- package/package.json +1 -1
package/API.md
CHANGED
|
@@ -74,6 +74,9 @@ Each returns an unsubscribe function. Late subscribers get the last known value
|
|
|
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
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 |
|
|
77
|
+
| `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 |
|
|
78
|
+
| `onFileSnapshot(handler)` | `'save' \| 'handoff'` → `FileEditorSnapshot \| Promise<FileEditorSnapshot>` | File editors only: return the whole edited file (`{ content, version }` for a text editor, `{ bytes, version }` for a binary one) and a version of the state it came from. `save` writes it; `handoff` moves it into the host's source view. Throwing fails that save. One handler at a time |
|
|
79
|
+
| `onFileSaved(listener)` | `version: string` | File editors only: the snapshot with that version is on disk. Answer with `reportFileChange({ dirty, edited: false })` so edits made during the write stay unsaved |
|
|
77
80
|
|
|
78
81
|
|
|
79
82
|
`HostReadyContext`
|
|
@@ -86,7 +89,7 @@ Each returns an unsubscribe function. Late subscribers get the last known value
|
|
|
86
89
|
| `locale` | `string` | Host language tag |
|
|
87
90
|
| `directory` | `string | null` |
|
|
88
91
|
| `session` | snapshot or `null` | Title falls back to `id`. `busy` is live status. `model` is `providerID/id` when present |
|
|
89
|
-
| `surface` | `'panel' \| 'dialog' \| 'page' \| 'background'` | Where the host mounted this frame |
|
|
92
|
+
| `surface` | `'panel' \| 'dialog' \| 'page' \| 'background' \| 'status' \| 'file'` | Where the host mounted this frame |
|
|
90
93
|
| `connection` | `{ connected, account }` | Integration link state |
|
|
91
94
|
| `settings` | `Record<string, string>` | Declared keys only |
|
|
92
95
|
| `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` |
|
|
@@ -154,6 +157,9 @@ Access tokens never appear in `ready` or in request results.
|
|
|
154
157
|
| `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 |
|
|
155
158
|
| `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 |
|
|
156
159
|
| `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
|
+
| `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
|
+
| `requestFileSave` | — | `void` | File editors only. Save now, as Cmd/Ctrl+S does |
|
|
162
|
+
| `reportFileUnsupported` | — | `void` | File editors only. This file cannot be opened here; the host shows its source and a notice |
|
|
157
163
|
| `dispose` | — | `void` | Remove listener, reject pending RPCs |
|
|
158
164
|
|
|
159
165
|
|
|
@@ -217,6 +223,12 @@ Storage belongs to the extension on the connected server and needs no extra capa
|
|
|
217
223
|
|
|
218
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.
|
|
219
225
|
|
|
226
|
+
### File editors
|
|
227
|
+
|
|
228
|
+
`contributes.fileEditors` lists up to 8 editors: `{ id: 'canvas', title: 'Excalidraw', match: ['*.excalidraw', '*.excalidraw.md'], entry: 'editor/index.html' }`. `match` holds 1 to 16 file-name patterns, compared case-insensitively with the name only (no `/`); `*` is any run of characters, `?` one, and a pattern must have at least one character that is not a wildcard. `entry` is package `.html` with built scripts, checked at install. `content` is `"text"` (the default) or `"binary"`: a text editor claims only text files and gets the text with `\n` line endings (the host restores the file's own on write); a binary editor claims any matching file, gets its bytes as a `Uint8Array`, returns bytes, and has no source view, and the host saves its unsaved changes before it moves to or from fullscreen. When the user opens a matching file in Files, the viewer loads your page instead of its own preview; the first enabled, approved extension in the list wins, ahead of the built-in viewers. `ctx.surface` is `file`.
|
|
229
|
+
|
|
230
|
+
The host keeps the file. It hands you the text (`onFileOpen`), asks for the edited text when it saves or when the user switches to the source view or fullscreen (`onFileSnapshot`), and tells you which snapshot reached the disk (`onFileSaved`). Saving, autosave, the unsaved-changes prompt, line endings, and external changes are the host's. Your page never reads or writes the file and needs no `files` capability for it. Report every change with `reportFileChange`; `createFileSaveTracker(version)` does the dirty bookkeeping. A file you cannot open goes back with `reportFileUnsupported`. Files over 20 000 000 characters (text) or bytes (binary) stay with the host's own view, and a snapshot over that is refused. An editor that hands back the wrong kind (text from a binary editor or bytes from a text one) fails the save. The page is reloaded, with the file handed over again, when the file changes on disk, the user discards changes, or the editor moves to or from fullscreen. A file-editor-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; `examples/checklist-editor` is a complete editor.
|
|
231
|
+
|
|
220
232
|
`sent` **values** (`startSession` / `prompt`): `sent` | `no-model` | `skipped` | `failed`. After `no-model` / `failed` on `startSession`, the session still exists.
|
|
221
233
|
|
|
222
234
|
**File path rules** (`readFile` / `writeFile` / `listDir` / `stat`): a relative path (`README.md`, `src/x.ts`, `.`) is joined to the project that is open when the call runs and needs the `files` capability; no open project is `NO_DIRECTORY`. A path starting with `/` or `~/` is outside the project, must match one of the package's `contributes.filesystem` globs, and needs the `filesystem` capability. Any `..` segment, a backslash, or a symlink that leads out of the allowed tree is `BAD_PATH`. The host compares canonical (realpath) paths, so `/tmp/x` on macOS is checked as `/private/tmp/x` and a pattern's literal prefix is canonicalized the same way. Content over 2 000 000 characters is `FILE_TOO_LARGE` in both directions; an OS permission refusal is `DENIED`.
|
|
@@ -440,6 +452,7 @@ Used by the OpenChamber host and by tools that validate packages. Guests rarely
|
|
|
440
452
|
| `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 |
|
|
441
453
|
| `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 |
|
|
442
454
|
| `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 |
|
|
455
|
+
| `fileEditors` | Optional, 1–8 editors: unique kebab-case `id`, `title` 1–60 chars, `match` 1–16 file-name patterns (no `/`, not only wildcards), `entry` package `.html` with built scripts, optional `content` `"text"` (default) or `"binary"` (`invalid-file-editors`). See File editors above |
|
|
443
456
|
| `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 |
|
|
444
457
|
| `integration` | Optional. Exactly one of `oauth`, `token`, or `host` (`provider: "linear"` only) |
|
|
445
458
|
| `service` | Optional. `entry` must be a built `.js` file on disk. `provides: ["browser"]` makes it the agent's browser when the user selects it; `surface: true` gives it a host-drawn live panel the user can take over (no `panel.entry` then). Neither needs a panel or background entry. See [GUEST_SERVICES.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/GUEST_SERVICES.md) |
|
|
@@ -525,6 +538,9 @@ Frozen on `apiVersion` 1 — named in docs, no host hole yet:
|
|
|
525
538
|
- Keyboard shortcuts, raw git remotes, magic prompts
|
|
526
539
|
- Second `host.provider` beyond Linear
|
|
527
540
|
- Arbitrary filesystem access from the page (only the open project with `files`, or declared `contributes.filesystem` globs), terminal, pairing, or host React components. A declared `service` is outside these limits: it is a process with the user's rights and no sandbox
|
|
541
|
+
- Network access from the page. Pages run under a Content Security Policy: scripts, styles, images, fonts, media and workers come from the package itself or `data:`/`blob:`, and `fetch` reaches only the package's own files. Ship fonts and images inside the package. To talk to an outside service, use `request` (the integration's `apiOrigin`) or declare up to 8 https origins in `contributes.origins`, for example `"origins": ["https://fonts.example.com"]`. The user approves that list at install, and again when an update adds to it. Approved origins are open to `fetch`, images, fonts, styles and media, never to scripts or workers. A `fetch` whose response you read also needs that server to allow CORS for the `null` origin, since the page's origin is opaque
|
|
542
|
+
|
|
543
|
+
A navigation of the page itself to another address is refused in the desktop app; don't rely on it anywhere.
|
|
528
544
|
|
|
529
545
|
Do not go around the guest contract through `RuntimeAPIs`.
|
|
530
546
|
|
package/DOCUMENTATION.md
CHANGED
|
@@ -15,6 +15,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
15
15
|
- `src/host.ts`: iframe client. Posts to `parent`. Ignores messages whose `source` is not `parent`. In-flight RPCs time out at `GUEST_REQUEST_TIMEOUT_MS` (20s) as `HOST_TIMEOUT`. `onDirectory` / `onSession` / `onSessionLifecycle` / `onItem` replay and stay current after later pushes, same as connection and settings. `onResolve` holds one handler for host `resolve` requests and answers each with `resolve-result` (the clamped chip, `null`, or the thrown error's message; no handler answers with an error). `setBadge` posts `badge` with the count clamped to 0..`GUEST_BADGE_MAX`.
|
|
16
16
|
- `scripts/bundle-guest.ts`: official IIFE build, exposed as the `openchamber-guest-bundle` bin. Run `bunx openchamber-guest-bundle panel/main.ts panel/main.js` from the guest folder; paths resolve against the current directory. Needs Bun. Any bundler with an IIFE target is an equivalent substitute.
|
|
17
17
|
- `dist/`: the published output (`bun run --cwd packages/sdk build`, `tsc -p tsconfig.build.json`). `package.json` `exports` point here so Node loads the SDK from `node_modules` (packaged Electron, `openchamber serve`). Inside the monorepo, Vite aliases and tsconfig `paths` resolve `@openchamber/sdk` to `src/` so builds and type-checks never depend on a stale `dist`. The server under Bun and Node uses `dist`; the root `postinstall` builds it. The package ships to npm from the release workflow with `bun publish`, right before `@openchamber/web`; `scripts/bump-version.mjs` bumps it with the app, so the SDK version always equals the OpenChamber version it was released with.
|
|
18
|
+
- `src/file-editor.ts`: the `contributes.fileEditors` contract: limits, file-name pattern check and match, the document/snapshot/change types, and `createFileSaveTracker`. Zod-free, so a guest editor bundles it.
|
|
18
19
|
- `GUEST_SERVICES.md`: local service contract (`contributes.service`, `serviceRequest`, grant, loopback).
|
|
19
20
|
- `src/service-providers.ts`: contracts a service fulfils for a host role (`contributes.service.provides`). Today only `browser`: the ten `browser.*` actions, their parameter and answer types, the limits, `BrowserProviderContext` (the calling project and chat, filled by the host from the tool call; `null` fields mean unknown), and `readBrowserProviderRequest` (a zod-free boundary parse for the service side). `src/service-provider-schemas.ts` is the host's zod parse of the answer, exported from `@openchamber/sdk/schemas`. A providing service needs no panel or background entry (`runtimeContributions` in `parse.ts`). The host side is `packages/web/server/lib/browser-control/provider.js`.
|
|
20
21
|
- `src/service-surface.ts`: the shared-surface contract (`contributes.service.surface`): loopback paths and headers, input event types, limits, and the zod-free `readSurfaceInputBatch` / `readSurfaceControlNotice` / `readSurfaceResizeRequest` for the service side. `src/service-surface-schemas.ts` (host entry) holds the viewer↔host socket messages (`surfaceViewerMessageSchema`, `surfaceHostMessageSchema`) shared by `packages/ui/src/lib/guests/surface-client.ts` and `packages/web/server/lib/guests/surface.js`, plus the service answer schemas. A surface service needs no entry of its own; with `panel.entry` that page is docked to one edge of the picture, `panel.dock` (`GUEST_SURFACE_DOCKS`) and `panel.size` px (`GUEST_SURFACE_DOCK_SIZE_*`), only valid with both. The rail panel is `packages/ui/src/components/layout/GuestSurfacePane.tsx` (host-drawn canvas, not an iframe); `ContextPanel` lays the page iframe (kept mounted) beside the pane (mounted only while shown) per `DOCK_LAYOUT`, and `lib/guests/surfaces.ts#guestSurfaceDocking` decides. Viewer identity is host-set: input carries `x-surface-viewer` and `x-surface-frame-seq` (the service's own number of the frame that viewer last drew; a 409 refuses it as stale), control notices carry `viewer`, and page `serviceRequest`s from a window with a live viewer (`packages/ui/src/lib/guests/surface-viewers.ts`) also carry `x-surface-viewer-controls`.
|
|
@@ -29,6 +30,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
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
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.
|
|
31
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
|
+
- `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.
|
|
32
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.
|
|
33
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.
|
|
34
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.
|
|
@@ -47,9 +49,9 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
47
49
|
- `contributes.commands` is 1 to 8 entries (`GUEST_COMMANDS_MAX`) with a unique `name` matching `GUEST_COMMAND_NAME` (`/^[a-z][a-z0-9-]{0,23}$/`) and an optional `description` of 1 to `GUEST_COMMAND_DESCRIPTION_MAX` (80) characters; else `invalid-commands`. The host owns collisions: a name the composer already has (built-in, OpenCode command, skill, or an earlier guest) is dropped with a console warning. The public catalog row carries `commands` as declared.
|
|
48
50
|
- `contributes.tools` is 1 to 16 entries (`GUEST_TOOLS_MAX`) that say how the extension's tool calls look in the chat, without code. `match` is the full tool name as OpenCode reports it, un-normalized (`mcp.jira.search`, `jira_search`), 1 to `GUEST_TOOL_MATCH_MAX` (128) characters of `[A-Za-z0-9_.:-]`, with one `*` allowed at the end as a suffix wildcard (`mcp.jira.*`; `*` alone is refused). Optional `name` (1 to `GUEST_TOOL_NAME_MAX`, 40) is the header title when `title` is absent or renders empty; `icon` is a Remixicon name or a package `.svg` path with the `panel.icon` rules (the chat draws the SVG as a `currentColor` mask at the glyph size through `resolveGuestToolIcon`, fetched as an authenticated asset under `/api/guests/<id>/<path>`; a Remixicon the sprite lacks leaves the host's own tool icon); `title` and `subtitle` are templates of up to `GUEST_TOOL_TEMPLATE_MAX` (200) characters with `{input.path}`, `{output.path}`, and `{metadata.path}` placeholders (dotted paths, array indexes allowed; a missing path is an empty string; a value is stringified and cut at `GUEST_TOOL_TEMPLATE_VALUE_MAX`, 200). `output` is `auto` (the host's own detection, the default), `text`, `json` (the JSON summary/tree/raw views), `markdown` (the assistant markdown renderer), `code` with `language` (1 to 32), or `table` with `columns` (1 to 16 dotted paths; rows are the output array or `output.items`). `language` outside `code` and `columns` outside `table` (or a `table` without `columns`) fail closed; anything else wrong is `invalid-tools`. The public catalog row carries `tools` as declared; the host applies them only for an active guest, an exact `match` beats a wildcard from any extension, and among equals the first extension in the catalog wins. The registry lives in `packages/ui/src/lib/guests/tool-presentation.ts` and is consulted by the existing chat tool renderers; VS Code and mobile keep the catalog empty, so nothing changes there.
|
|
49
51
|
- Host → guest `resolve` is `{ id, payload: { command, args } }`; the guest answers `resolve-result` with the same `id` and `{ item }` (a clamped `AttachIssueRequest` or `null`) or `{ error }` (1 to `GUEST_RESOLVE_ERROR_MAX` characters). The host waits `GUEST_REQUEST_TIMEOUT_MS`, then treats silence as a failure and never sends a `result` for a `resolve-result`. `badge` is `{ count }` with `count` an integer 0..999 or `null`; the host schema drops anything else.
|
|
50
|
-
- `ready` always carries `session` (`{ id, title, busy }` or `null`), `surface` (`panel` on the rail, `dialog` in the attach window, `page` full screen, `background` hidden, `status` in the Work Status panel), `connection` (`{ connected, account }`), and `settings` (declared keys only). Title falls back to `id` when the host record has no title. `busy` is live `session_status` `busy` or `retry`. Missing status is `busy: false`, not an empty success that clears a previous snapshot. `model` is `providerID/id` when the host session record has both. `agent` is the session agent name when present. `surface` is which host chrome mounted the iframe. It is not `openSurface`. `item` is the attached item the surface was opened for (the user clicked that item's chip on the composer; the same `AttachIssueRequest` shape the guest handed to `attach`, with `text` filled from the chip's context) or `null` from the rail icon and the + menu. The host also pushes `item` when it changes while the frame is up; `onItem` replays the latest value. `ready.theme.tokens` is the host colour bag: surfaces (`background`, `elevated`, `mutedSurface`, `subtle`), text (`foreground`, `elevatedForeground`, `muted`), interaction (`border`, `hover`, `active`, `selection`, `selectionForeground`, `focus`), `primary` / `primaryForeground`, the four status colours, `font`, `mono`, and `radius`. `applyHostReady` writes it onto the iframe. The host also pushes `connection` and `settings` the same way as `session`. Access tokens never appear in `ready` or a result.
|
|
52
|
+
- `ready` always carries `session` (`{ id, title, busy }` or `null`), `surface` (`panel` on the rail, `dialog` in the attach window, `page` full screen, `background` hidden, `status` in the Work Status panel, `file` as a Files view editor), `connection` (`{ connected, account }`), and `settings` (declared keys only). Title falls back to `id` when the host record has no title. `busy` is live `session_status` `busy` or `retry`. Missing status is `busy: false`, not an empty success that clears a previous snapshot. `model` is `providerID/id` when the host session record has both. `agent` is the session agent name when present. `surface` is which host chrome mounted the iframe. It is not `openSurface`. `item` is the attached item the surface was opened for (the user clicked that item's chip on the composer; the same `AttachIssueRequest` shape the guest handed to `attach`, with `text` filled from the chip's context) or `null` from the rail icon and the + menu. The host also pushes `item` when it changes while the frame is up; `onItem` replays the latest value. `ready.theme.tokens` is the host colour bag: surfaces (`background`, `elevated`, `mutedSurface`, `subtle`), text (`foreground`, `elevatedForeground`, `muted`), interaction (`border`, `hover`, `active`, `selection`, `selectionForeground`, `focus`), `primary` / `primaryForeground`, the four status colours, `font`, `mono`, and `radius`. `applyHostReady` writes it onto the iframe. The host also pushes `connection` and `settings` the same way as `session`. Access tokens never appear in `ready` or a result.
|
|
51
53
|
- `writeClipboard` text is 1–32000 characters. `compose` text is 1–16000 after trim. Compose default mode is `append`. Compose writes the chat composer. It does not send.
|
|
52
|
-
- `contributes.capabilities` is a list of `prompt`, `sessions`, `files`, and/or `model` (`DECLARED_GUEST_CAPABILITIES`). `model` gates `generate`: `{ prompt, system?, maxOutputTokens? }` → `{ text }`, posted with a `GUEST_GENERATE_TIMEOUT_MS` (90s) timeout instead of the usual 20s because the model answers slowly; the host answers `NO_MODEL` when the small-model service resolves nothing (or only Claude Code) and `MODEL_FAILED` when the provider call fails. `requestedGuestCapabilities` adds `conversation`, `service`, `network`, and `
|
|
54
|
+
- `contributes.capabilities` is a list of `prompt`, `sessions`, `files`, and/or `model` (`DECLARED_GUEST_CAPABILITIES`). `model` gates `generate`: `{ prompt, system?, maxOutputTokens? }` → `{ text }`, posted with a `GUEST_GENERATE_TIMEOUT_MS` (90s) timeout instead of the usual 20s because the model answers slowly; the host answers `NO_MODEL` when the small-model service resolves nothing (or only Claude Code) and `MODEL_FAILED` when the provider call fails. `requestedGuestCapabilities` adds `conversation`, `service`, `network`, `filesystem`, and `origins` from a session action with `payload: ["messages"]`, `contributes.service`, `contributes.integration`, `contributes.filesystem`, and `contributes.origins`; the user approves that whole list once at install (`PUT /api/guests/:id/capabilities`), and a package that later asks for more is re-approved. `hasGuestCapability` gates `prompt({ send })`, `startSession`, `request`, `serviceRequest`, and the four file calls; a refusal is `NOT_GRANTED` (`NO_SERVICE` for the service).
|
|
53
55
|
- `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.
|
|
54
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.
|
|
55
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.
|
|
@@ -66,7 +68,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
|
|
|
66
68
|
|
|
67
69
|
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.
|
|
68
70
|
|
|
69
|
-
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
|
|
71
|
+
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 SDK operations. The relay document gets `guestFramePolicy(null)` as its first `<meta>` (a `srcDoc` has no response headers), so it cannot connect anywhere. Existing authored CSP is preserved and only narrows it further. Direct iframe URLs retain their existing path.
|
|
70
72
|
|
|
71
73
|
`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.
|
|
72
74
|
|
|
@@ -82,7 +84,7 @@ Catalog loading and installation retain typed request diagnostics in `lib/guests
|
|
|
82
84
|
|
|
83
85
|
**Zip / git install** copies into `{dataDir}/extensions/{id}` with `source: "zip"` or `"git"`. Only git installs update: Settings → Extensions compares the installed `package.json` `version` with the origin's and offers Update when the origin is newer, so bump `version` to ship one; a `#tag` or `#branch` on the install URL pins that channel. The archive is not kept open. The host reads it (local file or https download, 20MB cap), extracts with a small allowlist unzip (store/deflate only; no zip64, encryption, or `..` paths; 500 files / 40MB uncompressed), unwraps a single root folder when `package.json` sits inside it, inspects the manifest, then renames staging to `{dataDir}/extensions/{id}`. Runtime serves and spawns from that copy. Uninstall deletes the copy. The original `.zip` on disk is untouched.
|
|
84
86
|
|
|
85
|
-
The iframe is `sandbox="allow-scripts"` with no `allow-same-origin`, so its origin is opaque. Host posts to that frame's `contentWindow` with target `*`. The string `null` is not a legal `postMessage` targetOrigin. Receive still accepts only `event.source === contentWindow`. Guest `connectHost` posts `hello` to `parent` with `*`. The host answers `hello` and also pushes `ready` on iframe `load`, because a cached panel can fire `hello` while React is swapping the listener.
|
|
87
|
+
The iframe is `sandbox="allow-scripts"` with no `allow-same-origin`, so its origin is opaque. Host posts to that frame's `contentWindow` with target `*`. The string `null` is not a legal `postMessage` targetOrigin. Receive still accepts only `event.source === contentWindow`. Every guest document also runs under `guestFramePolicy` (`src/frame-policy.ts`): the server sends it with `sandbox allow-scripts` on each served file, the relay loader as a `<meta>`. It keeps the guest off the network: scripts, styles, images, fonts, media, workers and nested frames load only from the package (`'self'`) or `data:`/`blob:`, and `connect-src` is the guest's own `/api/guests/<id>/` path (named by the request's `Host`, since a sandboxed document has no origin for `'self'`; `'none'` in relay). `'self'` for connections would reach the whole OpenChamber API. External data goes through the host (`request` to the declared, approved `apiOrigin`) or to `contributes.origins`: 1 to `GUEST_ORIGINS_MAX` (8) plain https origins (literal hostnames, so no `*` or other CSP syntax can widen them), granted as the `origins` capability. Its scope is the sorted list (`grant-scope.js`), so an update that adds an origin is unapproved until the dialog runs again. Approved origins join `connect-src`, `img-src`, `font-src`, `style-src` and `media-src`, never `script-src`/`worker-src`/`frame-src`: code from elsewhere could change after approval. The server reads them from the effective grants when it serves a file; relay frames get them from `PluginPane` (`approvedOrigins`, part of the frame key so an approval reloads the frame). External CDN assets not declared there do not load; bundle them. Guest files are served with `Access-Control-Allow-Origin: null`, because fonts and fetch from the opaque-origin frame are CORS requests. A guest navigating its own frame elsewhere is refused by the desktop shell (`packages/electron/guest-frame-navigation.mjs`, `will-frame-navigate`); the web runtime has no such hook. Guest `connectHost` posts `hello` to `parent` with `*`. The host answers `hello` and also pushes `ready` on iframe `load`, because a cached panel can fire `hello` while React is swapping the listener.
|
|
86
88
|
|
|
87
89
|
File calls go through `POST /api/guests/:id/files` with the `x-opencode-directory` header of the open project (`lib/guests/files.ts`). `PluginPane` answers `NOT_GRANTED` and `NO_DIRECTORY` itself, mirroring the server, so a refused call costs no round trip; the server is still the authority.
|
|
88
90
|
|
package/dist/contract.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from './api-version.ts';
|
|
2
|
+
import type { FileEditorChange, FileEditorDocument, FileSnapshotRequest, FileSnapshotResultPayload } from './file-editor.ts';
|
|
2
3
|
import type { GuestSessionWorktree, GuestStorageRequest, GuestStorageResult, GuestWorkspaceQuery, GuestWorkspaceSnapshot, GuestWorkspaceSubscription, GuestWorkspaceUpdate, GuestWorktree } from './workspace.ts';
|
|
3
4
|
export type HostThemeMode = 'light' | 'dark';
|
|
4
5
|
export type HostThemeTokens = {
|
|
@@ -54,7 +55,7 @@ export type SessionSnapshot = {
|
|
|
54
55
|
* Which host chrome mounted this iframe. Not `openSurface`. `status` is the
|
|
55
56
|
* extension's section in the chat's Work Status panel.
|
|
56
57
|
*/
|
|
57
|
-
export type GuestHostSurface = 'panel' | 'dialog' | 'page' | 'background' | 'status';
|
|
58
|
+
export type GuestHostSurface = 'panel' | 'dialog' | 'page' | 'background' | 'status' | 'file';
|
|
58
59
|
export type GuestConnection = {
|
|
59
60
|
connected: boolean;
|
|
60
61
|
account: string;
|
|
@@ -434,6 +435,24 @@ export type HostActionMessage = Envelope & {
|
|
|
434
435
|
id: string;
|
|
435
436
|
payload: GuestActionItem;
|
|
436
437
|
};
|
|
438
|
+
/** The file a `file` surface edits; pushed after `ready` and again whenever the frame reconnects. */
|
|
439
|
+
export type HostFileOpenMessage = Envelope & {
|
|
440
|
+
type: 'file-open';
|
|
441
|
+
payload: FileEditorDocument;
|
|
442
|
+
};
|
|
443
|
+
/** Host → guest request. The guest answers with `file-snapshot-result` carrying the same `id`. */
|
|
444
|
+
export type HostFileSnapshotMessage = Envelope & {
|
|
445
|
+
type: 'file-snapshot';
|
|
446
|
+
id: string;
|
|
447
|
+
payload: FileSnapshotRequest;
|
|
448
|
+
};
|
|
449
|
+
/** The snapshot with this `version` is on disk. */
|
|
450
|
+
export type HostFileSavedMessage = Envelope & {
|
|
451
|
+
type: 'file-saved';
|
|
452
|
+
payload: {
|
|
453
|
+
version: string;
|
|
454
|
+
};
|
|
455
|
+
};
|
|
437
456
|
export type HostResultMessage = Envelope & {
|
|
438
457
|
type: 'result';
|
|
439
458
|
id: string;
|
|
@@ -448,7 +467,7 @@ export type HostResultMessage = Envelope & {
|
|
|
448
467
|
export type HostMessage = (Envelope & {
|
|
449
468
|
type: 'workspace';
|
|
450
469
|
payload: GuestWorkspaceUpdate;
|
|
451
|
-
}) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostResolveMessage | HostActionMessage | HostResultMessage;
|
|
470
|
+
}) | HostReadyMessage | HostDirectoryMessage | HostSessionMessage | HostConnectionMessage | HostSettingsMessage | HostSessionLifecycleMessage | HostItemMessage | HostResolveMessage | HostActionMessage | HostFileOpenMessage | HostFileSnapshotMessage | HostFileSavedMessage | HostResultMessage;
|
|
452
471
|
type GuestCall<Type extends string, Payload = never> = Envelope & {
|
|
453
472
|
type: Type;
|
|
454
473
|
id: string;
|
|
@@ -501,11 +520,30 @@ export type GuestActionResultMessage = Envelope & {
|
|
|
501
520
|
id: string;
|
|
502
521
|
payload: ActionResultPayload;
|
|
503
522
|
};
|
|
523
|
+
/** Answers a host `file-snapshot` by `id`. The host sends no `result` back. */
|
|
524
|
+
export type GuestFileSnapshotResultMessage = Envelope & {
|
|
525
|
+
type: 'file-snapshot-result';
|
|
526
|
+
id: string;
|
|
527
|
+
payload: FileSnapshotResultPayload;
|
|
528
|
+
};
|
|
529
|
+
/** A file editor's state changed. Fire and forget. */
|
|
530
|
+
export type GuestFileChangeMessage = Envelope & {
|
|
531
|
+
type: 'file-change';
|
|
532
|
+
payload: FileEditorChange;
|
|
533
|
+
};
|
|
534
|
+
/** The user asked to save (Cmd/Ctrl+S inside the frame). Fire and forget. */
|
|
535
|
+
export type GuestFileSaveMessage = Envelope & {
|
|
536
|
+
type: 'file-save';
|
|
537
|
+
};
|
|
538
|
+
/** The editor cannot open this file; the host shows its source instead. Fire and forget. */
|
|
539
|
+
export type GuestFileUnsupportedMessage = Envelope & {
|
|
540
|
+
type: 'file-unsupported';
|
|
541
|
+
};
|
|
504
542
|
export type GuestMessage = GuestCall<'workspace-read', GuestWorkspaceQuery> | GuestCall<'workspace-subscribe', GuestWorkspaceSubscription> | GuestCall<'workspace-unsubscribe', {
|
|
505
543
|
subscriptionId: string;
|
|
506
544
|
}> | GuestCall<'storage', GuestStorageRequest> | GuestCall<'open-session', {
|
|
507
545
|
sessionId: string;
|
|
508
|
-
}> | 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;
|
|
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;
|
|
509
547
|
export declare const isServiceStatusResult: (value: HostResultPayload | undefined) => value is ServiceStatusResult;
|
|
510
548
|
export declare const isGuestRequestResult: (value: HostResultPayload | undefined) => value is GuestRequestResult;
|
|
511
549
|
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
|
+
'file-open', 'file-snapshot', 'file-saved',
|
|
214
215
|
]);
|
|
215
216
|
const asWireRecord = (data) => (Object(data) === data ? data : null);
|
|
216
217
|
const isNonEmptyString = (value) => (String(value) === value && value.length > 0);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Editors one package may declare. */
|
|
2
|
+
export declare const GUEST_FILE_EDITORS_MAX = 8;
|
|
3
|
+
/** Characters in an editor's `title`. */
|
|
4
|
+
export declare const GUEST_FILE_EDITOR_TITLE_MAX = 60;
|
|
5
|
+
/** File-name patterns one editor may list. */
|
|
6
|
+
export declare const GUEST_FILE_EDITOR_PATTERNS_MAX = 16;
|
|
7
|
+
/** Characters in one file-name pattern. */
|
|
8
|
+
export declare const GUEST_FILE_EDITOR_PATTERN_MAX = 128;
|
|
9
|
+
/**
|
|
10
|
+
* Characters of file text, or bytes of a binary file, the host hands to an
|
|
11
|
+
* editor and accepts back. Larger than `GUEST_FILE_CONTENT_MAX`: drawings with
|
|
12
|
+
* embedded images and office documents run to several megabytes. A larger
|
|
13
|
+
* file stays with the host's own view.
|
|
14
|
+
*/
|
|
15
|
+
export declare const GUEST_FILE_EDITOR_CONTENT_MAX = 20000000;
|
|
16
|
+
/** Characters in the version a snapshot carries. */
|
|
17
|
+
export declare const GUEST_FILE_EDITOR_VERSION_MAX = 256;
|
|
18
|
+
export type FileEditorContribution = {
|
|
19
|
+
/** Kebab-case, unique within the package. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Shown where the host names the editor. */
|
|
22
|
+
title: string;
|
|
23
|
+
/** File-name globs, matched case-insensitively against the name only: `*` is any run, `?` one character. */
|
|
24
|
+
match: string[];
|
|
25
|
+
/** Package-local `.html` the Files view loads for a matching file. */
|
|
26
|
+
entry: string;
|
|
27
|
+
/**
|
|
28
|
+
* `text` (the default) hands over the file as a string and only claims text
|
|
29
|
+
* files. `binary` hands over its bytes and claims any matching file, binary
|
|
30
|
+
* or not; the host has no source view for it.
|
|
31
|
+
*/
|
|
32
|
+
content?: FileEditorContentKind;
|
|
33
|
+
};
|
|
34
|
+
export type FileEditorContentKind = 'text' | 'binary';
|
|
35
|
+
/**
|
|
36
|
+
* A file-name glob: no path separators or NUL, and at least one character
|
|
37
|
+
* that is not a wildcard, so no editor can claim every file.
|
|
38
|
+
*/
|
|
39
|
+
export declare const isFileEditorPattern: (value: string) => boolean;
|
|
40
|
+
/** Whether `fileName` (a name, not a path) matches one file-editor pattern. */
|
|
41
|
+
export declare const matchesFileEditorPattern: (fileName: string, pattern: string) => boolean;
|
|
42
|
+
/** What the host hands the editor: the file it opened. */
|
|
43
|
+
export type FileEditorDocument = {
|
|
44
|
+
/** Path as the host shows it; for display, never for reading. */
|
|
45
|
+
path: string;
|
|
46
|
+
name: string;
|
|
47
|
+
/** True when the host will not save this file; the editor should not offer edits. */
|
|
48
|
+
readOnly: boolean;
|
|
49
|
+
} & (
|
|
50
|
+
/** The file's text with `\n` line endings; the host restores the file's own on write. */
|
|
51
|
+
{
|
|
52
|
+
encoding: 'text';
|
|
53
|
+
content: string;
|
|
54
|
+
}
|
|
55
|
+
/** The file's bytes, exactly as on disk. */
|
|
56
|
+
| {
|
|
57
|
+
encoding: 'binary';
|
|
58
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* The edited file and the version of the editor's state it came from: text
|
|
62
|
+
* for a `text` editor, bytes for a `binary` one.
|
|
63
|
+
*/
|
|
64
|
+
export type FileEditorSnapshot = {
|
|
65
|
+
/** Opaque to the host; handed back through `onFileSaved` once this snapshot is on disk. */
|
|
66
|
+
version: string;
|
|
67
|
+
} & ({
|
|
68
|
+
content: string;
|
|
69
|
+
} | {
|
|
70
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
71
|
+
});
|
|
72
|
+
/** Characters of text or bytes a snapshot or document carries. */
|
|
73
|
+
export declare const fileEditorPayloadSize: (value: {
|
|
74
|
+
content: string;
|
|
75
|
+
} | {
|
|
76
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
77
|
+
}) => number;
|
|
78
|
+
/** Whether two documents are the same file with the same content, byte for byte. */
|
|
79
|
+
export declare const sameFileEditorDocument: (left: FileEditorDocument, right: FileEditorDocument) => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Why the host wants a snapshot: `save` writes it to disk, `handoff` moves it
|
|
82
|
+
* into the host's source view or to the other place the editor is shown.
|
|
83
|
+
*/
|
|
84
|
+
export type FileSnapshotPurpose = 'save' | 'handoff';
|
|
85
|
+
export type FileSnapshotRequest = {
|
|
86
|
+
purpose: FileSnapshotPurpose;
|
|
87
|
+
};
|
|
88
|
+
export type FileSnapshotResultPayload = {
|
|
89
|
+
snapshot: FileEditorSnapshot;
|
|
90
|
+
} | {
|
|
91
|
+
error: string;
|
|
92
|
+
};
|
|
93
|
+
/** The editor's state after a change: `dirty` against what was saved, `edited` when the document itself changed. */
|
|
94
|
+
export type FileEditorChange = {
|
|
95
|
+
dirty: boolean;
|
|
96
|
+
edited: boolean;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Tracks whether an editor differs from what was last written. The host
|
|
100
|
+
* writes one snapshot and reports that snapshot's version saved, so edits
|
|
101
|
+
* made while the write ran leave the editor dirty instead of being recorded as
|
|
102
|
+
* saved but never written.
|
|
103
|
+
*/
|
|
104
|
+
export declare const createFileSaveTracker: (initialVersion: string | null) => {
|
|
105
|
+
/** A new live version; `edited` is true when it differs from the previous one. */
|
|
106
|
+
observe: (version: string) => FileEditorChange;
|
|
107
|
+
/** Records a written version; returns whether the live state is still dirty. */
|
|
108
|
+
markSaved: (version: string) => boolean;
|
|
109
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// File editors (`contributes.fileEditors`): the host's Files view opens files
|
|
2
|
+
// whose names match an editor's patterns in that editor's frame. The host owns
|
|
3
|
+
// the file: it reads it, hands the text to the frame, asks for the edited text
|
|
4
|
+
// when it saves, and keeps saving, autosave, the unsaved-changes prompt, and
|
|
5
|
+
// external-change handling. The editor never touches the disk.
|
|
6
|
+
/** Editors one package may declare. */
|
|
7
|
+
export const GUEST_FILE_EDITORS_MAX = 8;
|
|
8
|
+
/** Characters in an editor's `title`. */
|
|
9
|
+
export const GUEST_FILE_EDITOR_TITLE_MAX = 60;
|
|
10
|
+
/** File-name patterns one editor may list. */
|
|
11
|
+
export const GUEST_FILE_EDITOR_PATTERNS_MAX = 16;
|
|
12
|
+
/** Characters in one file-name pattern. */
|
|
13
|
+
export const GUEST_FILE_EDITOR_PATTERN_MAX = 128;
|
|
14
|
+
/**
|
|
15
|
+
* Characters of file text, or bytes of a binary file, the host hands to an
|
|
16
|
+
* editor and accepts back. Larger than `GUEST_FILE_CONTENT_MAX`: drawings with
|
|
17
|
+
* embedded images and office documents run to several megabytes. A larger
|
|
18
|
+
* file stays with the host's own view.
|
|
19
|
+
*/
|
|
20
|
+
export const GUEST_FILE_EDITOR_CONTENT_MAX = 20_000_000;
|
|
21
|
+
/** Characters in the version a snapshot carries. */
|
|
22
|
+
export const GUEST_FILE_EDITOR_VERSION_MAX = 256;
|
|
23
|
+
/**
|
|
24
|
+
* A file-name glob: no path separators or NUL, and at least one character
|
|
25
|
+
* that is not a wildcard, so no editor can claim every file.
|
|
26
|
+
*/
|
|
27
|
+
export const isFileEditorPattern = (value) => (value.length > 0
|
|
28
|
+
&& value.length <= GUEST_FILE_EDITOR_PATTERN_MAX
|
|
29
|
+
&& !/[/\\\0]/.test(value)
|
|
30
|
+
&& /[^*?]/.test(value));
|
|
31
|
+
const patternExpressions = new Map();
|
|
32
|
+
const patternExpression = (pattern) => {
|
|
33
|
+
const cached = patternExpressions.get(pattern);
|
|
34
|
+
if (cached)
|
|
35
|
+
return cached;
|
|
36
|
+
const source = pattern
|
|
37
|
+
.split('')
|
|
38
|
+
.map((character) => {
|
|
39
|
+
if (character === '*')
|
|
40
|
+
return '.*';
|
|
41
|
+
if (character === '?')
|
|
42
|
+
return '.';
|
|
43
|
+
return character.replace(/[\\^$.|+()[\]{}]/g, '\\$&');
|
|
44
|
+
})
|
|
45
|
+
.join('');
|
|
46
|
+
const expression = new RegExp(`^${source}$`, 'is');
|
|
47
|
+
patternExpressions.set(pattern, expression);
|
|
48
|
+
return expression;
|
|
49
|
+
};
|
|
50
|
+
/** Whether `fileName` (a name, not a path) matches one file-editor pattern. */
|
|
51
|
+
export const matchesFileEditorPattern = (fileName, pattern) => (isFileEditorPattern(pattern) && patternExpression(pattern).test(fileName));
|
|
52
|
+
/** Characters of text or bytes a snapshot or document carries. */
|
|
53
|
+
export const fileEditorPayloadSize = (value) => ('bytes' in value ? value.bytes.byteLength : value.content.length);
|
|
54
|
+
/** Whether two documents are the same file with the same content, byte for byte. */
|
|
55
|
+
export const sameFileEditorDocument = (left, right) => {
|
|
56
|
+
if (left.path !== right.path || left.readOnly !== right.readOnly)
|
|
57
|
+
return false;
|
|
58
|
+
if (left.encoding === 'text' || right.encoding === 'text') {
|
|
59
|
+
return left.encoding === 'text' && right.encoding === 'text' && left.content === right.content;
|
|
60
|
+
}
|
|
61
|
+
if (left.bytes.byteLength !== right.bytes.byteLength)
|
|
62
|
+
return false;
|
|
63
|
+
for (let index = 0; index < left.bytes.byteLength; index += 1) {
|
|
64
|
+
if (left.bytes[index] !== right.bytes[index])
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Tracks whether an editor differs from what was last written. The host
|
|
71
|
+
* writes one snapshot and reports that snapshot's version saved, so edits
|
|
72
|
+
* made while the write ran leave the editor dirty instead of being recorded as
|
|
73
|
+
* saved but never written.
|
|
74
|
+
*/
|
|
75
|
+
export const createFileSaveTracker = (initialVersion) => {
|
|
76
|
+
let saved = initialVersion;
|
|
77
|
+
let live = initialVersion;
|
|
78
|
+
return {
|
|
79
|
+
/** A new live version; `edited` is true when it differs from the previous one. */
|
|
80
|
+
observe: (version) => {
|
|
81
|
+
const edited = version !== live;
|
|
82
|
+
live = version;
|
|
83
|
+
return { edited, dirty: version !== saved };
|
|
84
|
+
},
|
|
85
|
+
/** Records a written version; returns whether the live state is still dirty. */
|
|
86
|
+
markSaved: (version) => {
|
|
87
|
+
saved = version;
|
|
88
|
+
return live !== version;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Content Security Policy every guest document runs under, on top of the
|
|
3
|
+
* `sandbox="allow-scripts"` iframe. It keeps the guest off the network: its
|
|
4
|
+
* own package files load, and `data:`/`blob:` URLs it builds itself, nothing
|
|
5
|
+
* else. Reaching the outside world is the host's job (`request` to the one
|
|
6
|
+
* declared `apiOrigin`, which the user approved), so a guest cannot send what
|
|
7
|
+
* it was shown (conversation, files, model output) anywhere on its own.
|
|
8
|
+
*
|
|
9
|
+
* `connectSource` is where `fetch`/XHR/WebSocket may go: the guest's own
|
|
10
|
+
* package path on the server, or null for none. `'self'` would reach the
|
|
11
|
+
* whole OpenChamber API, which a server without a UI password answers.
|
|
12
|
+
*
|
|
13
|
+
* `origins` are the `contributes.origins` the user approved: the guest may
|
|
14
|
+
* exchange data with them directly (fetch, images, fonts, styles, media).
|
|
15
|
+
* Never scripts or workers: code loaded from elsewhere could change after
|
|
16
|
+
* the extension was approved.
|
|
17
|
+
*
|
|
18
|
+
* Inline and eval stay allowed: they run code, they do not open connections.
|
|
19
|
+
*/
|
|
20
|
+
export declare const guestFramePolicy: (connectSource: string | null, origins?: readonly string[]) => string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Content Security Policy every guest document runs under, on top of the
|
|
3
|
+
* `sandbox="allow-scripts"` iframe. It keeps the guest off the network: its
|
|
4
|
+
* own package files load, and `data:`/`blob:` URLs it builds itself, nothing
|
|
5
|
+
* else. Reaching the outside world is the host's job (`request` to the one
|
|
6
|
+
* declared `apiOrigin`, which the user approved), so a guest cannot send what
|
|
7
|
+
* it was shown (conversation, files, model output) anywhere on its own.
|
|
8
|
+
*
|
|
9
|
+
* `connectSource` is where `fetch`/XHR/WebSocket may go: the guest's own
|
|
10
|
+
* package path on the server, or null for none. `'self'` would reach the
|
|
11
|
+
* whole OpenChamber API, which a server without a UI password answers.
|
|
12
|
+
*
|
|
13
|
+
* `origins` are the `contributes.origins` the user approved: the guest may
|
|
14
|
+
* exchange data with them directly (fetch, images, fonts, styles, media).
|
|
15
|
+
* Never scripts or workers: code loaded from elsewhere could change after
|
|
16
|
+
* the extension was approved.
|
|
17
|
+
*
|
|
18
|
+
* Inline and eval stay allowed: they run code, they do not open connections.
|
|
19
|
+
*/
|
|
20
|
+
export const guestFramePolicy = (connectSource, origins = []) => {
|
|
21
|
+
const approved = origins.length > 0 ? ` ${origins.join(' ')}` : '';
|
|
22
|
+
const local = "'self' data: blob:";
|
|
23
|
+
const connect = [connectSource, ...origins].filter((source) => Boolean(source));
|
|
24
|
+
return [
|
|
25
|
+
"default-src 'none'",
|
|
26
|
+
`script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' ${local}`,
|
|
27
|
+
`style-src 'unsafe-inline' ${local}${approved}`,
|
|
28
|
+
`img-src ${local}${approved}`,
|
|
29
|
+
`font-src ${local}${approved}`,
|
|
30
|
+
`media-src ${local}${approved}`,
|
|
31
|
+
`worker-src ${local}`,
|
|
32
|
+
`frame-src ${local}`,
|
|
33
|
+
`connect-src ${connect.length > 0 ? connect.join(' ') : "'none'"}`,
|
|
34
|
+
"object-src 'none'",
|
|
35
|
+
"form-action 'none'",
|
|
36
|
+
].join('; ');
|
|
37
|
+
};
|
package/dist/host.d.ts
CHANGED
|
@@ -1,5 +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 FileEditorChange, type FileEditorDocument, type FileEditorSnapshot, type FileSnapshotPurpose } from './file-editor.ts';
|
|
3
4
|
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
5
|
export type HostFrame = {
|
|
5
6
|
addEventListener: Window['addEventListener'];
|
|
@@ -113,6 +114,27 @@ export type HostClient = {
|
|
|
113
114
|
* chrome and ignore it.
|
|
114
115
|
*/
|
|
115
116
|
setHeight: (height: number) => Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* The file a `file` surface edits (`contributes.fileEditors`). Replays the
|
|
119
|
+
* last file; called again when the host reconnects the frame. Registering
|
|
120
|
+
* also routes Cmd/Ctrl+S inside the frame to the host's save.
|
|
121
|
+
*/
|
|
122
|
+
onFileOpen: (listener: (file: FileEditorDocument) => void) => () => void;
|
|
123
|
+
/**
|
|
124
|
+
* Answer the host when it needs the edited text: to save it (`save`) or to
|
|
125
|
+
* move it into its source view (`handoff`). Return the whole file and a
|
|
126
|
+
* version of the state it came from; a thrown error fails the save. One
|
|
127
|
+
* handler at a time.
|
|
128
|
+
*/
|
|
129
|
+
onFileSnapshot: (handler: (purpose: FileSnapshotPurpose) => Promise<FileEditorSnapshot> | FileEditorSnapshot) => () => void;
|
|
130
|
+
/** The snapshot with this version is on disk; edits made since keep the file dirty. */
|
|
131
|
+
onFileSaved: (listener: (version: string) => void) => () => void;
|
|
132
|
+
/** Report the editor's state after a change. `edited` delays autosave until edits stop. */
|
|
133
|
+
reportFileChange: (change: FileEditorChange) => void;
|
|
134
|
+
/** Ask the host to save now, as Cmd/Ctrl+S does. */
|
|
135
|
+
requestFileSave: () => void;
|
|
136
|
+
/** This file cannot be opened here; the host shows its source instead. */
|
|
137
|
+
reportFileUnsupported: () => void;
|
|
116
138
|
dispose: () => void;
|
|
117
139
|
};
|
|
118
140
|
export declare class HostRequestError extends Error {
|
package/dist/host.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from "./api-version.js";
|
|
2
2
|
import { GUEST_STORAGE_KEY_MAX, GUEST_STORAGE_VALUE_BYTES } from "./workspace.js";
|
|
3
|
+
import { GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_VERSION_MAX, fileEditorPayloadSize, sameFileEditorDocument, } from "./file-editor.js";
|
|
3
4
|
import { GUEST_FILE_CONTENT_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_TOAST_MAX, GUEST_FILE_PATH_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TIMEOUT_MS, GUEST_REQUEST_TIMEOUT_MS, GUEST_RESOLVE_ERROR_MAX, isGuestFilePath, isGuestRequestPath, clampAttachRequest, clampBadgeCount, clampFrameHeight, isGuestCommitSha, clampPromptRequest, clampStartSessionRequest, readHostMessage, isGenerateResult, isFileListResult, isFileReadResult, isFileStatResult, isJsonValue, isFileWriteResult, isServiceStatusResult, isGuestRequestResult, isPromptResult, isStartSessionResult, } from "./contract.js";
|
|
5
|
+
// Structural, not `instanceof KeyboardEvent`: the frame's own realm owns that
|
|
6
|
+
// constructor, and non-browser runtimes have none.
|
|
7
|
+
const isKeyEvent = (event) => 'key' in event && 'metaKey' in event && 'ctrlKey' in event;
|
|
8
|
+
const isSaveShortcut = (event) => ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 's');
|
|
4
9
|
export class HostRequestError extends Error {
|
|
5
10
|
code;
|
|
6
11
|
constructor(code, message) {
|
|
@@ -33,6 +38,11 @@ export const connectHost = (options = {}) => {
|
|
|
33
38
|
const itemListeners = new Set();
|
|
34
39
|
let resolveHandler = null;
|
|
35
40
|
let actionHandler = null;
|
|
41
|
+
const fileOpenListeners = new Set();
|
|
42
|
+
const fileSavedListeners = new Set();
|
|
43
|
+
let fileSnapshotHandler = null;
|
|
44
|
+
let lastFile = null;
|
|
45
|
+
let saveShortcutInstalled = false;
|
|
36
46
|
const pending = new Map();
|
|
37
47
|
const workspaceListeners = new Map();
|
|
38
48
|
let disposed = false;
|
|
@@ -153,6 +163,47 @@ export const connectHost = (options = {}) => {
|
|
|
153
163
|
});
|
|
154
164
|
return;
|
|
155
165
|
}
|
|
166
|
+
if (message.type === 'file-open') {
|
|
167
|
+
// The host pushes on both `hello` and iframe load; the same file twice
|
|
168
|
+
// must not make the editor reload what the user may already be editing.
|
|
169
|
+
const next = message.payload;
|
|
170
|
+
if (lastFile && sameFileEditorDocument(lastFile, next))
|
|
171
|
+
return;
|
|
172
|
+
lastFile = next;
|
|
173
|
+
emit(fileOpenListeners, message.payload);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (message.type === 'file-saved') {
|
|
177
|
+
emit(fileSavedListeners, message.payload.version);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (message.type === 'file-snapshot') {
|
|
181
|
+
const answer = (payload) => {
|
|
182
|
+
if (!disposed)
|
|
183
|
+
post({ channel: OPENCHAMBER_SDK_CHANNEL, v: OPENCHAMBER_SDK_API_VERSION,
|
|
184
|
+
type: 'file-snapshot-result', id: message.id, payload });
|
|
185
|
+
};
|
|
186
|
+
const fail = (text) => answer({ error: (text.trim() || 'Could not read the edited file.').slice(0, GUEST_RESOLVE_ERROR_MAX) });
|
|
187
|
+
const handler = fileSnapshotHandler;
|
|
188
|
+
if (!handler) {
|
|
189
|
+
fail('This extension does not edit files.');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
Promise.resolve().then(() => handler(message.payload.purpose)).then((snapshot) => {
|
|
193
|
+
if (fileEditorPayloadSize(snapshot) > GUEST_FILE_EDITOR_CONTENT_MAX) {
|
|
194
|
+
fail(`The file is over ${GUEST_FILE_EDITOR_CONTENT_MAX} ${'bytes' in snapshot ? 'bytes' : 'characters'}.`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (snapshot.version.length > GUEST_FILE_EDITOR_VERSION_MAX) {
|
|
198
|
+
fail(`The snapshot version is over ${GUEST_FILE_EDITOR_VERSION_MAX} characters.`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
answer({ snapshot: 'bytes' in snapshot
|
|
202
|
+
? { bytes: snapshot.bytes, version: snapshot.version }
|
|
203
|
+
: { content: snapshot.content, version: snapshot.version } });
|
|
204
|
+
}, (error) => fail(error instanceof Error ? error.message : String(error)));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
156
207
|
if (message.type === 'resolve') {
|
|
157
208
|
const answer = (payload) => {
|
|
158
209
|
post({
|
|
@@ -208,6 +259,21 @@ export const connectHost = (options = {}) => {
|
|
|
208
259
|
};
|
|
209
260
|
const request = (message) => (send(message).then(() => undefined));
|
|
210
261
|
const envelope = { channel: OPENCHAMBER_SDK_CHANNEL, v: OPENCHAMBER_SDK_API_VERSION };
|
|
262
|
+
// Fire-and-forget notices: nothing to wait for, and nothing to post to
|
|
263
|
+
// without a host frame.
|
|
264
|
+
const notify = (message) => {
|
|
265
|
+
if (!disposed && target.parent !== target)
|
|
266
|
+
post(message);
|
|
267
|
+
};
|
|
268
|
+
const requestFileSave = () => notify({ ...envelope, type: 'file-save' });
|
|
269
|
+
// Capture phase, so an editor library that binds Cmd/Ctrl+S itself does not
|
|
270
|
+
// swallow the host's save.
|
|
271
|
+
const onSaveShortcut = (event) => {
|
|
272
|
+
if (!isKeyEvent(event) || !isSaveShortcut(event))
|
|
273
|
+
return;
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
requestFileSave();
|
|
276
|
+
};
|
|
211
277
|
const requireIdentity = (value, maximum = 1024) => {
|
|
212
278
|
if (!value.trim() || value.length > maximum)
|
|
213
279
|
throw new HostRequestError('HOST_REJECTED', `Identity must contain 1 to ${maximum} characters.`);
|
|
@@ -627,6 +693,34 @@ export const connectHost = (options = {}) => {
|
|
|
627
693
|
id: nextId(ids),
|
|
628
694
|
payload: { height: clampFrameHeight(height) },
|
|
629
695
|
}),
|
|
696
|
+
onFileOpen: (listener) => {
|
|
697
|
+
fileOpenListeners.add(listener);
|
|
698
|
+
if (!saveShortcutInstalled) {
|
|
699
|
+
saveShortcutInstalled = true;
|
|
700
|
+
target.addEventListener('keydown', onSaveShortcut, true);
|
|
701
|
+
}
|
|
702
|
+
if (lastFile)
|
|
703
|
+
listener(lastFile);
|
|
704
|
+
return () => {
|
|
705
|
+
fileOpenListeners.delete(listener);
|
|
706
|
+
};
|
|
707
|
+
},
|
|
708
|
+
onFileSnapshot: (handler) => {
|
|
709
|
+
fileSnapshotHandler = handler;
|
|
710
|
+
return () => {
|
|
711
|
+
if (fileSnapshotHandler === handler)
|
|
712
|
+
fileSnapshotHandler = null;
|
|
713
|
+
};
|
|
714
|
+
},
|
|
715
|
+
onFileSaved: (listener) => {
|
|
716
|
+
fileSavedListeners.add(listener);
|
|
717
|
+
return () => {
|
|
718
|
+
fileSavedListeners.delete(listener);
|
|
719
|
+
};
|
|
720
|
+
},
|
|
721
|
+
reportFileChange: (change) => notify({ ...envelope, type: 'file-change', payload: { dirty: change.dirty, edited: change.edited } }),
|
|
722
|
+
requestFileSave,
|
|
723
|
+
reportFileUnsupported: () => notify({ ...envelope, type: 'file-unsupported' }),
|
|
630
724
|
dispose: () => {
|
|
631
725
|
for (const subscriptionId of workspaceListeners.keys()) {
|
|
632
726
|
post({ ...envelope, type: 'workspace-unsubscribe', id: nextId(ids), payload: { subscriptionId } });
|
|
@@ -635,6 +729,11 @@ export const connectHost = (options = {}) => {
|
|
|
635
729
|
disposed = true;
|
|
636
730
|
resolveHandler = null;
|
|
637
731
|
actionHandler = null;
|
|
732
|
+
fileSnapshotHandler = null;
|
|
733
|
+
fileOpenListeners.clear();
|
|
734
|
+
fileSavedListeners.clear();
|
|
735
|
+
if (saveShortcutInstalled)
|
|
736
|
+
target.removeEventListener('keydown', onSaveShortcut, true);
|
|
638
737
|
target.removeEventListener('message', onMessage);
|
|
639
738
|
for (const waiter of pending.values()) {
|
|
640
739
|
clearTimeout(waiter.timer);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
export { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL, OPENCHAMBER_SDK_MANIFEST_API_VERSIONS } from './api-version.ts';
|
|
2
2
|
export { GUEST_SCROLLBAR_CSS, GUEST_SCROLLBAR_SCRIPT } from './scrollbar-style.ts';
|
|
3
|
+
export { guestFramePolicy } from './frame-policy.ts';
|
|
3
4
|
export type { GuestLoadState, GuestProject, GuestWorktree, GuestSessionActivity, GuestSessionRecord, GuestDirectoryCoverage, GuestProjectsSnapshot, GuestWorktreesSnapshot, GuestSessionsSnapshot, GuestWorkspaceSnapshot, GuestWorkspaceQuery, GuestWorkspaceSubscription, GuestWorkspaceUpdate, GuestStorageRequest, GuestStorageResult, GuestSessionWorktree } from './workspace.ts';
|
|
4
5
|
export { GUEST_STORAGE_KEY_MAX, GUEST_STORAGE_KEYS_MAX, GUEST_STORAGE_VALUE_BYTES, GUEST_STORAGE_TOTAL_BYTES } from './workspace.ts';
|
|
5
6
|
export type { OpenChamberManifestApiVersion } from './api-version.ts';
|
|
6
7
|
export { compareOpenChamberVersions, hostMeetsOpenChamberEngine, openChamberEngineMinimum, OPENCHAMBER_ENGINE_PATTERN, parseOpenChamberVersion, } from './host-version.ts';
|
|
7
|
-
export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, toPublicService, toPublicIntegration, } from './manifest.ts';
|
|
8
|
+
export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, toPublicService, toPublicIntegration, } from './manifest.ts';
|
|
8
9
|
export type { GuestServiceProvides, GuestSurfaceDock, ServiceContribution, ServicePermissions, GuestActionContribution, GuestActionPayload, GuestActionRole, GuestActionWhere, GuestCommandContribution, GuestToolContribution, GuestToolOutput, AttachContribution, AttachContributionObject, AttachMode, DeclaredGuestCapability, GuestAuthorization, GuestCapability, IntegrationAuth, IntegrationContribution, IntegrationHost, IntegrationHostProvider, IntegrationOAuth, IntegrationOAuthAccount, IntegrationSettingField, IntegrationToken, IntegrationTokenScheme, OpenChamberContributes, OpenChamberEngines, OpenChamberManifest, PanelContribution, BackgroundContribution, PageContribution, StatusSectionContribution, ParseManifestErrorCode, ParseManifestFailure, ParseManifestResult, ParseManifestSuccess, PublicService, PublicServicePermissions, PublicGuestCapabilities, PublicIntegration, PublicIntegrationToken, PublicSocketBinding, ResolvedGuestApi, SocketBinding, SocketPlatform, } from './manifest.ts';
|
|
10
|
+
export { GUEST_FILE_EDITORS_MAX, GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_PATTERNS_MAX, GUEST_FILE_EDITOR_PATTERN_MAX, GUEST_FILE_EDITOR_TITLE_MAX, GUEST_FILE_EDITOR_VERSION_MAX, createFileSaveTracker, fileEditorPayloadSize, isFileEditorPattern, matchesFileEditorPattern, } from './file-editor.ts';
|
|
11
|
+
export type { FileEditorChange, FileEditorContentKind, FileEditorContribution, FileEditorDocument, FileEditorSnapshot, FileSnapshotPurpose, FileSnapshotRequest, FileSnapshotResultPayload, } from './file-editor.ts';
|
|
9
12
|
export { connectHost, HostRequestError } from './host.ts';
|
|
10
13
|
export type { HostClient, HostClientOptions, HostFrame } from './host.ts';
|
|
11
14
|
export { clampAttachRequest, clampBadgeCount, clampFrameHeight, isGuestCommitSha, GUEST_COMMIT_SHA, clampPromptRequest, clampStartSessionRequest, guestFileScope, isAttachData, isJsonValue, isFileListResult, isFileReadResult, isFileStatResult, isGenerateResult, isFileWriteResult, isGuestFilePath, isGuestAttachItem, isGuestMessageItem, isGuestSessionItem, isServiceStatusResult, isGuestRequestPath, isGuestRequestResult, isHostRequestErrorCode, isPromptResult, isStartSessionResult, readHostMessage, resolveHostRequestErrorCode, } from './contract.ts';
|
|
12
|
-
export type { BadgeRequest, ActionResultPayload, GuestActionItem, GuestActionResultMessage, HostActionMessage, GuestBadgeMessage, GuestResizeMessage, GuestOpenCommitMessage, OpenCommitRequest, ResizeRequest, GuestItem, GuestItemRole, GuestMessageItem, GuestResolveResultMessage, GuestSessionItem, GuestSessionItemMessage, HostResolveMessage, ResolveRequest, ResolveResultPayload, FileListEntry, FileListRequest, FileListResult, FileReadRequest, FileReadResult, FileStatRequest, FileStatResult, FileWriteRequest, FileWriteResult, GenerateRequest, GenerateResult, GuestFileEntryKind, GuestFileListMessage, GuestFileReadMessage, GuestFileScope, GuestFileStatKind, GuestFileStatMessage, GuestGenerateMessage, GuestFileWriteMessage, ServiceStatus, ServiceStatusResult, HostRequestErrorCode, AttachBranches, AttachIssueRequest, AttachThreadKind, JsonValue, HostResultPayload, PromptRequest, PromptResult, SessionLifecycleEvent, SessionLifecyclePhase, StartSessionRequest, StartSessionResult, StartSessionSent, ComposeRequest, GuestServiceRequestMessage, GuestServiceStatusMessage, GuestAttachMessage, GuestClipboardWriteMessage, GuestCloseMessage, GuestComposeMessage, GuestConnection, GuestHelloMessage, GuestMessage, GuestOauthDisconnectMessage, GuestOauthStartMessage, GuestOpenSurfaceMessage, GuestOpenUrlMessage, GuestRequest, GuestRequestMessage, GuestRequestMethod, GuestPromptMessage, GuestSessionLinkMessage, GuestStartSessionMessage, GuestRequestResult, GuestSettings, GuestToastMessage, GuestHostSurface, HostConnectionMessage, HostDirectoryMessage, HostItemMessage, HostMessage, HostReadyContext, HostReadyMessage, HostResultMessage, HostSessionLifecycleMessage, HostSessionMessage, HostSettingsMessage, HostTheme, HostThemeMode, HostThemeTokens, SessionSnapshot, ToastKind, ToastRequest, } from './contract.ts';
|
|
15
|
+
export type { BadgeRequest, ActionResultPayload, GuestActionItem, GuestActionResultMessage, HostActionMessage, HostFileOpenMessage, HostFileSnapshotMessage, HostFileSavedMessage, GuestFileSnapshotResultMessage, GuestFileChangeMessage, GuestFileSaveMessage, GuestFileUnsupportedMessage, GuestBadgeMessage, GuestResizeMessage, GuestOpenCommitMessage, OpenCommitRequest, ResizeRequest, GuestItem, GuestItemRole, GuestMessageItem, GuestResolveResultMessage, GuestSessionItem, GuestSessionItemMessage, HostResolveMessage, ResolveRequest, ResolveResultPayload, FileListEntry, FileListRequest, FileListResult, FileReadRequest, FileReadResult, FileStatRequest, FileStatResult, FileWriteRequest, FileWriteResult, GenerateRequest, GenerateResult, GuestFileEntryKind, GuestFileListMessage, GuestFileReadMessage, GuestFileScope, GuestFileStatKind, GuestFileStatMessage, GuestGenerateMessage, GuestFileWriteMessage, ServiceStatus, ServiceStatusResult, HostRequestErrorCode, AttachBranches, AttachIssueRequest, AttachThreadKind, JsonValue, HostResultPayload, PromptRequest, PromptResult, SessionLifecycleEvent, SessionLifecyclePhase, StartSessionRequest, StartSessionResult, StartSessionSent, ComposeRequest, GuestServiceRequestMessage, GuestServiceStatusMessage, GuestAttachMessage, GuestClipboardWriteMessage, GuestCloseMessage, GuestComposeMessage, GuestConnection, GuestHelloMessage, GuestMessage, GuestOauthDisconnectMessage, GuestOauthStartMessage, GuestOpenSurfaceMessage, GuestOpenUrlMessage, GuestRequest, GuestRequestMessage, GuestRequestMethod, GuestPromptMessage, GuestSessionLinkMessage, GuestStartSessionMessage, GuestRequestResult, GuestSettings, GuestToastMessage, GuestHostSurface, HostConnectionMessage, HostDirectoryMessage, HostItemMessage, HostMessage, HostReadyContext, HostReadyMessage, HostResultMessage, HostSessionLifecycleMessage, HostSessionMessage, HostSettingsMessage, HostTheme, HostThemeMode, HostThemeTokens, SessionSnapshot, ToastKind, ToastRequest, } from './contract.ts';
|
|
13
16
|
export { EMPTY_GUEST_CONNECTION, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_FILE_PATH_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_GENERATE_TIMEOUT_MS, GUEST_FILE_STAT_KINDS, SERVICE_STATUS_VALUES, HOST_REQUEST_ERROR_CODES, SESSION_LIFECYCLE_PHASES, START_SESSION_SENT, GUEST_ACCOUNT_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_TOAST_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_REQUEST_TIMEOUT_MS, GUEST_SETTING_VALUE_MAX, } from './contract.ts';
|
|
14
17
|
export { BROWSER_CONTROL_ACTIONS, BROWSER_PROVIDER_ACTION_TIMEOUT_MS, BROWSER_PROVIDER_IDLE_MS, BROWSER_PROVIDER_OPEN_TIMEOUT_MS, BROWSER_PROVIDER_PATH, BROWSER_PROVIDER_RESPONSE_MAX, BROWSER_SCROLL_DIRECTIONS, BROWSER_VIEWPORT_MODES, isBrowserControlAction, readBrowserProviderRequest, } from './service-providers.ts';
|
|
15
18
|
export type { BrowserCaptureData, BrowserCaptureParameters, BrowserClickData, BrowserClickParameters, BrowserConsoleProblem, BrowserControlAction, BrowserElementBounds, BrowserInspectData, BrowserInspectParameters, BrowserNavigationData, BrowserOpenData, BrowserOpenParameters, BrowserProviderContext, BrowserProviderData, BrowserProviderRequest, BrowserProviderResult, BrowserResizeData, BrowserResizeParameters, BrowserScrollData, BrowserScrollDirection, BrowserScrollParameters, BrowserSnapshotData, BrowserSnapshotElement, BrowserSnapshotParameters, BrowserTab, BrowserTabTarget, BrowserTypeData, BrowserTypeParameters, BrowserViewportMode, BrowserViewportSummary, } from './service-providers.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL, OPENCHAMBER_SDK_MANIFEST_API_VERSIONS } from "./api-version.js";
|
|
2
2
|
export { GUEST_SCROLLBAR_CSS, GUEST_SCROLLBAR_SCRIPT } from "./scrollbar-style.js";
|
|
3
|
+
export { guestFramePolicy } from "./frame-policy.js";
|
|
3
4
|
export { GUEST_STORAGE_KEY_MAX, GUEST_STORAGE_KEYS_MAX, GUEST_STORAGE_VALUE_BYTES, GUEST_STORAGE_TOTAL_BYTES } from "./workspace.js";
|
|
4
5
|
export { compareOpenChamberVersions, hostMeetsOpenChamberEngine, openChamberEngineMinimum, OPENCHAMBER_ENGINE_PATTERN, parseOpenChamberVersion, } from "./host-version.js";
|
|
5
|
-
export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, toPublicService, toPublicIntegration, } from "./manifest.js";
|
|
6
|
+
export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, toPublicService, toPublicIntegration, } from "./manifest.js";
|
|
7
|
+
export { GUEST_FILE_EDITORS_MAX, GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_PATTERNS_MAX, GUEST_FILE_EDITOR_PATTERN_MAX, GUEST_FILE_EDITOR_TITLE_MAX, GUEST_FILE_EDITOR_VERSION_MAX, createFileSaveTracker, fileEditorPayloadSize, isFileEditorPattern, matchesFileEditorPattern, } from "./file-editor.js";
|
|
6
8
|
export { connectHost, HostRequestError } from "./host.js";
|
|
7
9
|
export { clampAttachRequest, clampBadgeCount, clampFrameHeight, isGuestCommitSha, GUEST_COMMIT_SHA, clampPromptRequest, clampStartSessionRequest, guestFileScope, isAttachData, isJsonValue, isFileListResult, isFileReadResult, isFileStatResult, isGenerateResult, isFileWriteResult, isGuestFilePath, isGuestAttachItem, isGuestMessageItem, isGuestSessionItem, isServiceStatusResult, isGuestRequestPath, isGuestRequestResult, isHostRequestErrorCode, isPromptResult, isStartSessionResult, readHostMessage, resolveHostRequestErrorCode, } from "./contract.js";
|
|
8
10
|
export { EMPTY_GUEST_CONNECTION, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_FILE_PATH_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_GENERATE_TIMEOUT_MS, GUEST_FILE_STAT_KINDS, SERVICE_STATUS_VALUES, HOST_REQUEST_ERROR_CODES, SESSION_LIFECYCLE_PHASES, START_SESSION_SENT, GUEST_ACCOUNT_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_TOAST_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_REQUEST_TIMEOUT_MS, GUEST_SETTING_VALUE_MAX, } from "./contract.js";
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OpenChamberManifestApiVersion } from './api-version.ts';
|
|
2
|
+
import type { FileEditorContribution } from './file-editor.ts';
|
|
2
3
|
export declare const PANEL_ID: RegExp;
|
|
3
4
|
/**
|
|
4
5
|
* The extension's identity on the rail, the Extensions card, and the approval
|
|
@@ -271,16 +272,18 @@ export declare const serviceProvides: (service: Pick<ServiceContribution, "provi
|
|
|
271
272
|
* What a guest may do beyond drawing its own panel. The user approves the
|
|
272
273
|
* full list once, when the package is installed; a later package that asks
|
|
273
274
|
* for more is re-approved. `prompt`, `sessions`, `files`, and `model` are
|
|
274
|
-
* declared under `contributes.capabilities`; `service`, `network`,
|
|
275
|
-
* `filesystem` follow from `contributes.service`,
|
|
276
|
-
* and `contributes.
|
|
275
|
+
* declared under `contributes.capabilities`; `service`, `network`,
|
|
276
|
+
* `filesystem` and `origins` follow from `contributes.service`,
|
|
277
|
+
* `contributes.integration`, `contributes.filesystem` and `contributes.origins`. `model` is one-off text generation with the
|
|
277
278
|
* user's Small Model (`host.generate`), outside any session.
|
|
278
279
|
*/
|
|
279
|
-
export declare const GUEST_CAPABILITIES: readonly ["prompt", "sessions", "files", "model", "conversation", "service", "network", "filesystem"];
|
|
280
|
+
export declare const GUEST_CAPABILITIES: readonly ["prompt", "sessions", "files", "model", "conversation", "service", "network", "filesystem", "origins"];
|
|
280
281
|
export type GuestCapability = (typeof GUEST_CAPABILITIES)[number];
|
|
281
282
|
export declare const DECLARED_GUEST_CAPABILITIES: readonly ["prompt", "sessions", "files", "model"];
|
|
282
283
|
/** The capabilities a manifest may ask for directly. */
|
|
283
284
|
export type DeclaredGuestCapability = (typeof DECLARED_GUEST_CAPABILITIES)[number];
|
|
285
|
+
/** How many `contributes.origins` a package may declare. */
|
|
286
|
+
export declare const GUEST_ORIGINS_MAX = 8;
|
|
284
287
|
/** How many `contributes.filesystem` patterns a package may declare. */
|
|
285
288
|
export declare const GUEST_FILESYSTEM_PATTERNS_MAX = 16;
|
|
286
289
|
/** Characters in one `contributes.filesystem` pattern. */
|
|
@@ -303,12 +306,19 @@ export type OpenChamberContributes = {
|
|
|
303
306
|
service?: ServiceContribution;
|
|
304
307
|
/** Paths outside the project the panel may read and write. Grants `filesystem`. */
|
|
305
308
|
filesystem?: string[];
|
|
309
|
+
/**
|
|
310
|
+
* https origins the frame may exchange data with directly: fetch, images,
|
|
311
|
+
* fonts, styles and media, never scripts. Grants `origins`, approved per list.
|
|
312
|
+
*/
|
|
313
|
+
origins?: string[];
|
|
306
314
|
/** Menu entries on messages and sessions. */
|
|
307
315
|
actions?: GuestActionContribution[];
|
|
308
316
|
/** Composer slash commands that attach a chip. */
|
|
309
317
|
commands?: GuestCommandContribution[];
|
|
310
318
|
/** How the extension's tool calls look in the chat. */
|
|
311
319
|
tools?: GuestToolContribution[];
|
|
320
|
+
/** Editors the Files view opens matching files in. */
|
|
321
|
+
fileEditors?: FileEditorContribution[];
|
|
312
322
|
};
|
|
313
323
|
/** Whether any declared action asks for a session's messages, which needs `conversation`. */
|
|
314
324
|
export declare const guestActionsNeedConversation: (actions: readonly GuestActionContribution[] | undefined) => boolean;
|
|
@@ -317,7 +327,7 @@ export type PublicGuestCapabilities = {
|
|
|
317
327
|
requested: GuestCapability[];
|
|
318
328
|
granted: GuestCapability[];
|
|
319
329
|
};
|
|
320
|
-
export declare const requestedGuestCapabilities: (contributes: Pick<OpenChamberContributes, "capabilities" | "integration" | "service" | "filesystem" | "actions">) => GuestCapability[];
|
|
330
|
+
export declare const requestedGuestCapabilities: (contributes: Pick<OpenChamberContributes, "capabilities" | "integration" | "service" | "filesystem" | "actions" | "origins">) => GuestCapability[];
|
|
321
331
|
/**
|
|
322
332
|
* Whether the package ships a visible panel. Background-only extensions can
|
|
323
333
|
* execute code but have no rail surface, attach picker, or full-screen page.
|
|
@@ -348,7 +358,7 @@ export type OpenChamberManifest = {
|
|
|
348
358
|
engines?: OpenChamberEngines;
|
|
349
359
|
contributes: OpenChamberContributes;
|
|
350
360
|
};
|
|
351
|
-
export type ParseManifestErrorCode = 'not-object' | 'missing-openchamber' | 'unsupported-api-version' | 'invalid-engines' | 'invalid-version' | 'missing-panel' | 'invalid-panel' | 'invalid-panel-id' | 'invalid-panel-name' | 'invalid-panel-icon' | 'invalid-panel-entry' | 'invalid-background' | 'invalid-attach' | 'invalid-page' | 'invalid-status-section' | 'invalid-capabilities' | 'invalid-integration' | 'invalid-service' | 'invalid-filesystem' | 'invalid-actions' | 'invalid-commands' | 'invalid-tools';
|
|
361
|
+
export type ParseManifestErrorCode = 'not-object' | 'missing-openchamber' | 'unsupported-api-version' | 'invalid-engines' | 'invalid-version' | 'missing-panel' | 'invalid-panel' | 'invalid-panel-id' | 'invalid-panel-name' | 'invalid-panel-icon' | 'invalid-panel-entry' | 'invalid-background' | 'invalid-attach' | 'invalid-page' | 'invalid-status-section' | 'invalid-capabilities' | 'invalid-integration' | 'invalid-service' | 'invalid-filesystem' | 'invalid-origins' | 'invalid-actions' | 'invalid-commands' | 'invalid-tools' | 'invalid-file-editors';
|
|
352
362
|
export type ParseManifestFailure = {
|
|
353
363
|
ok: false;
|
|
354
364
|
code: ParseManifestErrorCode;
|
package/dist/manifest.js
CHANGED
|
@@ -97,13 +97,15 @@ export const serviceProvides = (service, role) => Boolean(service?.provides?.inc
|
|
|
97
97
|
* What a guest may do beyond drawing its own panel. The user approves the
|
|
98
98
|
* full list once, when the package is installed; a later package that asks
|
|
99
99
|
* for more is re-approved. `prompt`, `sessions`, `files`, and `model` are
|
|
100
|
-
* declared under `contributes.capabilities`; `service`, `network`,
|
|
101
|
-
* `filesystem` follow from `contributes.service`,
|
|
102
|
-
* and `contributes.
|
|
100
|
+
* declared under `contributes.capabilities`; `service`, `network`,
|
|
101
|
+
* `filesystem` and `origins` follow from `contributes.service`,
|
|
102
|
+
* `contributes.integration`, `contributes.filesystem` and `contributes.origins`. `model` is one-off text generation with the
|
|
103
103
|
* user's Small Model (`host.generate`), outside any session.
|
|
104
104
|
*/
|
|
105
|
-
export const GUEST_CAPABILITIES = ['prompt', 'sessions', 'files', 'model', 'conversation', 'service', 'network', 'filesystem'];
|
|
105
|
+
export const GUEST_CAPABILITIES = ['prompt', 'sessions', 'files', 'model', 'conversation', 'service', 'network', 'filesystem', 'origins'];
|
|
106
106
|
export const DECLARED_GUEST_CAPABILITIES = ['prompt', 'sessions', 'files', 'model'];
|
|
107
|
+
/** How many `contributes.origins` a package may declare. */
|
|
108
|
+
export const GUEST_ORIGINS_MAX = 8;
|
|
107
109
|
/** How many `contributes.filesystem` patterns a package may declare. */
|
|
108
110
|
export const GUEST_FILESYSTEM_PATTERNS_MAX = 16;
|
|
109
111
|
/** Characters in one `contributes.filesystem` pattern. */
|
|
@@ -138,6 +140,8 @@ export const requestedGuestCapabilities = (contributes) => {
|
|
|
138
140
|
declared.add('network');
|
|
139
141
|
if (contributes.filesystem && contributes.filesystem.length > 0)
|
|
140
142
|
declared.add('filesystem');
|
|
143
|
+
if (contributes.origins && contributes.origins.length > 0)
|
|
144
|
+
declared.add('origins');
|
|
141
145
|
return GUEST_CAPABILITIES.filter((capability) => declared.has(capability));
|
|
142
146
|
};
|
|
143
147
|
/**
|
package/dist/parse.d.ts
CHANGED
|
@@ -115,6 +115,7 @@ export declare const openChamberManifestSchema: z.ZodObject<{
|
|
|
115
115
|
surface: z.ZodOptional<z.ZodLiteral<true>>;
|
|
116
116
|
}, z.core.$strip>>;
|
|
117
117
|
filesystem: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
118
|
+
origins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
118
119
|
actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
119
120
|
id: z.ZodString;
|
|
120
121
|
label: z.ZodString;
|
|
@@ -146,8 +147,8 @@ export declare const openChamberManifestSchema: z.ZodObject<{
|
|
|
146
147
|
title: z.ZodOptional<z.ZodString>;
|
|
147
148
|
subtitle: z.ZodOptional<z.ZodString>;
|
|
148
149
|
output: z.ZodOptional<z.ZodEnum<{
|
|
149
|
-
code: "code";
|
|
150
150
|
text: "text";
|
|
151
|
+
code: "code";
|
|
151
152
|
auto: "auto";
|
|
152
153
|
json: "json";
|
|
153
154
|
markdown: "markdown";
|
|
@@ -156,6 +157,16 @@ export declare const openChamberManifestSchema: z.ZodObject<{
|
|
|
156
157
|
language: z.ZodOptional<z.ZodString>;
|
|
157
158
|
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
158
159
|
}, z.core.$strip>>>;
|
|
160
|
+
fileEditors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
161
|
+
id: z.ZodString;
|
|
162
|
+
title: z.ZodString;
|
|
163
|
+
match: z.ZodArray<z.ZodString>;
|
|
164
|
+
entry: z.ZodString;
|
|
165
|
+
content: z.ZodOptional<z.ZodEnum<{
|
|
166
|
+
text: "text";
|
|
167
|
+
binary: "binary";
|
|
168
|
+
}>>;
|
|
169
|
+
}, z.core.$strip>>>;
|
|
159
170
|
}, z.core.$strip>;
|
|
160
171
|
}, z.core.$strip>;
|
|
161
172
|
export declare const PACKAGE_VERSION_PATTERN: RegExp;
|
|
@@ -276,6 +287,7 @@ export declare const packageManifestSchema: z.ZodObject<{
|
|
|
276
287
|
surface: z.ZodOptional<z.ZodLiteral<true>>;
|
|
277
288
|
}, z.core.$strip>>;
|
|
278
289
|
filesystem: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
290
|
+
origins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
279
291
|
actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
280
292
|
id: z.ZodString;
|
|
281
293
|
label: z.ZodString;
|
|
@@ -307,8 +319,8 @@ export declare const packageManifestSchema: z.ZodObject<{
|
|
|
307
319
|
title: z.ZodOptional<z.ZodString>;
|
|
308
320
|
subtitle: z.ZodOptional<z.ZodString>;
|
|
309
321
|
output: z.ZodOptional<z.ZodEnum<{
|
|
310
|
-
code: "code";
|
|
311
322
|
text: "text";
|
|
323
|
+
code: "code";
|
|
312
324
|
auto: "auto";
|
|
313
325
|
json: "json";
|
|
314
326
|
markdown: "markdown";
|
|
@@ -317,6 +329,16 @@ export declare const packageManifestSchema: z.ZodObject<{
|
|
|
317
329
|
language: z.ZodOptional<z.ZodString>;
|
|
318
330
|
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
319
331
|
}, z.core.$strip>>>;
|
|
332
|
+
fileEditors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
333
|
+
id: z.ZodString;
|
|
334
|
+
title: z.ZodString;
|
|
335
|
+
match: z.ZodArray<z.ZodString>;
|
|
336
|
+
entry: z.ZodString;
|
|
337
|
+
content: z.ZodOptional<z.ZodEnum<{
|
|
338
|
+
text: "text";
|
|
339
|
+
binary: "binary";
|
|
340
|
+
}>>;
|
|
341
|
+
}, z.core.$strip>>>;
|
|
320
342
|
}, z.core.$strip>;
|
|
321
343
|
}, z.core.$strip>;
|
|
322
344
|
}, z.core.$strip>;
|
package/dist/parse.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { OPENCHAMBER_SDK_MANIFEST_API_VERSIONS } from "./api-version.js";
|
|
3
3
|
import { OPENCHAMBER_ENGINE_PATTERN } from "./host-version.js";
|
|
4
|
-
import { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_SERVICE_PROVIDES, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, PANEL_ID, hasGuestPage, isGuestFilesystemPattern, isGuestPackageSvgIcon, isSafeAssetPath, resolveIntegrationAuth, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, } from "./manifest.js";
|
|
4
|
+
import { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, PANEL_ID, hasGuestPage, isGuestFilesystemPattern, isGuestPackageSvgIcon, isSafeAssetPath, resolveIntegrationAuth, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, } from "./manifest.js";
|
|
5
|
+
import { GUEST_FILE_EDITORS_MAX, GUEST_FILE_EDITOR_PATTERNS_MAX, GUEST_FILE_EDITOR_TITLE_MAX, isFileEditorPattern, } from "./file-editor.js";
|
|
5
6
|
const isPanelIcon = (value) => (PANEL_ID.test(value) || isGuestPackageSvgIcon(value));
|
|
6
7
|
const isHttpsUrl = (value) => {
|
|
7
8
|
try {
|
|
@@ -12,6 +13,14 @@ const isHttpsUrl = (value) => {
|
|
|
12
13
|
return false;
|
|
13
14
|
}
|
|
14
15
|
};
|
|
16
|
+
// An origin the host writes into the frame's CSP as is: a literal hostname,
|
|
17
|
+
// so no `*` or other CSP syntax can widen what the user approved.
|
|
18
|
+
const APPROVABLE_HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
|
|
19
|
+
const isApprovableOrigin = (value) => {
|
|
20
|
+
if (!isHttpsOrigin(value))
|
|
21
|
+
return false;
|
|
22
|
+
return APPROVABLE_HOSTNAME.test(new URL(value).hostname);
|
|
23
|
+
};
|
|
15
24
|
const isHttpsOrigin = (value) => {
|
|
16
25
|
try {
|
|
17
26
|
const parsed = new URL(value);
|
|
@@ -183,6 +192,15 @@ const toolSchema = z.object({
|
|
|
183
192
|
}).refine((value) => value.output === 'code' || value.language === undefined, { path: ['language'] })
|
|
184
193
|
.refine((value) => (value.output === 'table') === (value.columns !== undefined), { path: ['columns'] });
|
|
185
194
|
const toolsSchema = z.array(toolSchema).min(1).max(GUEST_TOOLS_MAX);
|
|
195
|
+
const fileEditorSchema = z.object({
|
|
196
|
+
id: z.string().trim().regex(PANEL_ID),
|
|
197
|
+
title: z.string().trim().min(1).max(GUEST_FILE_EDITOR_TITLE_MAX),
|
|
198
|
+
match: z.array(z.string().trim().refine(isFileEditorPattern)).min(1).max(GUEST_FILE_EDITOR_PATTERNS_MAX),
|
|
199
|
+
entry: z.string().trim().refine((value) => isSafeAssetPath(value) && value.toLowerCase().endsWith('.html')),
|
|
200
|
+
content: z.enum(['text', 'binary']).optional(),
|
|
201
|
+
});
|
|
202
|
+
const fileEditorsSchema = z.array(fileEditorSchema).min(1).max(GUEST_FILE_EDITORS_MAX)
|
|
203
|
+
.refine((editors) => uniqueBy(editors, (editor) => editor.id), { message: 'file editor ids must be unique' });
|
|
186
204
|
const contributesSchema = z.object({
|
|
187
205
|
panel: panelSchema,
|
|
188
206
|
background: z.object({
|
|
@@ -202,9 +220,13 @@ const contributesSchema = z.object({
|
|
|
202
220
|
integration: integrationSchema.optional(),
|
|
203
221
|
service: serviceSchema.optional(),
|
|
204
222
|
filesystem: z.array(z.string().max(GUEST_FILESYSTEM_PATTERN_MAX).refine(isGuestFilesystemPattern)).min(1).max(GUEST_FILESYSTEM_PATTERNS_MAX).optional(),
|
|
223
|
+
origins: z.array(z.string().trim().refine(isApprovableOrigin)).min(1).max(GUEST_ORIGINS_MAX)
|
|
224
|
+
.refine((origins) => new Set(origins).size === origins.length, { message: 'origins must be unique' })
|
|
225
|
+
.optional(),
|
|
205
226
|
actions: actionsSchema.optional(),
|
|
206
227
|
commands: commandsSchema.optional(),
|
|
207
228
|
tools: toolsSchema.optional(),
|
|
229
|
+
fileEditors: fileEditorsSchema.optional(),
|
|
208
230
|
});
|
|
209
231
|
/**
|
|
210
232
|
* Contributions that need either the panel or background execution frame.
|
|
@@ -226,6 +248,8 @@ const runtimeContributions = (contributes) => {
|
|
|
226
248
|
declared.push('service');
|
|
227
249
|
if (contributes.filesystem !== undefined)
|
|
228
250
|
declared.push('filesystem');
|
|
251
|
+
if (contributes.origins !== undefined)
|
|
252
|
+
declared.push('origins');
|
|
229
253
|
if (contributes.actions !== undefined)
|
|
230
254
|
declared.push('actions');
|
|
231
255
|
if (contributes.commands !== undefined)
|
|
@@ -272,10 +296,10 @@ export const openChamberManifestSchema = z.object({
|
|
|
272
296
|
});
|
|
273
297
|
return;
|
|
274
298
|
}
|
|
275
|
-
if (contributes.statusSection) {
|
|
276
|
-
//
|
|
277
|
-
// but it only hosts itself: things the host opens or
|
|
278
|
-
// need a panel or background entry.
|
|
299
|
+
if (contributes.statusSection || contributes.fileEditors) {
|
|
300
|
+
// A status section or file editor frame runs code and may use what the
|
|
301
|
+
// package is granted, but it only hosts itself: things the host opens or
|
|
302
|
+
// invokes elsewhere need a panel or background entry.
|
|
279
303
|
const needsFrame = [];
|
|
280
304
|
if (contributes.page !== undefined)
|
|
281
305
|
needsFrame.push('page');
|
|
@@ -288,7 +312,7 @@ export const openChamberManifestSchema = z.object({
|
|
|
288
312
|
if (needsFrame.length > 0)
|
|
289
313
|
ctx.addIssue({
|
|
290
314
|
code: 'custom', path: ['panel'],
|
|
291
|
-
message: `${needsFrame.map((key) => `contributes.${key}`).join(', ')} needs panel.entry or background.entry; a status section only hosts itself.`,
|
|
315
|
+
message: `${needsFrame.map((key) => `contributes.${key}`).join(', ')} needs panel.entry or background.entry; a status section or file editor only hosts itself.`,
|
|
292
316
|
});
|
|
293
317
|
return;
|
|
294
318
|
}
|
|
@@ -383,6 +407,9 @@ const failureFromIssue = (issue) => {
|
|
|
383
407
|
if (path.startsWith('contributes.filesystem')) {
|
|
384
408
|
return fail('invalid-filesystem', 'contributes.filesystem lists 1 to 16 patterns starting with "/" or "~/", without "..", empty segments, or backslashes.');
|
|
385
409
|
}
|
|
410
|
+
if (path.startsWith('contributes.origins')) {
|
|
411
|
+
return fail('invalid-origins', `contributes.origins lists 1 to ${GUEST_ORIGINS_MAX} unique https origins like "https://fonts.example.com", without a path.`);
|
|
412
|
+
}
|
|
386
413
|
if (path.startsWith('contributes.actions')) {
|
|
387
414
|
return fail('invalid-actions', 'contributes.actions lists up to 8 entries with a unique kebab-case id, a label of 1 to 40 characters, where "message" or "session", optional roles (message only), and optional payload ["messages"] (session only).');
|
|
388
415
|
}
|
|
@@ -392,6 +419,9 @@ const failureFromIssue = (issue) => {
|
|
|
392
419
|
if (path.startsWith('contributes.tools')) {
|
|
393
420
|
return fail('invalid-tools', 'contributes.tools lists up to 16 entries with a match of 1 to 128 characters ([A-Za-z0-9_.:-], "*" only at the end), optional name (1 to 40), icon (Remixicon name or package .svg path), title and subtitle templates (1 to 200), output "auto" | "text" | "json" | "markdown" | "code" | "table", language (code only), and columns (table only, 1 to 16).');
|
|
394
421
|
}
|
|
422
|
+
if (path.startsWith('contributes.fileEditors')) {
|
|
423
|
+
return fail('invalid-file-editors', `contributes.fileEditors lists 1 to ${GUEST_FILE_EDITORS_MAX} editors with a unique kebab-case id, a title of 1 to ${GUEST_FILE_EDITOR_TITLE_MAX} characters, match: 1 to ${GUEST_FILE_EDITOR_PATTERNS_MAX} file-name patterns (no "/", not only wildcards, like "*.excalidraw"), an entry ending in .html inside the package, and optional content "text" or "binary".`);
|
|
424
|
+
}
|
|
395
425
|
if (path.startsWith('contributes.integration')) {
|
|
396
426
|
return fail('invalid-integration', 'contributes.integration needs a name, description, and oauth, token, or host.');
|
|
397
427
|
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -145,6 +145,7 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
145
145
|
page: "page";
|
|
146
146
|
background: "background";
|
|
147
147
|
status: "status";
|
|
148
|
+
file: "file";
|
|
148
149
|
}>;
|
|
149
150
|
connection: z.ZodObject<{
|
|
150
151
|
connected: z.ZodBoolean;
|
|
@@ -787,6 +788,41 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
787
788
|
}, z.core.$strip>]>;
|
|
788
789
|
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
789
790
|
v: z.ZodLiteral<1>;
|
|
791
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
792
|
+
type: z.ZodLiteral<"file-open">;
|
|
793
|
+
payload: z.ZodUnion<readonly [z.ZodObject<{
|
|
794
|
+
path: z.ZodString;
|
|
795
|
+
name: z.ZodString;
|
|
796
|
+
readOnly: z.ZodBoolean;
|
|
797
|
+
encoding: z.ZodLiteral<"text">;
|
|
798
|
+
content: z.ZodString;
|
|
799
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
800
|
+
path: z.ZodString;
|
|
801
|
+
name: z.ZodString;
|
|
802
|
+
readOnly: z.ZodBoolean;
|
|
803
|
+
encoding: z.ZodLiteral<"binary">;
|
|
804
|
+
bytes: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
|
|
805
|
+
}, z.core.$strip>]>;
|
|
806
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
807
|
+
v: z.ZodLiteral<1>;
|
|
808
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
809
|
+
type: z.ZodLiteral<"file-snapshot">;
|
|
810
|
+
id: z.ZodString;
|
|
811
|
+
payload: z.ZodObject<{
|
|
812
|
+
purpose: z.ZodEnum<{
|
|
813
|
+
save: "save";
|
|
814
|
+
handoff: "handoff";
|
|
815
|
+
}>;
|
|
816
|
+
}, z.core.$strip>;
|
|
817
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
818
|
+
v: z.ZodLiteral<1>;
|
|
819
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
820
|
+
type: z.ZodLiteral<"file-saved">;
|
|
821
|
+
payload: z.ZodObject<{
|
|
822
|
+
version: z.ZodString;
|
|
823
|
+
}, z.core.$strip>;
|
|
824
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
825
|
+
v: z.ZodLiteral<1>;
|
|
790
826
|
}, z.core.$strip>]>;
|
|
791
827
|
export declare const guestMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
792
828
|
type: z.ZodLiteral<"action-result">;
|
|
@@ -1157,6 +1193,38 @@ export declare const guestMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1157
1193
|
}, z.core.$strip>]>;
|
|
1158
1194
|
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
1159
1195
|
v: z.ZodLiteral<1>;
|
|
1196
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1197
|
+
type: z.ZodLiteral<"file-snapshot-result">;
|
|
1198
|
+
id: z.ZodString;
|
|
1199
|
+
payload: z.ZodUnion<readonly [z.ZodObject<{
|
|
1200
|
+
snapshot: z.ZodUnion<readonly [z.ZodObject<{
|
|
1201
|
+
content: z.ZodString;
|
|
1202
|
+
version: z.ZodString;
|
|
1203
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1204
|
+
bytes: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
|
|
1205
|
+
version: z.ZodString;
|
|
1206
|
+
}, z.core.$strip>]>;
|
|
1207
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1208
|
+
error: z.ZodString;
|
|
1209
|
+
}, z.core.$strip>]>;
|
|
1210
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
1211
|
+
v: z.ZodLiteral<1>;
|
|
1212
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1213
|
+
type: z.ZodLiteral<"file-change">;
|
|
1214
|
+
payload: z.ZodObject<{
|
|
1215
|
+
dirty: z.ZodBoolean;
|
|
1216
|
+
edited: z.ZodBoolean;
|
|
1217
|
+
}, z.core.$strip>;
|
|
1218
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
1219
|
+
v: z.ZodLiteral<1>;
|
|
1220
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1221
|
+
type: z.ZodLiteral<"file-save">;
|
|
1222
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
1223
|
+
v: z.ZodLiteral<1>;
|
|
1224
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1225
|
+
type: z.ZodLiteral<"file-unsupported">;
|
|
1226
|
+
channel: z.ZodLiteral<"openchamber.sdk">;
|
|
1227
|
+
v: z.ZodLiteral<1>;
|
|
1160
1228
|
}, z.core.$strip>], "type">;
|
|
1161
1229
|
type HostWire = z.input<typeof hostMessageSchema>;
|
|
1162
1230
|
type GuestWire = z.input<typeof guestMessageSchema>;
|
package/dist/protocol.js
CHANGED
|
@@ -2,6 +2,11 @@ import { z } from 'zod';
|
|
|
2
2
|
import { guestSessionWorktreeSchema, guestStorageRequestSchema, guestStorageResultSchema, guestWorkspaceQuerySchema, guestWorkspaceSnapshotSchema } from "./workspace-schemas.js";
|
|
3
3
|
import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from "./api-version.js";
|
|
4
4
|
import { SERVICE_STATUS_VALUES, ATTACH_PROVIDER_ID, GUEST_ACCOUNT_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_COMMIT_SHA, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_FILE_PATH_MAX, GUEST_FILE_STAT_KINDS, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_SETTING_VALUE_MAX, GUEST_TOAST_MAX, SESSION_LIFECYCLE_PHASES, SETTING_KEY, START_SESSION_SENT, isGuestFilePath, isGuestRequestPath, resolveHostRequestErrorCode, } from "./contract.js";
|
|
5
|
+
import { GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_VERSION_MAX } from "./file-editor.js";
|
|
6
|
+
// Structured clone recreates a frame's Uint8Array in the receiving realm, so
|
|
7
|
+
// `instanceof` holds for bytes that crossed postMessage.
|
|
8
|
+
const fileBytesSchema = z.instanceof(Uint8Array).refine((bytes) => bytes.byteLength <= GUEST_FILE_EDITOR_CONTENT_MAX);
|
|
9
|
+
const fileTextSchema = z.string().max(GUEST_FILE_EDITOR_CONTENT_MAX);
|
|
5
10
|
const envelope = {
|
|
6
11
|
channel: z.literal(OPENCHAMBER_SDK_CHANNEL),
|
|
7
12
|
v: z.literal(OPENCHAMBER_SDK_API_VERSION),
|
|
@@ -146,7 +151,7 @@ const readyPayloadSchema = z.object({
|
|
|
146
151
|
locale: z.string().min(1),
|
|
147
152
|
directory: z.string().nullable(),
|
|
148
153
|
session: sessionSnapshotSchema,
|
|
149
|
-
surface: z.enum(['panel', 'dialog', 'page', 'background', 'status']),
|
|
154
|
+
surface: z.enum(['panel', 'dialog', 'page', 'background', 'status', 'file']),
|
|
150
155
|
connection: guestConnectionSchema,
|
|
151
156
|
settings: guestSettingsSchema,
|
|
152
157
|
item: guestItemSchema,
|
|
@@ -259,6 +264,37 @@ export const hostMessageSchema = z.union([
|
|
|
259
264
|
id: z.string().min(1),
|
|
260
265
|
payload: z.union([messageItemSchema, sessionItemSchema]),
|
|
261
266
|
}),
|
|
267
|
+
z.object({
|
|
268
|
+
...envelope,
|
|
269
|
+
type: z.literal('file-open'),
|
|
270
|
+
payload: z.union([
|
|
271
|
+
z.object({
|
|
272
|
+
path: z.string().min(1).max(4096),
|
|
273
|
+
name: z.string().min(1).max(1024),
|
|
274
|
+
readOnly: z.boolean(),
|
|
275
|
+
encoding: z.literal('text'),
|
|
276
|
+
content: fileTextSchema,
|
|
277
|
+
}),
|
|
278
|
+
z.object({
|
|
279
|
+
path: z.string().min(1).max(4096),
|
|
280
|
+
name: z.string().min(1).max(1024),
|
|
281
|
+
readOnly: z.boolean(),
|
|
282
|
+
encoding: z.literal('binary'),
|
|
283
|
+
bytes: fileBytesSchema,
|
|
284
|
+
}),
|
|
285
|
+
]),
|
|
286
|
+
}),
|
|
287
|
+
z.object({
|
|
288
|
+
...envelope,
|
|
289
|
+
type: z.literal('file-snapshot'),
|
|
290
|
+
id: z.string().min(1),
|
|
291
|
+
payload: z.object({ purpose: z.enum(['save', 'handoff']) }),
|
|
292
|
+
}),
|
|
293
|
+
z.object({
|
|
294
|
+
...envelope,
|
|
295
|
+
type: z.literal('file-saved'),
|
|
296
|
+
payload: z.object({ version: z.string().max(GUEST_FILE_EDITOR_VERSION_MAX) }),
|
|
297
|
+
}),
|
|
262
298
|
]);
|
|
263
299
|
const filePathSchema = z.string().min(1).max(GUEST_FILE_PATH_MAX).refine(isGuestFilePath);
|
|
264
300
|
export const guestMessageSchema = z.discriminatedUnion('type', [
|
|
@@ -468,6 +504,27 @@ export const guestMessageSchema = z.discriminatedUnion('type', [
|
|
|
468
504
|
z.object({ error: z.string().trim().min(1).max(GUEST_RESOLVE_ERROR_MAX) }),
|
|
469
505
|
]),
|
|
470
506
|
}),
|
|
507
|
+
z.object({
|
|
508
|
+
...envelope,
|
|
509
|
+
type: z.literal('file-snapshot-result'),
|
|
510
|
+
id: z.string().min(1),
|
|
511
|
+
payload: z.union([
|
|
512
|
+
z.object({
|
|
513
|
+
snapshot: z.union([
|
|
514
|
+
z.object({ content: fileTextSchema, version: z.string().max(GUEST_FILE_EDITOR_VERSION_MAX) }),
|
|
515
|
+
z.object({ bytes: fileBytesSchema, version: z.string().max(GUEST_FILE_EDITOR_VERSION_MAX) }),
|
|
516
|
+
]),
|
|
517
|
+
}),
|
|
518
|
+
z.object({ error: z.string().trim().min(1).max(GUEST_RESOLVE_ERROR_MAX) }),
|
|
519
|
+
]),
|
|
520
|
+
}),
|
|
521
|
+
z.object({
|
|
522
|
+
...envelope,
|
|
523
|
+
type: z.literal('file-change'),
|
|
524
|
+
payload: z.object({ dirty: z.boolean(), edited: z.boolean() }),
|
|
525
|
+
}),
|
|
526
|
+
z.object({ ...envelope, type: z.literal('file-save') }),
|
|
527
|
+
z.object({ ...envelope, type: z.literal('file-unsupported') }),
|
|
471
528
|
]);
|
|
472
529
|
// The hand-written contract types and the schemas must describe the same wire
|
|
473
530
|
// shapes; either drifting from the other fails this file's type-check.
|