chatablex-web-sdk 1.0.0 → 1.0.3
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/README.md +700 -109
- package/README.zh-CN.md +699 -108
- package/dist/index.d.mts +8 -33
- package/dist/index.d.ts +8 -33
- package/dist/index.js +72 -10
- package/dist/index.mjs +72 -10
- package/package.json +13 -4
- package/src/bridge.ts +1 -1
- package/src/index.ts +4 -3
- package/src/modules/platform.ts +14 -0
- package/src/modules/ui.ts +2 -2
- package/src/types.ts +6 -38
- package/src/modules/skills.ts +0 -14
package/README.md
CHANGED
|
@@ -1,199 +1,790 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ChatableX Web SDK
|
|
2
2
|
|
|
3
3
|
English | [**简体中文**](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
Runtime SDK for building
|
|
5
|
+
**Runtime SDK for building ChatableX AI App WebUI extensions.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`chatablex-web-sdk` is the official JavaScript/TypeScript library that connects your web application to the **ChatableX desktop client** (Flutter WebView host). Unlike a type-only package, it ships the real bridge runtime — request/response RPC, event subscriptions, and tool execution callbacks.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Your WebUI runs inside a WebView. Many capabilities — native dialogs, file picking, session-aware storage, AI calls through the host pipeline — are awkward or inconsistent with browser-only APIs. This SDK exposes them as typed, promise-based modules.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Requirements](#requirements)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Project Setup](#project-setup)
|
|
19
|
+
- [Architecture](#architecture)
|
|
20
|
+
- [Core Concept: Tool Execution](#core-concept-tool-execution)
|
|
21
|
+
- [API Reference](#api-reference)
|
|
22
|
+
- [ChatableX (entry)](#chatablex-entry)
|
|
23
|
+
- [sdk.tool](#sdktool)
|
|
24
|
+
- [sdk.events](#sdkevents)
|
|
25
|
+
- [sdk.ai](#sdkai)
|
|
26
|
+
- [sdk.ui](#sdkui)
|
|
27
|
+
- [sdk.storage](#sdkstorage)
|
|
28
|
+
- [sdk.tools](#sdktools)
|
|
29
|
+
- [sdk.platform](#sdkplatform)
|
|
30
|
+
- [Events Reference](#events-reference)
|
|
31
|
+
- [Permissions](#permissions)
|
|
32
|
+
- [Host Capability Matrix](#host-capability-matrix)
|
|
33
|
+
- [Local Development](#local-development)
|
|
34
|
+
- [Framework Integration](#framework-integration)
|
|
35
|
+
- [TypeScript Types](#typescript-types)
|
|
36
|
+
- [Best Practices](#best-practices)
|
|
37
|
+
- [Troubleshooting](#troubleshooting)
|
|
38
|
+
- [Examples](#examples)
|
|
39
|
+
- [Versioning](#versioning)
|
|
40
|
+
- [License](#license)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
| Requirement | Details |
|
|
47
|
+
|-------------|---------|
|
|
48
|
+
| **ChatableX client** | Desktop app with WebView bridge (Flutter host) |
|
|
49
|
+
| **Extension mode** | `execution_mode: "webapp"` in `manifest.json` |
|
|
50
|
+
| **Node.js** | ≥ 16 (for building your WebUI) |
|
|
51
|
+
| **Build output** | `webui.entry` must point to `./dist/index.html` (Vite or equivalent) |
|
|
52
|
+
| **SDK install** | You **must** `npm install chatablex-web-sdk` — the host does **not** inject the SDK |
|
|
53
|
+
|
|
54
|
+
The platform consumes two things from your extension:
|
|
55
|
+
|
|
56
|
+
1. **Built artifacts** at `chatablex.webapp.webui.entry` (typically `./dist/index.html`)
|
|
57
|
+
2. **Bridge calls** via this SDK (`ChatableX.init`, `sdk.tool.onExecute`, etc.)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
10
62
|
|
|
11
63
|
```bash
|
|
12
64
|
npm install chatablex-web-sdk
|
|
13
|
-
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Local development against a monorepo copy:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
14
70
|
npm install ../chatablex-web-sdk
|
|
71
|
+
# or
|
|
72
|
+
npm install file:../chatablex-web-sdk
|
|
15
73
|
```
|
|
16
74
|
|
|
75
|
+
**Package exports** (ESM + CJS + TypeScript declarations):
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { ChatableX } from 'chatablex-web-sdk';
|
|
79
|
+
import type { ChatableXSDK, ToolResult, ChatResponse } from 'chatablex-web-sdk';
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
17
84
|
## Quick Start
|
|
18
85
|
|
|
19
|
-
|
|
86
|
+
Minimal integration — handle LLM tool calls in your WebUI:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
20
89
|
import { ChatableX } from 'chatablex-web-sdk';
|
|
21
90
|
|
|
22
|
-
|
|
23
|
-
const sdk = await ChatableX.init({
|
|
91
|
+
async function main() {
|
|
92
|
+
const sdk = await ChatableX.init({
|
|
93
|
+
appId: 'my-counter-app', // must match manifest.json "id"
|
|
94
|
+
debug: true,
|
|
95
|
+
});
|
|
24
96
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
97
|
+
sdk.tool.onExecute(async (params) => {
|
|
98
|
+
const { action, value } = params;
|
|
99
|
+
|
|
100
|
+
if (action === 'increment') {
|
|
101
|
+
const next = (Number(value) || 0) + 1;
|
|
102
|
+
return { success: true, data: { value: next } };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main().catch(console.error);
|
|
31
110
|
```
|
|
32
111
|
|
|
33
|
-
|
|
112
|
+
**You do not need every module.** The smallest production integration is usually `sdk.tool` only. Add `sdk.storage`, `sdk.events`, `sdk.ui`, etc. when your product needs them.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Project Setup
|
|
117
|
+
|
|
118
|
+
### manifest.json (webapp extension)
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"id": "my-counter-app",
|
|
123
|
+
"name": "Counter App",
|
|
124
|
+
"version": "1.0.0",
|
|
125
|
+
"type": "app",
|
|
126
|
+
"execution_mode": "webapp",
|
|
127
|
+
"return_direct": true,
|
|
128
|
+
"permissions": ["notification"],
|
|
129
|
+
"tools": [
|
|
130
|
+
{
|
|
131
|
+
"name": "counter_control",
|
|
132
|
+
"description": "Control the counter widget",
|
|
133
|
+
"inputSchema": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"action": { "type": "string", "enum": ["increment", "decrement", "get"] },
|
|
137
|
+
"value": { "type": "number" }
|
|
138
|
+
},
|
|
139
|
+
"required": ["action"]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
"chatablex": {
|
|
144
|
+
"webapp": {
|
|
145
|
+
"webui": {
|
|
146
|
+
"entry": "./dist/index.html"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
34
152
|
|
|
35
|
-
|
|
153
|
+
| Field | Rule |
|
|
154
|
+
|-------|------|
|
|
155
|
+
| `id` | Must equal `ChatableX.init({ appId })` |
|
|
156
|
+
| `execution_mode` | Must be `"webapp"` |
|
|
157
|
+
| `webui.entry` | Relative path → local HTTP serve; `https://` → remote URL |
|
|
158
|
+
| `tools[]` | Declares LLM-callable functions; host forwards args to `sdk.tool.onExecute` |
|
|
159
|
+
| `permissions` | Gates host-side APIs — see [Permissions](#permissions) |
|
|
160
|
+
|
|
161
|
+
### package.json scripts
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"scripts": {
|
|
166
|
+
"dev": "vite",
|
|
167
|
+
"build": "vite build",
|
|
168
|
+
"preview": "vite preview"
|
|
169
|
+
},
|
|
170
|
+
"dependencies": {
|
|
171
|
+
"chatablex-web-sdk": "^1.0.0"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
36
175
|
|
|
37
|
-
|
|
176
|
+
Run `npm run build` before publishing. The ChatableX client loads `dist/index.html`, not your dev server (unless you configure a remote `webui.entry` URL).
|
|
38
177
|
|
|
178
|
+
### Recommended project layout
|
|
39
179
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
180
|
+
```
|
|
181
|
+
my-app/
|
|
182
|
+
├── manifest.json # extension metadata
|
|
183
|
+
├── package.json
|
|
184
|
+
├── index.html # Vite entry HTML
|
|
185
|
+
├── src/
|
|
186
|
+
│ ├── main.ts # ChatableX.init() + app bootstrap
|
|
187
|
+
│ ├── app.ts # UI logic
|
|
188
|
+
│ └── bridge.ts # optional: tool routing helpers
|
|
189
|
+
├── dist/ # build output (consumed by host)
|
|
190
|
+
│ └── index.html
|
|
191
|
+
└── vite.config.ts
|
|
192
|
+
```
|
|
48
193
|
|
|
49
|
-
|
|
194
|
+
---
|
|
50
195
|
|
|
51
|
-
##
|
|
196
|
+
## Architecture
|
|
52
197
|
|
|
53
|
-
|
|
198
|
+
```
|
|
199
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
200
|
+
│ Your Web App (React / Vue / Svelte / Vanilla) │
|
|
201
|
+
│ import { ChatableX } from 'chatablex-web-sdk' │
|
|
202
|
+
└────────────────────────────┬─────────────────────────────────┘
|
|
203
|
+
│
|
|
204
|
+
▼
|
|
205
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
206
|
+
│ chatablex-web-sdk │
|
|
207
|
+
│ │
|
|
208
|
+
│ Bridge (RPC + events) │
|
|
209
|
+
│ JS → Host : window.ChatableXBridge.postMessage(JSON) │
|
|
210
|
+
│ Host → JS : window.ChatableXReceive(JSON) │
|
|
211
|
+
│ │
|
|
212
|
+
│ Modules: tool · events · ai · ui · storage · tools · │
|
|
213
|
+
│ tools · platform │
|
|
214
|
+
└────────────────────────────┬─────────────────────────────────┘
|
|
215
|
+
│ WebView JavaScriptChannel
|
|
216
|
+
▼
|
|
217
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
218
|
+
│ ChatableX Flutter Client │
|
|
219
|
+
│ Chat UI · SSE stream · Agent graph · SQLite storage │
|
|
220
|
+
└──────────────────────────────────────────────────────────────┘
|
|
221
|
+
```
|
|
54
222
|
|
|
55
|
-
|
|
56
|
-
|-----------|---------|---------|-------------|
|
|
57
|
-
| `appId` | string | — | **Required.** Must match your `manifest.json` `id`. |
|
|
58
|
-
| `debug` | boolean | false | Print debug logs to console. |
|
|
59
|
-
| `timeout` | number | 10000 | Handshake timeout in ms. |
|
|
223
|
+
### Bridge protocol
|
|
60
224
|
|
|
61
|
-
|
|
225
|
+
**Request (JS → Flutter):**
|
|
62
226
|
|
|
63
|
-
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"id": "ctx_1_1718200000000",
|
|
230
|
+
"method": "storage.get",
|
|
231
|
+
"params": { "key": "filters" },
|
|
232
|
+
"timestamp": 1718200000000
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Response (Flutter → JS):**
|
|
64
237
|
|
|
65
|
-
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"type": "response",
|
|
241
|
+
"id": "ctx_1_1718200000000",
|
|
242
|
+
"success": true,
|
|
243
|
+
"data": { "projectId": "p1" }
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Event push (Flutter → JS):**
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"type": "event",
|
|
252
|
+
"eventType": "toolExecution",
|
|
253
|
+
"data": { "action": "increment", "_requestId": "texec_1_...", "_toolName": "counter_control" }
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Tool result (JS → Flutter, fire-and-forget):**
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"method": "tool.executeResult",
|
|
262
|
+
"params": {
|
|
263
|
+
"_requestId": "texec_1_...",
|
|
264
|
+
"success": true,
|
|
265
|
+
"data": { "value": 42 }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
> `tool.executeResult` does **not** use the RPC `id` field. The host correlates results via `_requestId`. This is required because WebView `evaluateJavaScript` cannot await Promises.
|
|
271
|
+
|
|
272
|
+
### Initialization sequence
|
|
273
|
+
|
|
274
|
+
1. Your bundle loads in the WebView.
|
|
275
|
+
2. You call `ChatableX.init({ appId })`.
|
|
276
|
+
3. SDK installs `window.ChatableXReceive`.
|
|
277
|
+
4. SDK waits for `window.ChatableXBridge` (set by Flutter).
|
|
278
|
+
5. SDK sends `sdk_init` handshake → host responds with tool metadata.
|
|
279
|
+
6. SDK exposes `window.ChatableX` and returns the `sdk` object.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Core Concept: Tool Execution
|
|
284
|
+
|
|
285
|
+
This is the **primary integration path** for AI Apps. When the LLM invokes your tool, the host forwards parameters into your WebUI and waits for a result.
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
LLM (Agent) Flutter Host Your WebUI (SDK)
|
|
289
|
+
│ │ │
|
|
290
|
+
│ frontend_tool_call │ │
|
|
291
|
+
│────────────────────>│ │
|
|
292
|
+
│ │ event: toolExecution │
|
|
293
|
+
│ │ { ...args, _requestId } │
|
|
294
|
+
│ │─────────────────────────>│
|
|
295
|
+
│ │ │ onExecute(params)
|
|
296
|
+
│ │ │ → your logic
|
|
297
|
+
│ │ tool.executeResult │
|
|
298
|
+
│ │<─────────────────────────│
|
|
299
|
+
│ tool-result POST │ │
|
|
300
|
+
│<────────────────────│ │
|
|
301
|
+
│ Agent continues │ │
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Handler contract
|
|
66
305
|
|
|
67
306
|
```ts
|
|
68
307
|
sdk.tool.onExecute(async (params) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
308
|
+
// params includes LLM arguments PLUS host metadata:
|
|
309
|
+
// _toolName — which manifest tool was invoked (string)
|
|
310
|
+
// _requestId — correlation id (string, set by host)
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
success: true, // required
|
|
314
|
+
data: { /* any */ }, // optional, returned to LLM
|
|
315
|
+
error: 'reason', // optional, when success is false
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
| Return field | Type | Description |
|
|
321
|
+
|--------------|------|-------------|
|
|
322
|
+
| `success` | `boolean` | Whether the operation succeeded |
|
|
323
|
+
| `data` | `unknown` | Payload for the LLM / session (any JSON-serializable value) |
|
|
324
|
+
| `error` | `string` | Human-readable error when `success: false` |
|
|
325
|
+
|
|
326
|
+
**Rules:**
|
|
327
|
+
|
|
328
|
+
- Register **one** handler via `onExecute`. Calling it again **replaces** the previous handler.
|
|
329
|
+
- Handler exceptions are caught and converted to `{ success: false, error: message }`.
|
|
330
|
+
- If no handler is registered, the host receives `{ success: false, error: 'No execute handler registered' }`.
|
|
331
|
+
- Always route multi-tool apps by `params._toolName` (see `game-maker` reference below).
|
|
332
|
+
- The host times out after **30 seconds** if no `tool.executeResult` arrives.
|
|
333
|
+
|
|
334
|
+
### Multi-tool routing example
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
sdk.tool.onExecute(async (params) => {
|
|
338
|
+
const toolName = typeof params._toolName === 'string' ? params._toolName : '';
|
|
339
|
+
|
|
340
|
+
switch (toolName) {
|
|
341
|
+
case 'counter_control':
|
|
342
|
+
return handleCounter(params);
|
|
343
|
+
case 'export_data':
|
|
344
|
+
return handleExport(params);
|
|
345
|
+
default:
|
|
346
|
+
return { success: false, error: `Unknown tool: ${toolName}` };
|
|
73
347
|
}
|
|
74
|
-
return { success: false, error: 'unknown action' };
|
|
75
348
|
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
76
352
|
|
|
77
|
-
|
|
353
|
+
## API Reference
|
|
354
|
+
|
|
355
|
+
### ChatableX (entry)
|
|
356
|
+
|
|
357
|
+
#### `ChatableX.init(config): Promise<ChatableXSDK>`
|
|
358
|
+
|
|
359
|
+
Initialize the SDK and connect to the Flutter host.
|
|
360
|
+
|
|
361
|
+
| Option | Type | Default | Description |
|
|
362
|
+
|--------|------|---------|-------------|
|
|
363
|
+
| `appId` | `string` | — | **Required.** Must match `manifest.json` `id`. |
|
|
364
|
+
| `debug` | `boolean` | `false` | Log bridge activity to `console`. |
|
|
365
|
+
| `timeout` | `number` | `10000` | Ms to wait for `ChatableXBridge` during handshake. |
|
|
366
|
+
|
|
367
|
+
Returns a singleton. Subsequent `init()` calls return the same instance (first `appId` wins).
|
|
368
|
+
|
|
369
|
+
Throws if `ChatableXBridge` is not available within `timeout`.
|
|
370
|
+
|
|
371
|
+
#### `ChatableX.getInstance(): ChatableXSDK`
|
|
372
|
+
|
|
373
|
+
Returns the current instance. Throws if `init()` has not been called.
|
|
374
|
+
|
|
375
|
+
#### `ChatableX.isReady(): boolean`
|
|
376
|
+
|
|
377
|
+
`true` after the first successful `init()`.
|
|
378
|
+
|
|
379
|
+
#### `ChatableX.version: string`
|
|
380
|
+
|
|
381
|
+
Current SDK version (e.g. `"1.0.0"`).
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
### `sdk.tool`
|
|
386
|
+
|
|
387
|
+
Register and inspect your extension's tool execution handler.
|
|
388
|
+
|
|
389
|
+
| Method | Signature | Description |
|
|
390
|
+
|--------|-----------|-------------|
|
|
391
|
+
| `onExecute` | `(handler) => void` | Register the LLM tool handler. **Required for webapp extensions.** |
|
|
392
|
+
| `getInfo` | `() => ToolInfo` | Tool metadata from host handshake (`id`, `name`, `version`, `description`). |
|
|
393
|
+
|
|
394
|
+
```ts
|
|
78
395
|
const info = sdk.tool.getInfo();
|
|
396
|
+
// { id: 'my-app', name: 'My App', version: '1.0.0', description: '...' }
|
|
79
397
|
```
|
|
80
398
|
|
|
399
|
+
---
|
|
400
|
+
|
|
81
401
|
### `sdk.events`
|
|
82
402
|
|
|
83
|
-
|
|
403
|
+
Subscribe to host-pushed events. Each subscription also sends `events.subscribe` to the host so it knows to forward matching events.
|
|
84
404
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
405
|
+
| Method | Description |
|
|
406
|
+
|--------|-------------|
|
|
407
|
+
| `on(eventType, callback)` | Generic subscription. Returns `unsubscribe` function. |
|
|
408
|
+
| `onAiResponse(callback)` | Shorthand for `'aiResponse'`. |
|
|
409
|
+
| `onToolExecution(callback)` | Shorthand for `'toolExecution'`. |
|
|
410
|
+
| `onUserMessage(callback)` | Shorthand for `'userMessage'`. |
|
|
89
411
|
|
|
90
|
-
|
|
91
|
-
|
|
412
|
+
```ts
|
|
413
|
+
const unsub = sdk.events.on('streamingContent', ({ content, finished }) => {
|
|
414
|
+
appendToken(content);
|
|
92
415
|
if (finished) setLoading(false);
|
|
93
416
|
});
|
|
94
417
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Unsubscribe on unmount to avoid leaks
|
|
100
|
-
// unsubUser(); unsubStream(); unsubAi();
|
|
418
|
+
// Clean up on component unmount
|
|
419
|
+
unsub();
|
|
101
420
|
```
|
|
102
421
|
|
|
103
|
-
|
|
422
|
+
> **Note:** `unsubscribe()` removes the local listener only. The host is not notified via `events.unsubscribe` in the current SDK version.
|
|
423
|
+
|
|
424
|
+
---
|
|
104
425
|
|
|
105
426
|
### `sdk.ai`
|
|
106
427
|
|
|
107
|
-
|
|
428
|
+
Call the host's AI pipeline from your WebUI. Requires `ai_chat` permission in `manifest.json`.
|
|
429
|
+
|
|
430
|
+
| Method | Signature | Description |
|
|
431
|
+
|--------|-----------|-------------|
|
|
432
|
+
| `chat` | `(message, options?) => Promise<ChatResponse>` | Send a message through the host AI stack. |
|
|
433
|
+
| `chatStream` | `(message, options?) => Promise<unknown>` | Initiate streaming chat. Tokens arrive via `sdk.events.on('streamingContent')`. |
|
|
434
|
+
| `getContext` | `() => Promise<SessionContext>` | Fetch current session metadata and messages. |
|
|
108
435
|
|
|
109
436
|
```ts
|
|
110
|
-
const reply = await sdk.ai.chat('Summarize the last
|
|
437
|
+
const reply = await sdk.ai.chat('Summarize the last three messages', {
|
|
438
|
+
sessionId: 'optional-override',
|
|
111
439
|
stream: false,
|
|
112
440
|
});
|
|
113
441
|
|
|
114
442
|
const ctx = await sdk.ai.getContext();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Streaming may be pushed by the host; call chatStream when supported
|
|
118
|
-
await sdk.ai.chatStream('Write a short reply', { stream: true });
|
|
443
|
+
console.log(ctx.messages.length, ctx.activeTools);
|
|
119
444
|
```
|
|
120
445
|
|
|
446
|
+
**`ChatOptions`:** `sessionId`, `context`, `tools`, `skills`, `stream`.
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
121
450
|
### `sdk.ui`
|
|
122
451
|
|
|
123
|
-
|
|
452
|
+
Drive native host UI from your WebUI.
|
|
124
453
|
|
|
125
|
-
|
|
126
|
-
|
|
454
|
+
| Method | Signature | Permission | Description |
|
|
455
|
+
|--------|-----------|------------|-------------|
|
|
456
|
+
| `showNotification` | `(message, type?) => Promise<void>` | `notification` | Toast: `info` \| `success` \| `warning` \| `error`. |
|
|
457
|
+
| `showConfirm` | `(title, message) => Promise<boolean>` | — | Native confirm dialog. Returns `true` if confirmed. |
|
|
458
|
+
| `pickFile` | `(options?) => Promise<string \| null>` | `file_access` | Open native file picker. Returns path or `null` if cancelled. |
|
|
459
|
+
| `openTab` | `(config) => Promise<void>` | — | Request a new tab in the host shell. |
|
|
460
|
+
| `updateState` | `(state) => Promise<void>` | — | Notify host to refresh UI (e.g. `{ refreshMessages: true }`). |
|
|
127
461
|
|
|
128
|
-
|
|
462
|
+
```ts
|
|
463
|
+
const ok = await sdk.ui.showConfirm('Delete', 'This cannot be undone.');
|
|
129
464
|
if (!ok) return;
|
|
130
465
|
|
|
131
|
-
|
|
132
|
-
if (path) await uploadPreview(path);
|
|
133
|
-
|
|
466
|
+
await sdk.ui.showNotification('Saved', 'success');
|
|
134
467
|
await sdk.ui.updateState({ refreshMessages: true });
|
|
135
|
-
// await sdk.ui.openTab({ title: 'Details', type: 'custom', data: { id: 'x' } });
|
|
136
468
|
```
|
|
137
469
|
|
|
470
|
+
**`FilePickerOptions`:** `type` (`any` \| `image` \| `video` \| `audio` \| `custom`), `multiple`, `allowedExtensions`.
|
|
471
|
+
|
|
472
|
+
**`TabConfig`:** `id`, `title`, `type` (`chat` \| `tool` \| `skill` \| `custom`), optional `icon`, `data`.
|
|
473
|
+
|
|
474
|
+
> **Host-only:** `ui.saveFile` (native Save As dialog) is implemented in the Flutter host but not yet wrapped by this SDK. Advanced integrations can call it via raw `ChatableXBridge.postMessage`.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
138
478
|
### `sdk.storage`
|
|
139
479
|
|
|
140
|
-
|
|
480
|
+
Key-value storage persisted by the host (SQLite, scoped per tool). Use instead of `localStorage` when you need data to survive WebView resets and align with the desktop app.
|
|
481
|
+
|
|
482
|
+
| Method | Signature | Description |
|
|
483
|
+
|--------|-----------|-------------|
|
|
484
|
+
| `get` | `<T>(key) => Promise<T \| null>` | Read a value. Returns `null` if missing. |
|
|
485
|
+
| `set` | `<T>(key, value) => Promise<void>` | Write a JSON-serializable value. |
|
|
486
|
+
| `delete` | `(key) => Promise<void>` | Remove a key. |
|
|
141
487
|
|
|
142
488
|
```ts
|
|
143
|
-
const KEY = 'my-app:
|
|
489
|
+
const KEY = 'my-app:draft';
|
|
144
490
|
|
|
145
|
-
await sdk.storage.set(KEY, {
|
|
146
|
-
const
|
|
491
|
+
await sdk.storage.set(KEY, { title: 'Draft', nodes: [] });
|
|
492
|
+
const draft = await sdk.storage.get<{ title: string }>(KEY);
|
|
147
493
|
await sdk.storage.delete(KEY);
|
|
148
494
|
```
|
|
149
495
|
|
|
150
|
-
|
|
496
|
+
Storage keys are namespaced per tool instance on the host side.
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
### `sdk.tools`
|
|
151
501
|
|
|
152
|
-
|
|
502
|
+
List and invoke **other** platform tools from your WebUI.
|
|
503
|
+
|
|
504
|
+
| Method | Signature | Description |
|
|
505
|
+
|--------|-----------|-------------|
|
|
506
|
+
| `list` | `() => Promise<ToolInfo[]>` | List available tools. |
|
|
507
|
+
| `execute` | `(toolId, params) => Promise<ToolResult>` | Invoke a tool immediately. |
|
|
508
|
+
| `executeWithConfirm` | `(toolId, params) => Promise<ToolResult>` | Invoke after host confirmation dialog. |
|
|
153
509
|
|
|
154
510
|
```ts
|
|
155
511
|
const tools = await sdk.tools.list();
|
|
156
|
-
|
|
512
|
+
const result = await sdk.tools.execute('fetch-doc', { url: 'https://...' });
|
|
513
|
+
if (!result.success) throw new Error(result.error);
|
|
514
|
+
```
|
|
157
515
|
|
|
158
|
-
|
|
159
|
-
if (!step1.success) throw new Error(step1.error);
|
|
160
|
-
const step2 = await sdk.tools.execute('summarize', { text: step1.data });
|
|
516
|
+
> Skill-type extensions (`execution_mode: "skill"`) are activated in the chat session and injected into the system prompt — not executed via a separate SDK module. Use `sdk.tools` if your WebUI needs to orchestrate other extensions.
|
|
161
517
|
|
|
162
|
-
|
|
163
|
-
await sdk.tools.executeWithConfirm('delete-backup', { id: backupId });
|
|
518
|
+
---
|
|
164
519
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
520
|
+
### `sdk.platform`
|
|
521
|
+
|
|
522
|
+
Platform-level utilities.
|
|
523
|
+
|
|
524
|
+
| Method | Signature | Description |
|
|
525
|
+
|--------|-----------|-------------|
|
|
526
|
+
| `openInBrowser` | `(targetUrl) => Promise<void>` | Open URL in the system browser with auth handoff. |
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
await sdk.platform.openInBrowser('https://docs.example.com/guide');
|
|
170
530
|
```
|
|
171
531
|
|
|
172
|
-
|
|
532
|
+
Throws if `targetUrl` is empty or whitespace-only.
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Events Reference
|
|
537
|
+
|
|
538
|
+
| Event | Payload | When fired |
|
|
539
|
+
|-------|---------|------------|
|
|
540
|
+
| `toolExecution` | `{ toolCall, result? }` or raw args + `_requestId` | LLM invokes a tool; also used internally for `onExecute` dispatch |
|
|
541
|
+
| `aiResponse` | `ChatResponse` | AI reply completed in the host session |
|
|
542
|
+
| `streamingContent` | `{ content, finished? }` | Token/chunk during streaming generation |
|
|
543
|
+
| `userMessage` | `{ message, timestamp }` | User sent a message in the main chat |
|
|
544
|
+
| `close` | `{ toolId }` | WebUI is about to close |
|
|
545
|
+
|
|
546
|
+
Subscribe before the event fires. Use the returned `unsubscribe()` function in framework cleanup hooks (`useEffect` return, `onUnmounted`, etc.).
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Permissions
|
|
551
|
+
|
|
552
|
+
Declare in `manifest.json` → `permissions[]`. The host rejects unauthorized API calls.
|
|
553
|
+
|
|
554
|
+
| Manifest value | SDK APIs gated | Description |
|
|
555
|
+
|----------------|----------------|-------------|
|
|
556
|
+
| `ai_chat` | `sdk.ai.*` | Access host AI pipeline |
|
|
557
|
+
| `file_access` | `sdk.ui.pickFile` | Native file picker |
|
|
558
|
+
| `notification` | `sdk.ui.showNotification` | System toasts |
|
|
559
|
+
| `network` | (host-level) | Network access for the extension |
|
|
560
|
+
| `system_command` | (host-level) | Execute system commands |
|
|
561
|
+
|
|
562
|
+
When denied, RPC calls reject with `Error: Permission denied: <permission>`.
|
|
173
563
|
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Host Capability Matrix
|
|
567
|
+
|
|
568
|
+
SDK methods are thin RPC wrappers. Some host handlers are fully implemented; others return stubs. Plan your extension accordingly.
|
|
569
|
+
|
|
570
|
+
| SDK method | Host status | Notes |
|
|
571
|
+
|------------|-------------|-------|
|
|
572
|
+
| `sdk.tool.onExecute` | **Production** | Core path — fully supported |
|
|
573
|
+
| `sdk.storage.*` | **Production** | SQLite per tool |
|
|
574
|
+
| `sdk.ui.showNotification` | **Production** | Requires `notification` |
|
|
575
|
+
| `sdk.ui.showConfirm` | **Production** | |
|
|
576
|
+
| `sdk.ui.pickFile` | **Production** | Requires `file_access` |
|
|
577
|
+
| `sdk.ui.updateState` | **Production** | Delegates to host |
|
|
578
|
+
| `sdk.platform.openInBrowser` | **Production** | Auth handoff |
|
|
579
|
+
| `sdk.ai.chat` | **Production** | Requires `ai_chat` + delegate |
|
|
580
|
+
| `sdk.ai.getContext` | **Partial** | Returns minimal context |
|
|
581
|
+
| `sdk.ai.chatStream` | **Partial** | Returns `{ streaming: true }`; tokens via events |
|
|
582
|
+
| `sdk.events.*` | **Production** | |
|
|
583
|
+
| `sdk.tools.list` | **Stub** | Returns `[]` (host stub) |
|
|
584
|
+
| `sdk.tools.execute` | **Delegate** | Requires host delegate |
|
|
585
|
+
| `sdk.ui.openTab` | **Stub** | Returns success, no action |
|
|
586
|
+
| `ui.saveFile` (raw) | **Production** | Host only — not yet in SDK |
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## Local Development
|
|
591
|
+
|
|
592
|
+
Your WebUI should work in a normal browser for UI development. Detect the host and skip SDK initialization when absent.
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
function isInsideChatableX(): boolean {
|
|
596
|
+
return typeof window.ChatableXBridge === 'object' && window.ChatableXBridge !== null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function bootstrap() {
|
|
600
|
+
if (isInsideChatableX()) {
|
|
601
|
+
const sdk = await ChatableX.init({ appId: 'my-app', debug: true });
|
|
602
|
+
sdk.tool.onExecute(handleTool);
|
|
603
|
+
} else {
|
|
604
|
+
console.log('Running outside ChatableX — SDK inactive');
|
|
605
|
+
// Use mocks, local state, or manual test triggers
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
mountApp();
|
|
609
|
+
}
|
|
174
610
|
```
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
611
|
+
|
|
612
|
+
**Tips:**
|
|
613
|
+
|
|
614
|
+
- Use `npm run dev` (Vite) for fast iteration in the browser.
|
|
615
|
+
- Use `npm run build` + load in ChatableX for integration testing.
|
|
616
|
+
- The host serves `dist/` over `http://127.0.0.1:<port>/` for local extensions.
|
|
617
|
+
- Set `debug: true` during development to see bridge logs.
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
## Framework Integration
|
|
622
|
+
|
|
623
|
+
### React
|
|
624
|
+
|
|
625
|
+
```tsx
|
|
626
|
+
import { useEffect, useRef } from 'react';
|
|
627
|
+
import { ChatableX, type ChatableXSDK } from 'chatablex-web-sdk';
|
|
628
|
+
|
|
629
|
+
export function useChatableX(appId: string) {
|
|
630
|
+
const sdkRef = useRef<ChatableXSDK | null>(null);
|
|
631
|
+
|
|
632
|
+
useEffect(() => {
|
|
633
|
+
let cancelled = false;
|
|
634
|
+
let unsubStream: (() => void) | undefined;
|
|
635
|
+
|
|
636
|
+
(async () => {
|
|
637
|
+
if (!window.ChatableXBridge) return;
|
|
638
|
+
const sdk = await ChatableX.init({ appId });
|
|
639
|
+
if (cancelled) return;
|
|
640
|
+
sdkRef.current = sdk;
|
|
641
|
+
|
|
642
|
+
sdk.tool.onExecute(async (params) => {
|
|
643
|
+
// handle tools
|
|
644
|
+
return { success: true };
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
unsubStream = sdk.events.on('streamingContent', (data) => {
|
|
648
|
+
// update state
|
|
649
|
+
});
|
|
650
|
+
})();
|
|
651
|
+
|
|
652
|
+
return () => {
|
|
653
|
+
cancelled = true;
|
|
654
|
+
unsubStream?.();
|
|
655
|
+
};
|
|
656
|
+
}, [appId]);
|
|
657
|
+
|
|
658
|
+
return sdkRef;
|
|
659
|
+
}
|
|
195
660
|
```
|
|
196
661
|
|
|
662
|
+
### Vue 3
|
|
663
|
+
|
|
664
|
+
```ts
|
|
665
|
+
import { onMounted, onUnmounted, shallowRef } from 'vue';
|
|
666
|
+
import { ChatableX, type ChatableXSDK } from 'chatablex-web-sdk';
|
|
667
|
+
|
|
668
|
+
export function useChatableX(appId: string) {
|
|
669
|
+
const sdk = shallowRef<ChatableXSDK | null>(null);
|
|
670
|
+
let unsub: (() => void) | undefined;
|
|
671
|
+
|
|
672
|
+
onMounted(async () => {
|
|
673
|
+
if (!window.ChatableXBridge) return;
|
|
674
|
+
sdk.value = await ChatableX.init({ appId });
|
|
675
|
+
sdk.value.tool.onExecute(handleTool);
|
|
676
|
+
unsub = sdk.value.events.onAiResponse(handleAiResponse);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
onUnmounted(() => unsub?.());
|
|
680
|
+
|
|
681
|
+
return { sdk };
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## TypeScript Types
|
|
688
|
+
|
|
689
|
+
All public types are exported:
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
import type {
|
|
693
|
+
ChatableXSDK,
|
|
694
|
+
ChatableXInitConfig,
|
|
695
|
+
ToolInfo,
|
|
696
|
+
ToolResult,
|
|
697
|
+
ToolExecuteHandler,
|
|
698
|
+
ChatResponse,
|
|
699
|
+
ChatOptions,
|
|
700
|
+
SessionContext,
|
|
701
|
+
EventType,
|
|
702
|
+
EventCallbackMap,
|
|
703
|
+
NotificationType,
|
|
704
|
+
FilePickerOptions,
|
|
705
|
+
TabConfig,
|
|
706
|
+
StateUpdate,
|
|
707
|
+
Unsubscribe,
|
|
708
|
+
} from 'chatablex-web-sdk';
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Global `window` augmentation (after init):
|
|
712
|
+
|
|
713
|
+
| Global | Set by | Purpose |
|
|
714
|
+
|--------|--------|---------|
|
|
715
|
+
| `window.ChatableX` | SDK | Live `ChatableXSDK` instance |
|
|
716
|
+
| `window.ChatableXReceive` | SDK | Host → JS message receiver |
|
|
717
|
+
| `window.ChatableXBridge` | Flutter | JS → Host `postMessage` channel |
|
|
718
|
+
| `window.__CHATABLEX_DISPATCH__` | SDK | Direct tool dispatch (advanced) |
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## Best Practices
|
|
723
|
+
|
|
724
|
+
1. **Always call `init()` once** at app bootstrap, before registering handlers.
|
|
725
|
+
2. **Match `appId` to manifest `id`** — mismatches cause subtle storage and routing bugs.
|
|
726
|
+
3. **Route by `_toolName`** when your extension declares multiple `tools[]` entries.
|
|
727
|
+
4. **Return structured `data`** — the LLM reads tool results in the session context.
|
|
728
|
+
5. **Use `sdk.storage` for persistence** — not `localStorage`, if you need host-aligned state.
|
|
729
|
+
6. **Unsubscribe events** on teardown to avoid duplicate handlers in SPA navigation.
|
|
730
|
+
7. **Guard with `isInsideChatableX()`** so `npm run dev` works without the desktop client.
|
|
731
|
+
8. **Build before publish** — the host loads `dist/`, not TypeScript source.
|
|
732
|
+
9. **Declare permissions upfront** — don't call gated APIs without manifest entries.
|
|
733
|
+
10. **Keep handlers fast** — the host enforces a 30s timeout on tool execution.
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## Troubleshooting
|
|
738
|
+
|
|
739
|
+
| Symptom | Likely cause | Fix |
|
|
740
|
+
|---------|--------------|-----|
|
|
741
|
+
| `ChatableXBridge not available` | Page loaded outside ChatableX, or init ran before channel registered | Guard with `isInsideChatableX()`; call `init()` after DOM ready |
|
|
742
|
+
| `ChatableX SDK not initialised` | `getInstance()` before `init()` | Await `init()` first |
|
|
743
|
+
| Tool call hangs 30s then fails | `onExecute` not registered, or no `tool.executeResult` sent | Ensure `init()` completed and handler is set |
|
|
744
|
+
| `Permission denied` | Missing manifest permission | Add `ai_chat`, `file_access`, or `notification` |
|
|
745
|
+
| `sdk_init handshake failed` | Host bridge not ready (non-fatal) | SDK continues with default metadata; check `debug: true` logs |
|
|
746
|
+
| Storage returns `null` | First read or wrong key | Normal on first access; verify key spelling |
|
|
747
|
+
| Works in dev, blank in ChatableX | Forgot to build, or wrong `webui.entry` | Run `npm run build`; verify `dist/index.html` exists |
|
|
748
|
+
| Second `init()` ignored | Singleton by design | Restart WebView to re-init with a different `appId` |
|
|
749
|
+
|
|
750
|
+
**Debug checklist:**
|
|
751
|
+
|
|
752
|
+
```ts
|
|
753
|
+
await ChatableX.init({ appId: 'my-app', debug: true });
|
|
754
|
+
console.log('SDK ready:', ChatableX.isReady());
|
|
755
|
+
console.log('Tool info:', ChatableX.getInstance().tool.getInfo());
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## Examples
|
|
761
|
+
|
|
762
|
+
Official sample apps under [`examples/`](examples/). Each includes unit tests + bridge integration tests + `dist/` build output.
|
|
763
|
+
|
|
764
|
+
| App | Framework | Tool | Demo flow |
|
|
765
|
+
|-----|-----------|------|-----------|
|
|
766
|
+
| [counter-app](examples/counter-app/) | React | `counter_control` | `get` → `increment` → `get` |
|
|
767
|
+
| [todo-app](examples/todo-app/) | Vue 3 | `todo_control` | `get` → `add` → `get` (uses `sdk.storage`) |
|
|
768
|
+
|
|
769
|
+
```bash
|
|
770
|
+
npm run test:examples # run all example tests
|
|
771
|
+
npm run build:examples # build both dist/
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
Both tools expose a **`get` action** so the LLM reads real state before mutating — critical for reliable multi-turn demos.
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
## Versioning
|
|
779
|
+
|
|
780
|
+
| SDK version | npm tag | Notes |
|
|
781
|
+
|-------------|---------|-------|
|
|
782
|
+
| `1.0.0` | `latest` | Current stable |
|
|
783
|
+
|
|
784
|
+
Breaking changes to bridge method names or `tool.executeResult` shape will trigger a major version bump. The Flutter host in each ChatableX client release is the canonical contract owner.
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
197
788
|
## License
|
|
198
789
|
|
|
199
|
-
MIT
|
|
790
|
+
MIT © ChatableX Team
|