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