@aiworker/sdk 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +575 -0
- package/DOCUMENTATION.md +120 -0
- package/GUEST_SERVICES.md +166 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/api-version.d.ts +6 -0
- package/dist/api-version.js +5 -0
- package/dist/contract.d.ts +504 -0
- package/dist/contract.js +256 -0
- package/dist/host-version.d.ts +16 -0
- package/dist/host-version.js +50 -0
- package/dist/host.d.ts +107 -0
- package/dist/host.js +639 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/manifest.d.ts +303 -0
- package/dist/manifest.js +222 -0
- package/dist/parse.d.ts +293 -0
- package/dist/parse.js +398 -0
- package/dist/protocol.d.ts +1148 -0
- package/dist/protocol.js +473 -0
- package/dist/schemas.d.ts +4 -0
- package/dist/schemas.js +6 -0
- package/dist/scrollbar-style.d.ts +2 -0
- package/dist/scrollbar-style.js +32 -0
- package/dist/ui/badge.d.ts +10 -0
- package/dist/ui/badge.js +26 -0
- package/dist/ui/banner.d.ts +13 -0
- package/dist/ui/banner.js +48 -0
- package/dist/ui/button.d.ts +14 -0
- package/dist/ui/button.js +49 -0
- package/dist/ui/checkbox.d.ts +12 -0
- package/dist/ui/checkbox.js +45 -0
- package/dist/ui/dom.d.ts +15 -0
- package/dist/ui/dom.js +60 -0
- package/dist/ui/empty.d.ts +11 -0
- package/dist/ui/empty.js +44 -0
- package/dist/ui/field.d.ts +18 -0
- package/dist/ui/field.js +49 -0
- package/dist/ui/icons.d.ts +10 -0
- package/dist/ui/icons.js +23 -0
- package/dist/ui/index.d.ts +35 -0
- package/dist/ui/index.js +17 -0
- package/dist/ui/list.d.ts +26 -0
- package/dist/ui/list.js +90 -0
- package/dist/ui/menu.d.ts +20 -0
- package/dist/ui/menu.js +115 -0
- package/dist/ui/navigation.d.ts +18 -0
- package/dist/ui/navigation.js +38 -0
- package/dist/ui/option.d.ts +16 -0
- package/dist/ui/option.js +35 -0
- package/dist/ui/popup.d.ts +5 -0
- package/dist/ui/popup.js +43 -0
- package/dist/ui/progress.d.ts +11 -0
- package/dist/ui/progress.js +44 -0
- package/dist/ui/search.d.ts +11 -0
- package/dist/ui/search.js +62 -0
- package/dist/ui/select.d.ts +22 -0
- package/dist/ui/select.js +168 -0
- package/dist/ui/separator.d.ts +6 -0
- package/dist/ui/separator.js +26 -0
- package/dist/ui/spinner.d.ts +8 -0
- package/dist/ui/spinner.js +29 -0
- package/dist/ui/style.d.ts +1 -0
- package/dist/ui/style.js +202 -0
- package/dist/ui/tabs.d.ts +15 -0
- package/dist/ui/tabs.js +61 -0
- package/dist/ui/text.d.ts +23 -0
- package/dist/ui/text.js +89 -0
- package/dist/ui/theme.d.ts +22 -0
- package/dist/ui/theme.js +79 -0
- package/dist/workspace-schemas.d.ts +136 -0
- package/dist/workspace-schemas.js +44 -0
- package/dist/workspace.d.ts +109 -0
- package/dist/workspace.js +4 -0
- package/package.json +55 -0
- package/scripts/bundle-guest.ts +44 -0
package/API.md
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
# @aiworker/sdk — developer API reference
|
|
2
|
+
|
|
3
|
+
What third-party guest authors import and call. Source of truth: `[packages/sdk/src](https://roweb.cn/roweb/aihander/tree/main/packages/sdk/src)`. Longer guides live in the [product docs](https://roweb.cn/roweb/aihander/tree/main/packages/docs/content/docs) (`sdk.mdx`, `sdk/host.mdx`, `sdk/ui.mdx`) and in `[GUEST_SERVICES.md](https://roweb.cn/roweb/aihander/blob/main/packages/sdk/GUEST_SERVICES.md)` for local services.
|
|
4
|
+
|
|
5
|
+
**Package:** `@aiworker/sdk`
|
|
6
|
+
**API version:** manifest `apiVersion: 1`, wire envelope `v: 1`
|
|
7
|
+
**Runtimes that load guests:** web and desktop. VS Code and mobile mark the catalog `unsupported`.
|
|
8
|
+
|
|
9
|
+
Two entrypoints:
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
| Import | Role |
|
|
13
|
+
| --------------------- | -------------------------------------------------------------- |
|
|
14
|
+
| `@aiworker/sdk` | Manifest parse, iframe protocol, `connectHost` |
|
|
15
|
+
| `@aiworker/sdk/ui` | Optional DOM drawing kit (buttons, fields, lists, popups) |
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Ship checklist (install fails without these)
|
|
21
|
+
|
|
22
|
+
`inspectGuestPackage` (Settings → Extensions install) checks the folder or zip **on disk**. Parse alone is not enough. No AiWorker runtime compiles TypeScript: packaged desktop, `aiworker serve`, and the dev server all serve the built `.js` files as they sit in the package.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
| Must exist | When | Failure code |
|
|
26
|
+
| -------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------ |
|
|
27
|
+
| Semver `version` on `package.json` (`1.0.0`) | Always on install | `invalid-manifest` |
|
|
28
|
+
| `panel.entry` or `background.entry` HTML file | Every declared entry | `invalid-manifest` |
|
|
29
|
+
| Every relative `<script src="…">` `.js` from that HTML | Every declared entry | `missing-build` |
|
|
30
|
+
| File named by `panel.icon` | Only when icon ends in `.svg` (e.g. `icon.svg`) | `invalid-manifest` |
|
|
31
|
+
| File named by `service.entry` (e.g. `service/main.js`) | When `contributes.service` is set | `missing-build` |
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
**Icon.** Remixicon kebab name (`window`) needs no file. A package SVG path (`icon.svg`) must sit inside the package. URLs and absolute paths fail parse as `invalid-panel-icon`. Missing SVG on disk fails install as `invalid-manifest`.
|
|
35
|
+
|
|
36
|
+
**Panel JS.** Classic IIFE. The iframe cannot load ESM. Point `panel/index.html` at `./main.js` and ship that file.
|
|
37
|
+
|
|
38
|
+
**Service JS.** Same rule as the panel: `service.entry` must be compiled JS already in the package. `.ts` alone fails as `missing-build`.
|
|
39
|
+
|
|
40
|
+
Bundle with the SDK helper from the guest folder (`--node` targets Node for the service):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bunx openchamber-guest-bundle panel/main.ts panel/main.js
|
|
44
|
+
bunx openchamber-guest-bundle --node service/main.ts service/main.js
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Zip or folder for install should include at least: `package.json`, `panel/index.html`, `panel/main.js`, and any declared `icon.svg` / `service/main.js`. Skip `node_modules` and TypeScript sources. Zip and git installs land in `{dataDir}/extensions/{id}`. See also `[GUEST_SERVICES.md](https://roweb.cn/roweb/aihander/blob/main/packages/sdk/GUEST_SERVICES.md)`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 1. `connectHost` — iframe client
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { connectHost, HostRequestError } from '@aiworker/sdk';
|
|
55
|
+
|
|
56
|
+
const host = connectHost();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Throws `HOST_UNAVAILABLE` when there is no `window`. Outside an iframe (`parent === self`) it still returns a client, but every call rejects with `HOST_UNAVAILABLE`. Call `dispose()` on teardown; in-flight RPCs then reject as `HOST_UNAVAILABLE`. Silent host for 20s → `HOST_TIMEOUT`.
|
|
60
|
+
|
|
61
|
+
### 1.1 Subscriptions (host pushes)
|
|
62
|
+
|
|
63
|
+
Each returns an unsubscribe function. Late subscribers get the last known value (replay from `ready` or the last dedicated push).
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
| Method | Payload | Notes |
|
|
67
|
+
| ------------------------------ | ----------------------- | ------------------------------------------------------------ |
|
|
68
|
+
| `onReady(listener)` | `HostReadyContext` | First snapshot and later full refreshes |
|
|
69
|
+
| `onDirectory(listener)` | `string | null` |
|
|
70
|
+
| `onSession(listener)` | `SessionSnapshot | null` |
|
|
71
|
+
| `onSessionLifecycle(listener)` | `SessionLifecycleEvent` | `{ sessionId, phase }` — `started` / `completed` / `failure` |
|
|
72
|
+
| `onConnection(listener)` | `GuestConnection` | `{ connected, account }` |
|
|
73
|
+
| `onSettings(listener)` | `GuestSettings` | Declared integration fields only (`Record<string, string>`) |
|
|
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
|
+
| `onResolve(handler)` | `{ command, args }` → `Promise<AttachIssueRequest \| null>` | Answers a `contributes.commands` slash command. Return the chip to attach, `null` for nothing (the user sees a short notice), or throw (the message reaches the user). One handler at a time |
|
|
76
|
+
| `onAction(handler)` | `GuestActionItem` → `void \| Promise<void>` | Runs a `mode: "background"` message or session action. Register synchronously after `connectHost`. Await every operation; the frame is removed when the handler settles. Throw to report an error. One handler at a time; returns an unsubscribe function |
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
`HostReadyContext`
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
| Field | Type | Meaning |
|
|
83
|
+
| -------------- | ------------------------ | ---------------------------------------------------------------------------------------- |
|
|
84
|
+
| `theme.mode` | `'light' | 'dark'` |
|
|
85
|
+
| `theme.tokens` | surfaces, text, interaction states, `primary`, status colors, `font`, `mono`, `radius` | Pass to `applyHostReady` before mounting UI |
|
|
86
|
+
| `locale` | `string` | Host language tag |
|
|
87
|
+
| `directory` | `string | null` |
|
|
88
|
+
| `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 |
|
|
90
|
+
| `connection` | `{ connected, account }` | Integration link state |
|
|
91
|
+
| `settings` | `Record<string, string>` | Declared keys only |
|
|
92
|
+
| `item` | `GuestItem | null` | Set when the user clicked this guest's chip on the composer, or ran one of this guest's `contributes.actions`. Narrow with `isGuestMessageItem` / `isGuestSessionItem` / `isGuestAttachItem` |
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
`theme.tokens` includes `primaryText`, `successText`, `warningText`, `errorText`, and `infoText`. The host computes these for text on neutral surfaces and the UI kit's tinted controls. Keep using the base colors for fills and `primaryForeground` for text on a solid primary fill.
|
|
96
|
+
|
|
97
|
+
`applyHostReady` exposes the computed colors as `--primary-text`, `--success-text`, `--warning-text`, `--error-text`, and `--info-text`, with matching `--oc-*-text` aliases. These are required theme fields. Apply each `onReady` snapshot to update them when the theme changes.
|
|
98
|
+
|
|
99
|
+
`GuestItem` is `AttachIssueRequest | GuestMessageItem | GuestSessionItem`:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
type GuestMessageItem = {
|
|
103
|
+
kind: 'message';
|
|
104
|
+
action: string; // the action id from the manifest
|
|
105
|
+
sessionId: string;
|
|
106
|
+
sessionTitle: string;
|
|
107
|
+
directory: string | null; // the session's project directory
|
|
108
|
+
messageId: string;
|
|
109
|
+
role: 'user' | 'assistant';
|
|
110
|
+
text: string; // what the Markdown export renders for that message, at most 200 000 chars
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type GuestSessionItem = {
|
|
114
|
+
kind: 'session';
|
|
115
|
+
action: string;
|
|
116
|
+
sessionId: string;
|
|
117
|
+
sessionTitle: string;
|
|
118
|
+
directory: string | null; // the session's project directory
|
|
119
|
+
messages?: Array<{ id: string; role: 'user' | 'assistant'; text: string; createdAt: number }>; // oldest first; only with payload ["messages"] and the conversation grant
|
|
120
|
+
truncated?: boolean; // the oldest messages were dropped so the item stays under 2 000 000 serialized chars
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
Access tokens never appear in `ready` or in request results.
|
|
126
|
+
|
|
127
|
+
**Session lifecycle phases:** live `busy` / `retry` → `started`; `idle` → `completed`; unknown status → `failure` (not abort/crash).
|
|
128
|
+
|
|
129
|
+
### 1.2 Actions (RPC)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
| Method | Arguments | Returns | Behavior |
|
|
133
|
+
| ----------------- | --------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
134
|
+
| `toast` | `{ kind: 'info' \| 'success' \| 'error', message, copy?, dismiss?, persistent? }` | `Promise<void>` | Show a toast, optionally with host-owned Copy and OK buttons; see Toast buttons below |
|
|
135
|
+
| `openUrl` | `url: string` | `Promise<void>` | Open URL in the host |
|
|
136
|
+
| `openSurface` | `surfaceId: string` | `Promise<void>` | Switch host chrome to that surface |
|
|
137
|
+
| `writeClipboard` | `text: string` | `Promise<void>` | Copy in the host (1–32000 chars) |
|
|
138
|
+
| `compose` | `{ text, mode?: 'append' | 'replace' }` | `Promise<void>` |
|
|
139
|
+
| `attach` | `AttachIssueRequest` | `Promise<void>` | Composer chip (exclusive with GitHub/Linear) |
|
|
140
|
+
| `startSession` | `StartSessionRequest` | `Promise<{ sessionId, sent }>` | Create session (+ optional worktree), write snapshot. `text` can become first message |
|
|
141
|
+
| `prompt` | `{ text, send?: boolean }` | `Promise<{ sent }>` | Current session: omit/`false` = replace-compose; `send: true` = send |
|
|
142
|
+
| `sessionLink` | `AttachIssueRequest` | `Promise<void>` | Write snapshot on **current** session. Does not create one |
|
|
143
|
+
| `close` | — | `Promise<void>` | Dismiss attach dialog. No-op on the rail |
|
|
144
|
+
| `oauthStart` | — | `Promise<void>` | Open provider authorize URL (or first-party Linear) |
|
|
145
|
+
| `oauthDisconnect` | — | `Promise<void>` | Drop guest tokens / Linear connection |
|
|
146
|
+
| `request` | `{ method, path, query?, body? }` | `Promise<{ status, body }>` | HTTPS call on declared `apiOrigin`. Host attaches auth |
|
|
147
|
+
| `serviceRequest` | same shape as `request` | `Promise<{ status, body }>` | Proxy to this guest's local service on loopback |
|
|
148
|
+
| `serviceStatus` | — | `Promise<{ status }>` | `stopped` |
|
|
149
|
+
| `readFile` | `path: string` | `Promise<{ content }>` | UTF-8 text. Relative = inside the open project (`files`); `/…` or `~/…` = declared `filesystem` pattern |
|
|
150
|
+
| `writeFile` | `path: string, content: string` | `Promise<{ written: true }>` | Atomic (temp + rename), creates parent folders. Same path rules |
|
|
151
|
+
| `listDir` | `path: string` | `Promise<{ entries }>` | `{ name, kind: 'file' \| 'directory' \| 'other' }[]`, sorted, capped at 2 000. Same path rules |
|
|
152
|
+
| `stat` | `path: string` | `Promise<{ kind, size, mtime }>` | `kind` adds `'missing'`; a missing path is not an error. Same path rules |
|
|
153
|
+
| `setBadge` | `count: number \| null` | `Promise<void>` | Number on this guest's rail icon, 0–999 (clamped); `null` clears. Opening the panel clears it too. In memory only |
|
|
154
|
+
| `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 |
|
|
155
|
+
| `dispose` | — | `void` | Remove listener, reject pending RPCs |
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
`AttachIssueRequest`
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
{
|
|
162
|
+
providerId: string; // usually panel id
|
|
163
|
+
id: string; // guest identifier, not a GitHub number
|
|
164
|
+
title: string;
|
|
165
|
+
url: string;
|
|
166
|
+
text?: string; // optional model context
|
|
167
|
+
kind?: 'issue' | 'pull'; // default issue
|
|
168
|
+
author?: string;
|
|
169
|
+
branches?: { head: string; base: string }; // for pull
|
|
170
|
+
data?: JsonValue; // opaque, comes back as ready.item.data; not sent to the model
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`data` is plain JSON (`string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }`). It is stored with the chip and the session snapshot and returned unchanged when the user clicks the chip. `JSON.stringify(data).length` must stay within `GUEST_ATTACH_DATA_MAX` (16 000): `clampAttachRequest` silently drops a larger `data`, and the host schema refuses the whole message if it arrives over the limit.
|
|
175
|
+
|
|
176
|
+
`StartSessionRequest` adds `projectId?`, `navigation?: 'preserve' | 'open'`, and `worktree?` to the attach fields. Navigation defaults to `preserve`. Without `projectId`, the current directory is used. With one, creation targets that registered project without switching the app first.
|
|
177
|
+
|
|
178
|
+
`worktree` is `false` or omitted for the target directory, `true` for a generated new worktree, `{ kind: 'existing', directory }` for a known worktree belonging to the project, or `{ kind: 'new', name?, baseBranch? }`. `name` names both the branch and worktree. Omitted name/base use the host's normal defaults. First-message model/agent/variant selection is captured when the call starts.
|
|
179
|
+
|
|
180
|
+
A created session returns `{ sessionId, directory, sent, linked, worktree? }`. `linked: false` means the session exists but saving the attached item failed. If a worktree was created but bootstrap or session creation failed, the result is `{ sessionId: null, sent: 'skipped', directory, worktree, failure }`, where `failure` is `bootstrap-failed` or `session-create-failed`. The worktree is retained. Inspect the result before offering Retry, which would otherwise create another worktree. The call waits up to 180 seconds; a timeout does not prove the server rolled back.
|
|
181
|
+
|
|
182
|
+
### Workspace lists and subscriptions
|
|
183
|
+
|
|
184
|
+
These methods extend the existing `sessions` capability. They expose registered projects on the connected server, session metadata and live state, and known worktrees. They do not grant conversation content or file access.
|
|
185
|
+
|
|
186
|
+
| Method | Result |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `listProjects()` | `Promise<GuestProjectsSnapshot>` |
|
|
189
|
+
| `listWorktrees(projectId)` | `Promise<GuestWorktreesSnapshot>` |
|
|
190
|
+
| `listSessions(projectId)` | `Promise<GuestSessionsSnapshot>` including known archived sessions |
|
|
191
|
+
| `onProjects(listener)` | `Promise<() => void>` |
|
|
192
|
+
| `onWorktrees(projectId, listener)` | `Promise<() => void>` |
|
|
193
|
+
| `onSessions(projectId, listener)` | `Promise<() => void>` |
|
|
194
|
+
| `openSession(sessionId)` | `Promise<void>`, explicitly opens the chat and closes the page |
|
|
195
|
+
|
|
196
|
+
Await subscription registration to handle refusal, then retain its returned unsubscribe function. Each subscription sends an initial snapshot, then changes. At most 32 subscriptions per iframe. `dispose()` releases them all. Unmount, disable, uninstall and runtime switch also release host subscriptions.
|
|
197
|
+
|
|
198
|
+
Snapshots carry `state: 'loading' | 'ready' | 'error'`; session snapshots also carry per-directory `coverage`. Loading/error may retain data. Only `ready` establishes complete empty success. Reads use existing shared stores and hydration, never a git scan per extension request.
|
|
199
|
+
|
|
200
|
+
Projects contain `id`, `name`, `directory`. Worktrees contain `directory`, `name`, `branch`, and `status: 'ready' | 'pending' | 'invalid' | 'missing'`. Session records contain `id`, `title`, `projectId`, `directory`, `parentId`, `createdAt`, `updatedAt`, `archivedAt`, `worktree`, `activity`, `outcome`, and `items`. Item references contain only this extension's `id` and optional `data`.
|
|
201
|
+
|
|
202
|
+
`activity` is `unknown`, `idle`, `running`, `retrying`, `waiting-permission`, or `waiting-question`. `outcome` is the last observed `completed` or `failed` turn, or `null` when unknown or working. Outcomes are in memory for the latest 2,000 observed sessions, reset on runtime switch, and are not reconstructed from persisted history. A later idle event preserves an observed failure until another run starts. `completed` never means the extension's task is Done. Blocking-request contents and approve/reply actions are not exposed.
|
|
203
|
+
|
|
204
|
+
### Extension storage
|
|
205
|
+
|
|
206
|
+
`host.storage.get(key)` returns JSON or `undefined` for a missing key. JSON `null` is a stored value. `set(key, value)` and `delete(key)` return `Promise<void>`; `keys()` returns `Promise<string[]>` in sorted order.
|
|
207
|
+
|
|
208
|
+
Storage belongs to the extension on the connected server and needs no extra capability. Keys contain 1 to 128 characters, each serialized value is at most 64 KiB UTF-8, and the complete namespace is at most 2 MiB and 2,000 keys. Use a project ID in your key when data belongs to one project. Concurrent operations serialize on the server, writes are atomic, and read/write failures preserve existing data. Uninstall deletes the namespace, including for folder installs.
|
|
209
|
+
|
|
210
|
+
### Full-screen pages
|
|
211
|
+
|
|
212
|
+
`contributes.page: true` reuses `panel.entry`; `{ entry: 'panel/page.html', title?: 'Board' }` uses separate package HTML. It requires `panel.entry` and the same installed/approved/enabled state as the panel. The sidebar's Extension pages menu is the only page opener; `openSurface` does not open it. `ctx.surface` is `page`, `close()` closes it, and reload or runtime switch returns to chat. Pages use the existing sandbox and capabilities on web/desktop. VS Code and mobile remain unsupported.
|
|
213
|
+
|
|
214
|
+
`sent` **values** (`startSession` / `prompt`): `sent` | `no-model` | `skipped` | `failed`. After `no-model` / `failed` on `startSession`, the session still exists.
|
|
215
|
+
|
|
216
|
+
**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`.
|
|
217
|
+
|
|
218
|
+
`request` **/** `serviceRequest` **rules:** `method` is `GET` | `POST` | `PUT` | `PATCH` | `DELETE`. `path` must start with `/`, no scheme, stay on the declared origin (cloud API or service loopback). Guest parses `body` as JSON when needed.
|
|
219
|
+
|
|
220
|
+
### Toast buttons
|
|
221
|
+
|
|
222
|
+
`ToastRequest.copy` is an optional boolean or `{ text: string }`. `true` copies the displayed message; an object supplies the clipboard value. `false` or omission adds no Copy button. The custom text must contain 1 to `GUEST_CLIPBOARD_TEXT_MAX` characters, with whitespace preserved. The displayed message is trimmed and must contain 1 to `GUEST_TOAST_MAX` characters. The client rejects invalid text lengths as `HOST_REJECTED` before sending; the host independently validates the wire payload.
|
|
223
|
+
|
|
224
|
+
`dismiss: true` adds OK. `persistent: true` disables automatic expiry and always adds OK regardless of `dismiss`, so every persistent toast can be closed. Omitted or false `persistent` retains the host's usual duration.
|
|
225
|
+
|
|
226
|
+
Copy leaves the toast open, reports success on the button, and shows a retryable error when the clipboard fails. OK dismisses only its own toast. Buttons use the host's locale and clipboard helper. They retain only the supplied text and toast identity, work after guest disposal or a runtime switch, and never call back into the extension. The `toast` promise resolves after display acknowledgement, without waiting for a click. Web and desktop use the same behavior; extension support on other runtimes is unchanged.
|
|
227
|
+
|
|
228
|
+
### Background actions
|
|
229
|
+
|
|
230
|
+
`contributes.actions[].mode` accepts `"open"` or `"background"`. Omitted mode keeps the existing panel/dialog routing and `onItem` delivery. Background actions load `background.entry` when declared, otherwise `panel.entry`. They never use separate attach-dialog HTML. They run on web and desktop, including connections through the private relay; VS Code and mobile still do not load extensions.
|
|
231
|
+
|
|
232
|
+
`contributes.background` is `{ entry: "<package-local .html>" }`. It starts on demand for an action or command and adds no visible UI. With a background entry, `panel.entry` may be omitted: there is no rail icon, attach picker, or full-screen page, but background actions, slash commands, storage and granted APIs work. Identity remains in `panel.id/name/icon`. Open-mode actions, `page`, and enabled `attach` require `panel.entry`; invalid combinations fail as `invalid-panel`. Without either entry, the extension remains tools-only. Invalid background paths or a missing entry field fail as `invalid-background`.
|
|
233
|
+
|
|
234
|
+
When both entries exist, the visible panel cannot register the slash-command resolver: the hidden background entry handles `onResolve` and receives `surface: "background"`. Existing panel-only commands retain their current behavior. Clicking an attached chip from a background-only extension shows a no-panel notice; its browser action remains available.
|
|
235
|
+
|
|
236
|
+
Every background click mounts a fresh sandboxed iframe and sends one `action` request with an invocation `id` and a `GuestActionItem` payload. `host.onAction` receives that message or session item and sends `action-result` with `{ ok: true }` when the handler finishes, or `{ ok: false, error }` when it throws. The error is limited to `GUEST_RESOLVE_ERROR_MAX` characters. The host validates the reply and accepts only the matching invocation from that frame. It sends no acknowledgement for `action-result`.
|
|
237
|
+
|
|
238
|
+
`ready.surface` is `"background"` and `ready.item` is `null`. Context updates never replay the action. The session and directory context remain scoped to the clicked target; the item itself is the snapshot captured at click time. Calls that explicitly write the composer still write the currently visible composer. `close()` is a no-op in a background frame; returning from the handler completes the action.
|
|
239
|
+
|
|
240
|
+
The 20-second deadline covers loading and execution. At most eight background invocations may run concurrently. Completion, failure, timeout, disabling, lost approval, extension update/removal, runtime switch, and host unmount release the frame and its listeners. Late messages are ignored. Already completed or server-accepted effects are not undone, and the host does not automatically retry actions. A background action has the same declared capabilities as its extension. It does not hide the extension's rail icon.
|
|
241
|
+
|
|
242
|
+
See [Actions without opening a panel](./README.md#actions-without-opening-a-panel) for a toast example.
|
|
243
|
+
|
|
244
|
+
### 1.3 Error codes (`HostRequestError.code`)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
| Code | When |
|
|
248
|
+
| ------------------ | --------------------------------------------- |
|
|
249
|
+
| `HOST_UNAVAILABLE` | No window, not in iframe, or disposed |
|
|
250
|
+
| `HOST_TIMEOUT` | No answer for 20s |
|
|
251
|
+
| `HOST_REJECTED` | Host refusal, or unknown wire code |
|
|
252
|
+
| `DISCONNECTED` | No token / Linear connection |
|
|
253
|
+
| `DISABLED` | Extension paused in Settings |
|
|
254
|
+
| `BAD_PATH` | Path left origin or malformed |
|
|
255
|
+
| `NO_INTEGRATION` | Manifest has no `integration` |
|
|
256
|
+
| `NO_SESSION` | `prompt` / `sessionLink` with no open session |
|
|
257
|
+
| `SESSION_BUSY` | `prompt({ send: true })` while busy |
|
|
258
|
+
| `NO_SERVICE` | No service, not approved, or not running |
|
|
259
|
+
| `NOT_GRANTED` | The user has not approved this capability |
|
|
260
|
+
| `NO_DIRECTORY` | Relative file path with no open project |
|
|
261
|
+
| `NOT_FOUND` | `readFile` / `listDir` on a path that does not exist |
|
|
262
|
+
| `FILE_TOO_LARGE` | File or content over 2 000 000 characters |
|
|
263
|
+
| `DENIED` | The operating system refused the file access |
|
|
264
|
+
| `NO_MODEL` | `generate` with no usable Small Model |
|
|
265
|
+
| `MODEL_FAILED` | The Small Model returned an error |
|
|
266
|
+
| `SERVICE_FAILED` | Service crashed or never became ready |
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
### 1.4 Field limits (client clamps before send)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
| Field | Max |
|
|
273
|
+
| -------------------------------- | --------- |
|
|
274
|
+
| Clipboard text | 32 000 |
|
|
275
|
+
| Compose / prompt / attach `text` | 16 000 |
|
|
276
|
+
| Attach `data` (serialized) | 16 000 |
|
|
277
|
+
| Attach `id` | 128 |
|
|
278
|
+
| Attach `title` | 200 |
|
|
279
|
+
| Attach `url` | 2 000 |
|
|
280
|
+
| Attach `author` | 80 |
|
|
281
|
+
| Branch name | 200 |
|
|
282
|
+
| Request path | 2 000 |
|
|
283
|
+
| Request body | 64 000 |
|
|
284
|
+
| Request response | 256 000 |
|
|
285
|
+
| Request timeout | 20 000 ms |
|
|
286
|
+
| `resolve` answer (host waits) | 20 000 ms |
|
|
287
|
+
| Background action loading and execution | 20 000 ms |
|
|
288
|
+
| Badge count | 999 |
|
|
289
|
+
| Message item `text` | 200 000 |
|
|
290
|
+
| Session item (serialized) | 2 000 000 |
|
|
291
|
+
| File path | 1 024 |
|
|
292
|
+
| File content (read and write) | 2 000 000 |
|
|
293
|
+
| `listDir` entries | 2 000 |
|
|
294
|
+
| `generate` prompt / system | 64 000 / 8 000 |
|
|
295
|
+
| `generate` `maxOutputTokens` | 4 000 |
|
|
296
|
+
| `generate` answer | 256 000 |
|
|
297
|
+
| `generate` timeout | 90 000 ms |
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 2. `@aiworker/sdk/ui` — drawing kit
|
|
303
|
+
|
|
304
|
+
DOM building blocks that use host tokens. They do **not** call the provider or `connectHost`. You compose the screen and wire callbacks yourself.
|
|
305
|
+
|
|
306
|
+
Always call theme first:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { applyHostReady, mountList } from '@aiworker/sdk/ui';
|
|
310
|
+
|
|
311
|
+
let mounted = false;
|
|
312
|
+
host.onReady((ctx) => {
|
|
313
|
+
applyHostReady(ctx, document.documentElement);
|
|
314
|
+
if (mounted) return;
|
|
315
|
+
mounted = true;
|
|
316
|
+
// then mount…
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Every mount returns `{ update(partial), dispose() }`. `update` merges the fields you pass and repaints; `dispose` removes the node and its listeners.
|
|
321
|
+
|
|
322
|
+
Selection controls report a proposed value; the caller commits it with `update`. Use `activeId` for tabs, `value` for selects, and `checked` for checkboxes and switches. Text and search inputs display typing immediately, but call `update({ value })` too so later updates do not restore stale props. Button clicks do not change `variant`; use tabs for mode selection. Preserve input state across tab changes instead of remounting empty fields.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { mountTabs } from '@aiworker/sdk/ui';
|
|
326
|
+
|
|
327
|
+
let activeId = 'convert';
|
|
328
|
+
const tabs = mountTabs(root, {
|
|
329
|
+
items: [{ id: 'convert', label: 'Convert' }, { id: 'format', label: 'Format' }],
|
|
330
|
+
activeId,
|
|
331
|
+
onChange: (next) => {
|
|
332
|
+
activeId = next;
|
|
333
|
+
tabs.update({ activeId });
|
|
334
|
+
// Show the matching panel while preserving its draft values.
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### 2.1 Theme
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
| Function | Role |
|
|
343
|
+
| ----------------------------- | --------------------------------------------------------- |
|
|
344
|
+
| `applyHostReady(ctx, root)` | Writes theme tokens + `data-oc-surface` / `data-oc-theme` |
|
|
345
|
+
| `applyHostTheme(theme, root)` | Tokens only |
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
### 2.2 Mount functions
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
| Function | Use for | Main props |
|
|
352
|
+
| --------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
353
|
+
| `mountButton(root, props)` | Button | `label`, `onClick`, `variant?` (`default` / `secondary` / `outline` / `ghost` / `destructive`), `size?` (`default` / `sm` / `xs`), `disabled?`, `loading?` |
|
|
354
|
+
| `mountTextField(root, props)` | Input or textarea | `value`, `onChange`, `label?`, `placeholder?`, `password?`, `multiline?`, `rows?`, `disabled?`, `error?`, `helper?`, `mono?` |
|
|
355
|
+
| `mountSearchField(root, props)` | Search box | `value`, `onChange`, `placeholder?`, `label?`, `autofocus?` |
|
|
356
|
+
| `mountSelect(root, props)` | Dropdown | `value`, `options: { id, label, hint? }[]`, `onChange`, `label?`, `placeholder?`, `searchable?`, `searchPlaceholder?`, `disabled?` |
|
|
357
|
+
| `mountCheckbox` / `mountSwitch` | Checkbox / toggle | `label`, `checked`, `onChange`, `disabled?`, `description?` |
|
|
358
|
+
| `mountTabs(root, props)` | Pill tabs | `items: { id, label, count? }[]`, `activeId`, `onChange`, `trackBackground?` |
|
|
359
|
+
| `mountBadge(root, props)` | Pill | `label`, `tone?` (`neutral` / `primary` / `success` / `warning` / `error` / `info`) |
|
|
360
|
+
| `mountList(root, props)` | Keyboard list | `items: { id, title, subtitle?, leading?, meta?, badge?, disabled? }[]`, `onSelect`, `selectedId?`, `emptyText?`, `ariaLabel?` |
|
|
361
|
+
| `mountEmpty(root, props)` | Empty / disconnected state | `title`, `body?`, `action?: { label, onClick }` |
|
|
362
|
+
| `mountSpinner(root, props?)` | Loading ring | `size?` (`sm` / `default`), `label?` |
|
|
363
|
+
| `mountBanner(root, props)` | Notice | `tone` (`info` / `success` / `warning` / `error`), `title`, `body?`, `action?` |
|
|
364
|
+
| `mountSeparator(root, props?)` | Divider | `label?` |
|
|
365
|
+
| `mountProgress(root, props)` | Progress bar | `value` (0..100), `tone?`, `label?` |
|
|
366
|
+
| `mountMenu(root, props)` | Action dropdown | `label`, `items: ({ id, label, destructive?, disabled? } \| { separator: true })[]`, `onSelect`, `variant?`, `size?` |
|
|
367
|
+
| `mountText(root, props)` | Provider text | `text`, `onOpenUrl?` |
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
### 2.3 Helpers
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
| Function | Role |
|
|
374
|
+
| ------------------------------------------ | --------------------------------------------------------------------------- |
|
|
375
|
+
| `filterSelectOptions(options, query)` | Same case-insensitive label / id match `mountSelect` uses |
|
|
376
|
+
| `moveListSelection(items, currentId, key)` | Keyboard step (`next` / `previous` / `first` / `last`) that skips disabled items |
|
|
377
|
+
| `navigationKey(event, axis?)` | Maps arrows, Home, End, Ctrl+N / Ctrl+P to a step |
|
|
378
|
+
| `splitTextMedia(text)` | Splits text into runs, `` images, and `[label](https://…)` links |
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
`mountText` keeps everything as text except `http(s)` markdown images and links. A sandboxed iframe cannot open a link itself, so pass `onOpenUrl` and forward the URL to `host.openUrl`.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 3. Manifest and host-side parse (`@aiworker/sdk`)
|
|
386
|
+
|
|
387
|
+
Used by the AiWorker host and by tools that validate packages. Guests rarely call these from the iframe.
|
|
388
|
+
|
|
389
|
+
### 3.1 Manifest block (inside `package.json`)
|
|
390
|
+
|
|
391
|
+
```json
|
|
392
|
+
{
|
|
393
|
+
"name": "@acme/hello-panel",
|
|
394
|
+
"version": "1.0.0",
|
|
395
|
+
"aiworker": {
|
|
396
|
+
"apiVersion": 1,
|
|
397
|
+
"engines": { "aiworker": ">=1.22.0" },
|
|
398
|
+
"contributes": {
|
|
399
|
+
"panel": {
|
|
400
|
+
"id": "acme-hello",
|
|
401
|
+
"name": "Hello",
|
|
402
|
+
"icon": "window",
|
|
403
|
+
"entry": "panel/index.html"
|
|
404
|
+
},
|
|
405
|
+
"attach": "dialog",
|
|
406
|
+
"capabilities": ["prompt", "sessions", "files"],
|
|
407
|
+
"filesystem": ["~/.config/opencode/opencode.json", "/tmp/acme/**"],
|
|
408
|
+
"actions": [
|
|
409
|
+
{ "id": "create-task", "label": "Create task from message", "icon": "add-circle", "where": "message", "roles": ["assistant"] },
|
|
410
|
+
{ "id": "summarize", "label": "Summarize session", "where": "session", "payload": ["messages"] }
|
|
411
|
+
],
|
|
412
|
+
"commands": [{ "name": "task", "description": "Attach a task by id" }],
|
|
413
|
+
"tools": [{ "match": "mcp.tasks.*", "name": "Tasks", "icon": "checkbox-circle", "title": "{input.id}", "output": "table", "columns": ["id", "title", "status"] }],
|
|
414
|
+
"integration": { /* oauth | token | host */ },
|
|
415
|
+
"service": { /* optional local process */ }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
| Key | Rules |
|
|
423
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
424
|
+
| `version` | Semver required on install (`1.0.0`) |
|
|
425
|
+
| `apiVersion` | Must be `1` |
|
|
426
|
+
| `engines.aiworker` | Optional. Only `1.22.0` or `>=1.22.0`. Older host → `host-too-old` |
|
|
427
|
+
| `panel.id` | kebab-case |
|
|
428
|
+
| `panel.icon` | Remixicon kebab name (`window`) **or** package `.svg` path. Remixicon needs no file. An `.svg` path must exist on disk or install fails (`invalid-manifest`). No URLs/absolute paths |
|
|
429
|
+
| `panel.entry` | Optional visible-panel HTML path inside the package. No `..`, absolute path, or URL. Its scripts must be built. Omitting it removes the rail icon and visible views; `background.entry` can still run code. With neither entry, only `tools` plus package identity/version/engines are allowed. `hasGuestPage` means a visible panel, not background execution |
|
|
430
|
+
| `background.entry` | Optional package-local `.html` path. Loaded on demand for background actions and slash commands, preferred over `panel.entry` for these calls. Adds no rail icon. The file and its built scripts must exist. Malformed declarations are `invalid-background` |
|
|
431
|
+
| `attach` | `true` / `"panel"` → + menu opens rail; `"dialog"` → host window; omit/`false` → off menus. Object form `{ "mode": "panel" \| "dialog", "entry"?: "panel/attach.html" }`: `entry` (dialog only, same path rules as `panel.entry`, must exist with built scripts) is the page the dialog loads instead of `panel.entry` |
|
|
432
|
+
| `capabilities` | Optional list of `prompt`, `sessions`, `files`, `model`. `files` is read **and** write inside the open project; `model` is `generate`. Approved once at install |
|
|
433
|
+
| `actions` | Optional, 1–8 entries, unique kebab-case `id`, `label` 1–40 chars, optional `icon` with the `panel.icon` rules, `where: "message" \| "session"`, optional `mode: "open" \| "background"`. Message actions may narrow `roles` to `["user"]` / `["assistant"]`, default both. Session actions may request `payload: ["messages"]`, adding the `conversation` capability. Invalid shape is `invalid-actions`. Default `open` mode opens the guest with `ready.item`, using the attach dialog for `attach: "dialog"` and the rail otherwise. `background` calls `onAction` in a temporary hidden frame; see Background actions above |
|
|
434
|
+
| `commands` | Optional, 1–8 entries, unique `name` matching `/^[a-z][a-z0-9-]{0,23}$/`, optional `description` 1–80 chars (`invalid-commands`). `/name args` in the chat box calls `onResolve` instead of the model and attaches what it returns. A name the composer already has (built-in, OpenCode command, skill) is ignored with a console warning |
|
|
435
|
+
| `tools` | Optional, 1–16 entries that say how the extension's tool calls look in the chat. `match` is the full tool name OpenCode reports (`mcp.jira.search`, `jira_search`), 1–128 chars of `[A-Za-z0-9_.:-]`, with `*` allowed once at the end as a suffix wildcard (`mcp.jira.*`). Optional `name` (1–40, the header title when `title` is absent or renders empty), `icon` (Remixicon name or package `.svg` path, same rules as `panel.icon`; the SVG is drawn in the text colour at the glyph size), `title` / `subtitle` templates (1–200, `{input.path}` / `{output.path}` / `{metadata.path}` placeholders, a missing path renders empty, values are cut at 200), `output` `"auto"` (default) \| `"text"` \| `"json"` \| `"markdown"` \| `"code"` \| `"table"`, `language` (code only), `columns` (table only, 1–16 dotted paths; rows are the output array or `output.items`). Bad shape is `invalid-tools`. An exact `match` beats a wildcard from any extension; among equals the first extension wins. Only an enabled, fully approved extension's rules apply |
|
|
436
|
+
| `filesystem` | Optional, 1–16 globs, each 1–256 chars, starting with `/` or `~/`; `**` spans folders, `*` / `?` stay in one segment; no `..`, empty segment, or backslash (`invalid-filesystem`). Declaring it adds the `filesystem` capability and the dialog lists the globs |
|
|
437
|
+
| `integration` | Optional. Exactly one of `oauth`, `token`, or `host` (`provider: "linear"` only) |
|
|
438
|
+
| `service` | Optional. `entry` must be a built `.js` file on disk. See [GUEST_SERVICES.md](https://roweb.cn/roweb/aihander/blob/main/packages/sdk/GUEST_SERVICES.md) |
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
Extra keys are dropped, not forwarded.
|
|
442
|
+
|
|
443
|
+
### 3.2 Parse / version helpers
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
| Export | Role |
|
|
447
|
+
| -------------------------------------------------- | --------------------------------------------------------- |
|
|
448
|
+
| `parseManifest(document)` (`@aiworker/sdk/schemas`) | Typed document → success/failure (does not throw on junk) |
|
|
449
|
+
| `parseManifestJson(json)` (`@aiworker/sdk/schemas`) | String → same result |
|
|
450
|
+
| `resolveAttachMode(attach)` | Normalize to `'panel' |
|
|
451
|
+
| `resolveAttachEntry(contributes)` | Dialog page from the object form, or `null` when the dialog reuses `panel.entry` |
|
|
452
|
+
| `hasGuestPage(contributes)` | `true` when `panel.entry` is set; background-only and tools-only packages return `false` |
|
|
453
|
+
| `resolveIntegrationAuth` / `resolveIntegrationApi` | Auth kind and API origin |
|
|
454
|
+
| `toPublicIntegration` / `toPublicService` | Catalog-safe public slices |
|
|
455
|
+
| `isGuestPackageSvgIcon` | Whether icon is a package SVG path |
|
|
456
|
+
| `compareAiWorkerVersions` | Semver compare |
|
|
457
|
+
| `hostMeetsAiWorkerEngine` | Host vs `engines.aiworker` floor |
|
|
458
|
+
| `openChamberEngineMinimum` | Normalize `>=1.22.0` → floor string |
|
|
459
|
+
| `parseAiWorkerVersion` | Parse `x.y.z` |
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
### 3.3 Protocol helpers (host + guest tooling)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
| Export | Role |
|
|
466
|
+
| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
467
|
+
| `readHostMessage` (`@aiworker/sdk`) | Guest-side read of a host push; no schema library |
|
|
468
|
+
| `parseHostMessage` / `parseGuestMessage` (`@aiworker/sdk/schemas`) | Host-side typed parse |
|
|
469
|
+
| `hostMessageSchema` / `guestMessageSchema` (`@aiworker/sdk/schemas`) | Zod schemas for `postMessage` data |
|
|
470
|
+
| `clampAttachRequest` / `clampStartSessionRequest` / `clampPromptRequest` | Enforce field max lengths |
|
|
471
|
+
| `isGuestRequestPath` / `isGuestRequestResult` / `isStartSessionResult` / `isPromptResult` / `isServiceStatusResult` | Narrow result payloads |
|
|
472
|
+
| `isGuestAttachItem` / `isGuestMessageItem` / `isGuestSessionItem` | Narrow `ready.item` |
|
|
473
|
+
| `clampBadgeCount` / `guestActionsNeedConversation` | Badge range; whether declared actions need `conversation` |
|
|
474
|
+
| `isHostRequestErrorCode` / `resolveHostRequestErrorCode` | Error code validation |
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
Constants: `AIWORKER_SDK_CHANNEL`, `AIWORKER_SDK_API_VERSION`, `HOST_LINEAR_API_ORIGIN`, `GUEST_*_MAX`, `GUEST_REQUEST_TIMEOUT_MS`, `GUEST_ACTIONS_MAX`, `GUEST_COMMANDS_MAX`, `GUEST_COMMAND_NAME`, `GUEST_TOOLS_MAX`, `GUEST_TOOL_MATCH`, `GUEST_TOOL_OUTPUTS`, `HOST_REQUEST_ERROR_CODES`, `SERVICE_STATUS_VALUES`, `SESSION_LIFECYCLE_PHASES`, `START_SESSION_SENT`.
|
|
478
|
+
|
|
479
|
+
Wire messages added for these: host → guest `resolve` (`{ id, payload: { command, args } }`), guest → host `resolve-result` (`{ id, payload: { item } | { error } }`, no `result` comes back) and `badge` (`{ count }`).
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## 4. Local services (`contributes.service`)
|
|
484
|
+
|
|
485
|
+
For Docker sockets, CLI binaries, kubectl, and similar. The sandboxed iframe cannot dial Unix sockets; the host spawns a package entry and proxies HTTP.
|
|
486
|
+
|
|
487
|
+
Panel → `serviceRequest` → host → `127.0.0.1:port` → service process → socket/CLI.
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
| Panel call | Role |
|
|
491
|
+
| ----------------------------------------------- | -------------------------------- |
|
|
492
|
+
| `serviceRequest({ method, path, query?, body? })` | Proxy to this guest's service only |
|
|
493
|
+
| `serviceStatus()` | Lifecycle state |
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
Manifest sketch: `service.entry` (path to **built** JS, e.g. `service/main.js`), `runtime: "host"`, `permissions.sockets` and/or `permissions.exec`. Install refuses with `missing-build` when that file is absent. Declaring a service adds `service` to the capabilities the user approves at install; until then `serviceRequest` is `NO_SERVICE`.
|
|
497
|
+
|
|
498
|
+
Bundle the service with the Node target:
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
bunx openchamber-guest-bundle --node service/main.ts service/main.js
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Full contract (env vars, `/health`, grants, socket overrides): `[GUEST_SERVICES.md](https://roweb.cn/roweb/aihander/blob/main/packages/sdk/GUEST_SERVICES.md)`.
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## 5. What this package does not provide
|
|
509
|
+
|
|
510
|
+
Frozen on `apiVersion` 1 — named in docs, no host hole yet:
|
|
511
|
+
|
|
512
|
+
- Host-side `issues.search` / `issues.get` (guest draws the list; chip is `attach`)
|
|
513
|
+
- Public OAuth broker
|
|
514
|
+
- Keyboard shortcuts, raw git remotes, magic prompts
|
|
515
|
+
- Second `host.provider` beyond Linear
|
|
516
|
+
- 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
|
|
517
|
+
|
|
518
|
+
Do not go around the guest contract through `RuntimeAPIs`.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## 6. Minimal panel sketch
|
|
523
|
+
|
|
524
|
+
Mount once and keep the handles. `onReady` can repeat when the session or theme changes. Field listeners replay their current values too; compare relevant fields before fetching data again. For a complete request example with stale-response handling, see [the extension example](https://docs.aiworker.dev/sdk/example/).
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
import { connectHost } from '@aiworker/sdk';
|
|
528
|
+
import { applyHostReady, mountList, mountEmpty } from '@aiworker/sdk/ui';
|
|
529
|
+
|
|
530
|
+
const host = connectHost();
|
|
531
|
+
const root = document.querySelector('#root')!;
|
|
532
|
+
|
|
533
|
+
let mounted = false;
|
|
534
|
+
host.onReady((ctx) => {
|
|
535
|
+
applyHostReady(ctx, document.documentElement);
|
|
536
|
+
if (mounted) return;
|
|
537
|
+
mounted = true;
|
|
538
|
+
|
|
539
|
+
const signInRoot = root.appendChild(document.createElement('div'));
|
|
540
|
+
const listRoot = root.appendChild(document.createElement('div'));
|
|
541
|
+
mountEmpty(signInRoot, {
|
|
542
|
+
title: 'Connect Acme',
|
|
543
|
+
action: { label: 'Sign in', onClick: () => { void host.oauthStart(); } },
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
mountList(listRoot, {
|
|
547
|
+
items: [], // fill from host.request
|
|
548
|
+
onSelect: (id) => {
|
|
549
|
+
void host.attach({
|
|
550
|
+
providerId: 'acme-hello',
|
|
551
|
+
id,
|
|
552
|
+
title: id,
|
|
553
|
+
url: '',
|
|
554
|
+
});
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
host.onConnection((connection) => {
|
|
558
|
+
signInRoot.hidden = connection.connected;
|
|
559
|
+
listRoot.hidden = !connection.connected;
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Related files
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
| File | Audience |
|
|
570
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
|
571
|
+
| [README.md](https://roweb.cn/roweb/aihander/blob/main/packages/sdk/README.md) | Package overview and first hole |
|
|
572
|
+
| [DOCUMENTATION.md](https://roweb.cn/roweb/aihander/blob/main/packages/sdk/DOCUMENTATION.md) | Agent / maintainer invariants |
|
|
573
|
+
| [GUEST_SERVICES.md](https://roweb.cn/roweb/aihander/blob/main/packages/sdk/GUEST_SERVICES.md) | Local service contract |
|
|
574
|
+
| [src/ui/DOCUMENTATION.md](https://roweb.cn/roweb/aihander/blob/main/packages/sdk/src/ui/DOCUMENTATION.md) | UI kit invariants |
|
|
575
|
+
| [sdk.mdx](https://roweb.cn/roweb/aihander/blob/main/packages/docs/content/docs/sdk.mdx) / [sdk/host.mdx](https://roweb.cn/roweb/aihander/blob/main/packages/docs/content/docs/sdk/host.mdx) / [sdk/ui.mdx](https://roweb.cn/roweb/aihander/blob/main/packages/docs/content/docs/sdk/ui.mdx) | Author-facing website pages |
|