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 CHANGED
@@ -1,199 +1,844 @@
1
- # chatablex-web-sdk
1
+ # ChatableX Web SDK
2
+
3
+ [![license](https://img.shields.io/npm/l/chatablex-web-sdk.svg?style=flat-square)](https://github.com/chatablex/chatablex-web-sdk/blob/main/package.json)
4
+ [![npm version](https://img.shields.io/npm/v/chatablex-web-sdk.svg?style=flat-square)](https://www.npmjs.com/package/chatablex-web-sdk)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/chatablex/chatablex-web-sdk/ci.yml?branch=main&label=Build%20%26%20Test&logo=github)](https://github.com/chatablex/chatablex-web-sdk/actions/workflows/ci.yml)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/chatablex/chatablex-web-sdk/pulls)
2
8
 
3
9
  English | [**简体中文**](README.zh-CN.md)
4
10
 
5
- Runtime SDK for building **ChatableX AI App** WebUI applications.
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
- 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 dependency — the platform does **not** inject it for you.
70
+ ```bash
71
+ npm install chatablex-web-sdk
72
+ ```
8
73
 
9
- ## Install
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
- ```tsx
93
+ Minimal integration — handle LLM tool calls in your WebUI:
94
+
95
+ ```ts
20
96
  import { ChatableX } from 'chatablex-web-sdk';
21
97
 
22
- // Initialize connects to the Flutter WebView host
23
- const sdk = await ChatableX.init({ appId: 'my-app', debug: true });
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
- // 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
- });
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
- ## What are the SDK namespaces for?
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
- 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**.
201
+ ---
36
202
 
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.
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
- | 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. |
230
+ ### Bridge protocol
48
231
 
49
- The **API** sections below each include: a **typical scenario** (when to use it) + **example code** (how to wire it up).
232
+ **Request (JS Flutter):**
50
233
 
51
- ## API
234
+ ```json
235
+ {
236
+ "id": "ctx_1_1718200000000",
237
+ "method": "storage.get",
238
+ "params": { "key": "filters" },
239
+ "timestamp": 1718200000000
240
+ }
241
+ ```
52
242
 
53
- ### `ChatableX.init(config)`
243
+ **Response (Flutter → JS):**
54
244
 
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. |
245
+ ```json
246
+ {
247
+ "type": "response",
248
+ "id": "ctx_1_1718200000000",
249
+ "success": true,
250
+ "data": { "projectId": "p1" }
251
+ }
252
+ ```
60
253
 
61
- Returns `Promise<ChatableXSDK>`.
254
+ **Event push (Flutter → JS):**
62
255
 
63
- ### `sdk.tool`
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
- **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.
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 { action, rowId } = params as { action?: string; rowId?: string };
70
- if (action === 'delete') {
71
- await deleteRow(rowId);
72
- return { success: true, message: 'Deleted' };
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
- // Metadata from manifest (filled in by the host after handshake)
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
- **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.
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
- ```ts
86
- const unsubUser = sdk.events.onUserMessage(({ message }) => {
87
- appendActivityFeed(`User: ${message}`);
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
- const unsubStream = sdk.events.on('streamingContent', ({ content, finished }) => {
91
- setPartialReply(content);
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
- const unsubAi = sdk.events.onAiResponse((data) => {
96
- setLastReply(data.content);
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
- You can also subscribe to `toolExecution`, `close`, etc. (depends on host support).
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
- **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.
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 user message in three bullets', {
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
- 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 });
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
- **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.
459
+ Drive native host UI from your WebUI.
124
460
 
125
- ```ts
126
- await sdk.ui.showNotification('Export finished', 'success');
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
- const ok = await sdk.ui.showConfirm('Delete record', 'This cannot be undone. Continue?');
469
+ ```ts
470
+ const ok = await sdk.ui.showConfirm('Delete', 'This cannot be undone.');
129
471
  if (!ok) return;
130
472
 
131
- const path = await sdk.ui.pickFile({ type: 'image' });
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
- **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.
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:filters';
496
+ const KEY = 'my-app:draft';
144
497
 
145
- await sdk.storage.set(KEY, { projectId: 'p1', sort: 'date' });
146
- const filters = await sdk.storage.get<{ projectId: string; sort: string }>(KEY);
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
- ### `sdk.tools` / `sdk.skills`
503
+ Storage keys are namespaced per tool instance on the host side.
151
504
 
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.
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
- setToolPicker(tools.filter((t) => t.id !== sdk.tool.getInfo().id));
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
- 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 });
527
+ ### `sdk.platform`
161
528
 
162
- // High-risk actions: confirm in the host first
163
- await sdk.tools.executeWithConfirm('delete-backup', { id: backupId });
529
+ Platform-level utilities.
164
530
 
165
- const skills = await sdk.skills.list();
166
- const skillResult = await sdk.skills.execute('weekly-report-skill', {
167
- week: '2026-W13',
168
- department: 'sales',
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
- ## Architecture
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
- 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
- └─────────────────────────────────────┘
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