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 CHANGED
@@ -1,199 +1,790 @@
1
- # chatablex-web-sdk
1
+ # ChatableX Web SDK
2
2
 
3
3
  English | [**简体中文**](README.zh-CN.md)
4
4
 
5
- Runtime SDK for building **ChatableX AI App** WebUI applications.
5
+ **Runtime SDK for building ChatableX AI App WebUI extensions.**
6
6
 
7
- Unlike a type-only package, this SDK contains the actual bridge runtime that connects your web app to the ChatableX Flutter host. You must install it as a dependencythe platform does **not** inject it for you.
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
- ## Install
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
- # or link locally during development:
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
- ```tsx
86
+ Minimal integration — handle LLM tool calls in your WebUI:
87
+
88
+ ```ts
20
89
  import { ChatableX } from 'chatablex-web-sdk';
21
90
 
22
- // Initialize connects to the Flutter WebView host
23
- const sdk = await ChatableX.init({ appId: 'my-app', debug: true });
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
- // Register a tool handler (called when the LLM invokes your tool)
26
- sdk.tool.onExecute(async (params) => {
27
- const { action, value } = params;
28
- // ... perform action, update UI ...
29
- return { success: true, result: 42 };
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
- ## What are the SDK namespaces for?
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
- The object returned by `ChatableX.init()` is a **set of APIs grouped by responsibility**. Your WebUI runs inside a WebView; many capabilities (native dialogs, file picking, storage aligned with the main chat, AI calls through the host’s stack) are awkward or inconsistent if you only use browser APIs. These modules call into the **Flutter host via the JS bridge**.
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
- **You do not need every module** for every app the smallest integration is usually `sdk.tool` (handle LLM invocations). Add others when you need them.
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
- | Namespace | Role (why it exists) |
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. |
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
- The **API** sections below each include: a **typical scenario** (when to use it) + **example code** (how to wire it up).
194
+ ---
50
195
 
51
- ## API
196
+ ## Architecture
52
197
 
53
- ### `ChatableX.init(config)`
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
- | Option | Type | Default | Description |
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
- Returns `Promise<ChatableXSDK>`.
225
+ **Request (JS → Flutter):**
62
226
 
63
- ### `sdk.tool`
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
- **When to use**: The user opens your AI App from chat, or the model invokes your tool; the host forwards JSON params into the WebView and you return a result back into the session.
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
- const { action, rowId } = params as { action?: string; rowId?: string };
70
- if (action === 'delete') {
71
- await deleteRow(rowId);
72
- return { success: true, message: 'Deleted' };
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
- // Metadata from manifest (filled in by the host after handshake)
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
- **When to use**: Your side panel should stay in sync with the main window—new user messages, streaming assistant output, or other tool executions should update your UI.
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
- ```ts
86
- const unsubUser = sdk.events.onUserMessage(({ message }) => {
87
- appendActivityFeed(`User: ${message}`);
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
- const unsubStream = sdk.events.on('streamingContent', ({ content, finished }) => {
91
- setPartialReply(content);
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
- const unsubAi = sdk.events.onAiResponse((data) => {
96
- setLastReply(data.content);
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
- You can also subscribe to `toolExecution`, `close`, etc. (depends on host support).
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
- **When to use**: A button in your panel like “ask about this session again” should use the **hosts** model and context, not a separate API key inside the page; or you need session metadata for a summary view.
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 user message in three bullets', {
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
- if (ctx.name) setSessionTitle(ctx.name); // some fields depend on host implementation
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
- **When to use**: Destructive actions need a **native confirm**; long jobs end with a **host toast**; picking files should use the **host file picker**; after work you may **refresh the main transcript** or close the WebUI.
452
+ Drive native host UI from your WebUI.
124
453
 
125
- ```ts
126
- await sdk.ui.showNotification('Export finished', 'success');
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
- const ok = await sdk.ui.showConfirm('Delete record', 'This cannot be undone. Continue?');
462
+ ```ts
463
+ const ok = await sdk.ui.showConfirm('Delete', 'This cannot be undone.');
129
464
  if (!ok) return;
130
465
 
131
- const path = await sdk.ui.pickFile({ type: 'image' });
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
- **When to use**: Persist filters, layout, or drafts for your panel on the **host** so it behaves like the rest of the desktop app—not only `localStorage` inside the WebView.
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:filters';
489
+ const KEY = 'my-app:draft';
144
490
 
145
- await sdk.storage.set(KEY, { projectId: 'p1', sort: 'date' });
146
- const filters = await sdk.storage.get<{ projectId: string; sort: string }>(KEY);
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
- ### `sdk.tools` / `sdk.skills`
496
+ Storage keys are namespaced per tool instance on the host side.
497
+
498
+ ---
499
+
500
+ ### `sdk.tools`
151
501
 
152
- **When to use**: A one-click flow in your panel chains **other installed tools** in order; or you show a form so the user fills variables and runs a **skill** (a skill may orchestrate several tools). Use `executeWithConfirm` for risky steps so the host shows a confirm dialog first.
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
- setToolPicker(tools.filter((t) => t.id !== sdk.tool.getInfo().id));
512
+ const result = await sdk.tools.execute('fetch-doc', { url: 'https://...' });
513
+ if (!result.success) throw new Error(result.error);
514
+ ```
157
515
 
158
- const step1 = await sdk.tools.execute('fetch-doc', { url });
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
- // High-risk actions: confirm in the host first
163
- await sdk.tools.executeWithConfirm('delete-backup', { id: backupId });
518
+ ---
164
519
 
165
- const skills = await sdk.skills.list();
166
- const skillResult = await sdk.skills.execute('weekly-report-skill', {
167
- week: '2026-W13',
168
- department: 'sales',
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
- ## Architecture
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
- Your App (React/Vue/Vanilla)
176
- │ import { ChatableX } from 'chatablex-web-sdk'
177
-
178
-
179
- ┌─────────────────────────────────────┐
180
- │ chatablex-web-sdk (this package) │
181
- │ │
182
- │ Bridge layer: │
183
- │ JS → Flutter: ChatableXBridge │
184
- │ Flutter → JS: ChatableXReceive │
185
- │ │
186
- │ Modules: tool, events, ai, ui, │
187
- │ storage, tools, skills │
188
- └──────────────┬──────────────────────┘
189
- │ WebView Bridge
190
-
191
- ┌─────────────────────────────────────┐
192
- │ ChatableX Flutter Client │
193
- │ (owns chat UI, SSE stream, agent) │
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