@workerdeck/react 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tobias Strebitzer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @workerdeck/react
2
+
3
+ Headless React layer for WorkerDeck: the `useClaudeSession` hook plus a pure transcript
4
+ reducer. No styling opinion — bring your own rendering, or use
5
+ [`@workerdeck/ui`](https://www.npmjs.com/package/@workerdeck/ui), the styled layer on top.
6
+
7
+ Part of [WorkerDeck](https://github.com/workerdeck/workerdeck). It sits between
8
+ [`@workerdeck/client`](https://www.npmjs.com/package/@workerdeck/client) (REST + WebSocket
9
+ attach) and your components: the hook attaches to a session, folds the event stream through the
10
+ reducer, and hands back live state plus the control surface (send, approve/deny, interrupt,
11
+ permission mode, model).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @workerdeck/react @workerdeck/client
17
+ ```
18
+
19
+ `react` is a peer dependency (`^18 || ^19`).
20
+
21
+ ## Usage
22
+
23
+ ```tsx
24
+ import { WorkerDeckClient } from '@workerdeck/client'
25
+ import { useClaudeSession } from '@workerdeck/react'
26
+
27
+ const client = new WorkerDeckClient({ baseUrl: 'http://127.0.0.1:8787/v1' })
28
+
29
+ function Panel({ sessionId }: { sessionId: string }) {
30
+ const { state, connected, send, approve, deny, interrupt } = useClaudeSession(client, sessionId)
31
+
32
+ return (
33
+ <div>
34
+ <header>{state.status} {state.model} {connected ? '' : '(reconnecting)'}</header>
35
+ {state.items.map((item) =>
36
+ item.kind === 'assistant_text' ? <p key={item.id}>{item.text}</p> : null,
37
+ )}
38
+ {state.pendingApprovals.map((req) => (
39
+ <div key={req.id}>
40
+ {req.toolName}
41
+ <button onClick={() => approve(req.id)}>Allow</button>
42
+ <button onClick={() => deny(req.id)}>Deny</button>
43
+ </div>
44
+ ))}
45
+ <input onKeyDown={(e) => e.key === 'Enter' && send(e.currentTarget.value)} />
46
+ </div>
47
+ )
48
+ }
49
+ ```
50
+
51
+ The hook attaches on mount, detaches on unmount, and survives reconnects — the underlying handle
52
+ replays from the last seen seq, and the reducer ignores anything it has already applied.
53
+
54
+ ### The transcript reducer, standalone
55
+
56
+ The state machine is framework-free and exported directly — usable in tests, workers, or any
57
+ non-React consumer of the event stream:
58
+
59
+ ```ts
60
+ import { applyEvent, initialTranscriptState, seedFromSessionInfo } from '@workerdeck/react'
61
+
62
+ let state = initialTranscriptState
63
+ state = seedFromSessionInfo(state, sessionInfo) // optional: seed from the attach snapshot
64
+ for (const event of events) state = applyEvent(state, event)
65
+ ```
66
+
67
+ `seedFromSessionInfo` fills fields (status, model, permission mode) a promptless session's event
68
+ stream doesn't carry yet; events stay authoritative once they arrive.
69
+
70
+ ## What the state contains
71
+
72
+ `TranscriptState` is everything a session panel needs to render:
73
+
74
+ - `items` — the ordered transcript: `user`, `assistant_text` (with a `streaming` flag),
75
+ `thinking`, `tool_call` (input + eventual result), `turn_result`, and `notice` items.
76
+ Streaming deltas accumulate in-place and are superseded by the full assistant message.
77
+ - `pendingApprovals` — permission requests awaiting an approve/deny decision.
78
+ - `status` / `statusDetail`, `model`, `cwd`, `sdkSessionId`, `permissionMode`.
79
+ - `models` and `commands` — what the session can switch to / accepts (from `capabilities`).
80
+ - `contextUsage`, `rateLimits` (keyed by window; absent for API-key sessions — render nothing,
81
+ not 0%), `totalCostUsd` (session-cumulative), and `lastSeq` for replay dedupe.
82
+
83
+ The reducer is pure and immutable: same events in, same state out — which is also how it is
84
+ unit-tested. Keep rendering logic out of it.
85
+
86
+ ## Running tool calls in the tab
87
+
88
+ A provider-engine session can ask the *browser* to execute a sandboxed tool call, so documents the
89
+ user holds locally never reach the server. `useToolCallHost` answers those requests from a mounted
90
+ component, running the code in a QuickJS guest
91
+ ([`@workerdeck/sandbox`](https://www.npmjs.com/package/@workerdeck/sandbox), loaded on
92
+ demand):
93
+
94
+ ```tsx
95
+ const { handle } = useClaudeSession(client, sessionId)
96
+ const { executions } = useToolCallHost(handle, {
97
+ tools: ['eval_script'], // allowlist — anything else the server asks for is refused
98
+ timeoutMs: 15_000,
99
+ fetchText: (url) => myGatedFetch(url), // omit and the guest has no network at all
100
+ })
101
+ ```
102
+
103
+ The host must ride **the hook's own `handle`** — the server bridges each call to the first attached
104
+ client, so a second, separately-created handle would sit idle while the real one gets the requests.
105
+ `createToolCallHost` is the same logic without React. Results returned from a tab are untrusted
106
+ input by construction: fine for the user's own data, never a source of server-authoritative state.
107
+
108
+ ## License
109
+
110
+ MIT © Tobias Strebitzer — see
111
+ [LICENSE](https://github.com/workerdeck/workerdeck/blob/master/LICENSE).
@@ -0,0 +1,197 @@
1
+ import { SessionHandle, WorkerDeckClient } from "@workerdeck/client";
2
+ import { ContextUsage, ModelOption, PermissionMode, PermissionRequest, ProfileEngine, RateLimitInfo, SessionEvent, SessionInfo, SessionStatus, SlashCommandInfo, ToolExecutionBackend } from "@workerdeck/protocol";
3
+ import { RunScriptResult, SandboxEngine, SandboxVfs } from "@workerdeck/sandbox";
4
+
5
+ //#region src/transcript.d.ts
6
+ /**
7
+ * Pure transcript state machine over the wire-protocol event stream. Framework-free
8
+ * so it can be unit-tested and reused outside React.
9
+ */
10
+ type TranscriptItem = {
11
+ kind: 'user';
12
+ id: string;
13
+ text: string;
14
+ } | {
15
+ kind: 'assistant_text';
16
+ id: string;
17
+ text: string;
18
+ streaming: boolean;
19
+ parentToolUseId: string | null;
20
+ } | {
21
+ kind: 'thinking';
22
+ id: string;
23
+ text: string;
24
+ parentToolUseId: string | null;
25
+ } | {
26
+ kind: 'tool_call';
27
+ id: string;
28
+ name: string;
29
+ input: unknown;
30
+ parentToolUseId: string | null;
31
+ /**
32
+ * - `running` — the model called it; execution has not been reported
33
+ * - `pending` — dispatched to an executor (bridged to this client, queued)
34
+ * - `deferred` — parked beyond this turn; may outlive the session's liveness
35
+ * - `settled` / `failed` — terminal
36
+ *
37
+ * Derive UI from this, not from `result` being present: a pending or
38
+ * deferred call has no result yet and is not the same as a running one.
39
+ */
40
+ status: 'running' | 'pending' | 'deferred' | 'settled' | 'failed';
41
+ result?: {
42
+ text: string;
43
+ isError: boolean;
44
+ }; /** Correlation id when this call is executed outside the model loop. */
45
+ executionId?: string; /** Which backend is executing it, when known. */
46
+ backend?: ToolExecutionBackend; /** Logs captured by the executor (guest console output). */
47
+ logs?: string[];
48
+ } | {
49
+ kind: 'turn_result';
50
+ id: string;
51
+ subtype: string;
52
+ isError: boolean;
53
+ durationMs: number;
54
+ totalCostUsd: number;
55
+ errors?: string[];
56
+ } | {
57
+ kind: 'notice';
58
+ id: string;
59
+ level: 'info' | 'error';
60
+ text: string;
61
+ }
62
+ /** The agent handed over a session file (`file_delivered`). Render a download
63
+ * card; the file is served by GET /sessions/:id/files/<path> while the
64
+ * session lives. */
65
+ | {
66
+ kind: 'file_delivered';
67
+ id: string;
68
+ path: string;
69
+ bytes: number;
70
+ description?: string;
71
+ };
72
+ type TranscriptState = {
73
+ status: SessionStatus;
74
+ statusDetail?: string;
75
+ model?: string;
76
+ cwd?: string;
77
+ sdkSessionId?: string;
78
+ /** Engine running the session, from the attach snapshot. Gates CLI-only
79
+ * affordances; absent (an older server) reads as 'claude'. */
80
+ engine?: ProfileEngine; /** Models the session can switch to (from the `capabilities` event). */
81
+ models?: ModelOption[]; /** Slash commands the CLI accepts (from the `capabilities` event). */
82
+ commands?: SlashCommandInfo[]; /** Seeded from `system_init`, updated on `permission_mode_changed`. */
83
+ permissionMode?: PermissionMode; /** Latest context-window snapshot; absent until the first turn completes. */
84
+ contextUsage?: ContextUsage;
85
+ /** Latest rate-limit snapshot per window ('five_hour', 'seven_day', ...).
86
+ * Absent for API-key sessions — render nothing, not 0%. */
87
+ rateLimits?: Record<string, RateLimitInfo>;
88
+ items: TranscriptItem[];
89
+ pendingApprovals: PermissionRequest[];
90
+ totalCostUsd: number;
91
+ lastSeq: number;
92
+ };
93
+ declare const initialTranscriptState: TranscriptState;
94
+ /**
95
+ * Seed transcript state from the attach snapshot (the `attached` frame's SessionInfo).
96
+ * A promptless session emits no `system_init` until its first message, so fields like
97
+ * `permissionMode` and `model` would otherwise stay empty — fill only what events
98
+ * haven't set yet; the event stream stays authoritative.
99
+ */
100
+ declare function seedFromSessionInfo(state: TranscriptState, info: SessionInfo): TranscriptState;
101
+ declare function applyEvent(state: TranscriptState, event: SessionEvent): TranscriptState;
102
+ //#endregion
103
+ //#region src/use-session.d.ts
104
+ type UseClaudeSessionOptions = {
105
+ /** Called when the server rejects a command with a protocol_error frame — e.g. a
106
+ * permission-mode switch the CLI refuses. Without a handler these are dropped
107
+ * silently and the UI looks like "nothing happened". */
108
+ onProtocolError?: (message: string) => void;
109
+ };
110
+ type UseClaudeSessionResult = {
111
+ state: TranscriptState;
112
+ connected: boolean;
113
+ /** The live attach handle, for wiring companions that must ride the SAME
114
+ * socket — e.g. useToolCallHost: the bridge asks the first attached client,
115
+ * so a host on a second handle would never see the requests. Undefined until
116
+ * attached and after unmount. */
117
+ handle: SessionHandle | undefined;
118
+ send: (text: string) => void;
119
+ approve: (requestId: string, updatedInput?: Record<string, unknown>) => void;
120
+ deny: (requestId: string, message?: string) => void;
121
+ interrupt: () => void;
122
+ setPermissionMode: (mode: PermissionMode) => void;
123
+ setModel: (model?: string) => void;
124
+ closeSession: () => void;
125
+ };
126
+ /** Attach to a session and maintain live transcript state. Detaches on unmount. */
127
+ declare function useClaudeSession(client: WorkerDeckClient, sessionId: string | undefined, options?: UseClaudeSessionOptions): UseClaudeSessionResult;
128
+ //#endregion
129
+ //#region src/tool-host.d.ts
130
+ /** What the host was asked to do and how it went (for UI/telemetry). */
131
+ type ToolHostExecution = {
132
+ executionId: string;
133
+ toolName: string;
134
+ status: 'running' | 'settled' | 'failed' | 'canceled';
135
+ reason?: string;
136
+ startedAt: number;
137
+ endedAt?: number;
138
+ };
139
+ type ToolHostRunner = (request: {
140
+ script: string;
141
+ vfs: SandboxVfs;
142
+ timeoutMs: number;
143
+ memoryLimitBytes: number;
144
+ signal: AbortSignal;
145
+ }) => Promise<RunScriptResult>;
146
+ type ToolCallHostOptions = {
147
+ /** Tools this client will execute. Anything else is refused, so a server can
148
+ * never talk this tab into running something it didn't opt into.
149
+ * Default: `['eval_script']`. */
150
+ tools?: string[]; /** Guest wall-clock limit, unless the request asks for less. Default 5000. */
151
+ timeoutMs?: number; /** Guest allocator cap, unless the request asks for less. Default 64 MiB. */
152
+ memoryLimitBytes?: number;
153
+ /**
154
+ * Load the WASM guest engine. Called at most once, on the first bridged call
155
+ * — nothing is downloaded or parsed until a session actually bridges one.
156
+ * Defaults to `@workerdeck/sandbox` with the single-file browser build.
157
+ */
158
+ loadEngine?: () => Promise<SandboxEngine>;
159
+ /**
160
+ * Run the script. Defaults to executing on this thread, which is fine for the
161
+ * short, time-boxed evaluations this is built for. Supply your own (a Web
162
+ * Worker running the same engine) to keep long evaluations off the UI thread
163
+ * — the guest deadline preempts the interpreter, but only between bytecode
164
+ * ops on whichever thread it runs on.
165
+ */
166
+ execute?: ToolHostRunner; /** Host-gated fetch for the guest. Omitted = the guest has no network at all. */
167
+ fetchText?: (url: string) => Promise<string>; /** Observe executions (rendering, logging). */
168
+ onExecution?: (execution: ToolHostExecution) => void;
169
+ };
170
+ /**
171
+ * Answers server-bridged tool calls by executing them in this browser tab.
172
+ * Framework-free — {@link useToolCallHost} is a thin React wrapper.
173
+ *
174
+ * The point is data locality: documents fetched or held client-side can be
175
+ * evaluated here and never touch the server. The engine loads lazily, so a page
176
+ * that never bridges a call never pays for the WASM guest.
177
+ */
178
+ declare function createToolCallHost(handle: SessionHandle, options?: ToolCallHostOptions): {
179
+ dispose: () => void;
180
+ };
181
+ //#endregion
182
+ //#region src/use-tool-host.d.ts
183
+ type UseToolCallHostOptions = ToolCallHostOptions & {
184
+ /** Turn the host off without unmounting. Default true. */enabled?: boolean; /** How many recent executions to keep for rendering. Default 50. */
185
+ historyLimit?: number;
186
+ };
187
+ /**
188
+ * React wrapper around {@link createToolCallHost}: subscribes while mounted and
189
+ * exposes recent executions for rendering. All the logic lives in the
190
+ * framework-free host — this only manages the subscription's lifetime.
191
+ */
192
+ declare function useToolCallHost(handle: SessionHandle | undefined, options?: UseToolCallHostOptions): {
193
+ executions: ToolHostExecution[];
194
+ };
195
+ //#endregion
196
+ export { type ToolCallHostOptions, type ToolHostExecution, type ToolHostRunner, type TranscriptItem, type TranscriptState, type UseClaudeSessionResult, type UseToolCallHostOptions, applyEvent, createToolCallHost, initialTranscriptState, seedFromSessionInfo, useClaudeSession, useToolCallHost };
197
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,520 @@
1
+ import { useEffect, useMemo, useReducer, useRef, useState } from "react";
2
+ //#region src/transcript.ts
3
+ const initialTranscriptState = {
4
+ status: "starting",
5
+ items: [],
6
+ pendingApprovals: [],
7
+ totalCostUsd: 0,
8
+ lastSeq: 0
9
+ };
10
+ const STREAMING_ID = "streaming";
11
+ const STREAMING_THINKING_ID = "streaming-thinking";
12
+ function blockText(content) {
13
+ if (content === void 0) return "";
14
+ if (typeof content === "string") return content;
15
+ return content.map((part) => typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n");
16
+ }
17
+ function contentToBlocks(content) {
18
+ return typeof content === "string" ? [{
19
+ type: "text",
20
+ text: content
21
+ }] : content;
22
+ }
23
+ /** Render an execution's by-value output for the transcript. */
24
+ function outputText(output) {
25
+ if (output.type === "text") return output.value;
26
+ try {
27
+ return JSON.stringify(output.value);
28
+ } catch {
29
+ return String(output.value);
30
+ }
31
+ }
32
+ /** CLI-side command output arrives as user text wrapped in local-command tags. */
33
+ const LOCAL_COMMAND_OUTPUT = /^<local-command-(stdout|stderr)>([\s\S]*?)<\/local-command-\1>$/;
34
+ function upsert(items, item) {
35
+ const index = items.findIndex((existing) => existing.id === item.id && existing.kind === item.kind);
36
+ if (index === -1) return [...items, item];
37
+ const next = [...items];
38
+ next[index] = item;
39
+ return next;
40
+ }
41
+ /**
42
+ * Seed transcript state from the attach snapshot (the `attached` frame's SessionInfo).
43
+ * A promptless session emits no `system_init` until its first message, so fields like
44
+ * `permissionMode` and `model` would otherwise stay empty — fill only what events
45
+ * haven't set yet; the event stream stays authoritative.
46
+ */
47
+ function seedFromSessionInfo(state, info) {
48
+ return {
49
+ ...state,
50
+ status: state.lastSeq === 0 ? info.status : state.status,
51
+ model: state.model ?? info.model,
52
+ permissionMode: state.permissionMode ?? info.permissionMode,
53
+ cwd: state.cwd ?? info.cwd,
54
+ sdkSessionId: state.sdkSessionId ?? info.sdkSessionId,
55
+ engine: info.engine ?? state.engine
56
+ };
57
+ }
58
+ function applyEvent(state, event) {
59
+ if (event.seq <= state.lastSeq) return state;
60
+ const base = {
61
+ ...state,
62
+ lastSeq: event.seq
63
+ };
64
+ switch (event.type) {
65
+ case "system_init": return {
66
+ ...base,
67
+ model: event.model,
68
+ cwd: event.cwd,
69
+ sdkSessionId: event.sdkSessionId,
70
+ permissionMode: event.permissionMode
71
+ };
72
+ case "status_changed": return {
73
+ ...base,
74
+ status: event.status,
75
+ statusDetail: event.detail
76
+ };
77
+ case "capabilities": return {
78
+ ...base,
79
+ models: event.models,
80
+ commands: event.commands
81
+ };
82
+ case "model_changed": return event.model === void 0 ? base : {
83
+ ...base,
84
+ model: event.model
85
+ };
86
+ case "permission_mode_changed": return {
87
+ ...base,
88
+ permissionMode: event.mode
89
+ };
90
+ case "context_usage": return {
91
+ ...base,
92
+ contextUsage: event.usage
93
+ };
94
+ case "rate_limit": {
95
+ const key = event.info.rateLimitType;
96
+ if (!key) return base;
97
+ return {
98
+ ...base,
99
+ rateLimits: {
100
+ ...base.rateLimits,
101
+ [key]: event.info
102
+ }
103
+ };
104
+ }
105
+ case "user_message": {
106
+ let items = base.items;
107
+ for (const block of contentToBlocks(event.message.content)) if (block.type === "tool_result") {
108
+ const toolResult = block;
109
+ const isError = toolResult.is_error === true;
110
+ items = items.map((item) => item.kind === "tool_call" && item.id === toolResult.tool_use_id ? {
111
+ ...item,
112
+ status: isError ? "failed" : "settled",
113
+ result: {
114
+ text: blockText(toolResult.content),
115
+ isError
116
+ }
117
+ } : item);
118
+ } else if (block.type === "text" && !event.synthetic) {
119
+ const text = block.text;
120
+ const localOutput = LOCAL_COMMAND_OUTPUT.exec(text.trim());
121
+ if (localOutput) items = upsert(items, {
122
+ kind: "notice",
123
+ id: event.uuid ?? `user-${event.seq}`,
124
+ level: localOutput[1] === "stderr" ? "error" : "info",
125
+ text: localOutput[2].trim()
126
+ });
127
+ else items = upsert(items, {
128
+ kind: "user",
129
+ id: event.uuid ?? `user-${event.seq}`,
130
+ text
131
+ });
132
+ }
133
+ return {
134
+ ...base,
135
+ items
136
+ };
137
+ }
138
+ case "assistant_message": {
139
+ let streamedThinking = base.items.find((item) => item.kind === "thinking" && item.id === STREAMING_THINKING_ID)?.text ?? "";
140
+ let items = base.items.filter((item) => !(item.kind === "assistant_text" && item.id === STREAMING_ID) && !(item.kind === "thinking" && item.id === STREAMING_THINKING_ID));
141
+ contentToBlocks(event.message.content).forEach((block, index) => {
142
+ const id = `${event.uuid}-${index}`;
143
+ if (block.type === "text") items = upsert(items, {
144
+ kind: "assistant_text",
145
+ id,
146
+ text: block.text,
147
+ streaming: false,
148
+ parentToolUseId: event.parentToolUseId
149
+ });
150
+ else if (block.type === "thinking") {
151
+ const text = block.thinking || streamedThinking;
152
+ streamedThinking = "";
153
+ if (text.trim() === "") return;
154
+ items = upsert(items, {
155
+ kind: "thinking",
156
+ id,
157
+ text,
158
+ parentToolUseId: event.parentToolUseId
159
+ });
160
+ } else if (block.type === "tool_use") {
161
+ const toolUse = block;
162
+ items = upsert(items, {
163
+ kind: "tool_call",
164
+ id: toolUse.id,
165
+ name: toolUse.name,
166
+ input: toolUse.input,
167
+ parentToolUseId: event.parentToolUseId,
168
+ status: "running"
169
+ });
170
+ }
171
+ });
172
+ return {
173
+ ...base,
174
+ items
175
+ };
176
+ }
177
+ case "stream_delta": {
178
+ const delta = event.event;
179
+ if (delta.type !== "content_block_delta") return base;
180
+ if (delta.delta?.type === "text_delta") {
181
+ const item = {
182
+ kind: "assistant_text",
183
+ id: STREAMING_ID,
184
+ text: (base.items.find((item) => item.kind === "assistant_text" && item.id === STREAMING_ID)?.text ?? "") + (delta.delta.text ?? ""),
185
+ streaming: true,
186
+ parentToolUseId: event.parentToolUseId
187
+ };
188
+ return {
189
+ ...base,
190
+ items: upsert(base.items, item)
191
+ };
192
+ }
193
+ if (delta.delta?.type === "thinking_delta") {
194
+ const item = {
195
+ kind: "thinking",
196
+ id: STREAMING_THINKING_ID,
197
+ text: (base.items.find((item) => item.kind === "thinking" && item.id === STREAMING_THINKING_ID)?.text ?? "") + (delta.delta.thinking ?? ""),
198
+ parentToolUseId: event.parentToolUseId
199
+ };
200
+ return {
201
+ ...base,
202
+ items: upsert(base.items, item)
203
+ };
204
+ }
205
+ return base;
206
+ }
207
+ case "turn_result": return {
208
+ ...base,
209
+ totalCostUsd: event.totalCostUsd,
210
+ items: [...base.items, {
211
+ kind: "turn_result",
212
+ id: `turn-${event.seq}`,
213
+ subtype: event.subtype,
214
+ isError: event.isError,
215
+ durationMs: event.durationMs,
216
+ totalCostUsd: event.totalCostUsd,
217
+ errors: event.errors
218
+ }]
219
+ };
220
+ case "permission_requested": return {
221
+ ...base,
222
+ pendingApprovals: [...base.pendingApprovals, event.request]
223
+ };
224
+ case "permission_resolved": return {
225
+ ...base,
226
+ pendingApprovals: base.pendingApprovals.filter((r) => r.id !== event.requestId)
227
+ };
228
+ case "execution_dispatched": return {
229
+ ...base,
230
+ items: base.items.map((item) => item.kind === "tool_call" && item.id === event.executionId ? {
231
+ ...item,
232
+ status: event.deferred ? "deferred" : "pending",
233
+ executionId: event.executionId,
234
+ backend: event.backend
235
+ } : item)
236
+ };
237
+ case "execution_result": return {
238
+ ...base,
239
+ items: base.items.map((item) => item.kind === "tool_call" && item.id === event.executionId ? {
240
+ ...item,
241
+ status: "settled",
242
+ executionId: event.executionId,
243
+ result: {
244
+ text: outputText(event.output),
245
+ isError: false
246
+ },
247
+ logs: event.logs ?? item.logs
248
+ } : item)
249
+ };
250
+ case "execution_failed": return {
251
+ ...base,
252
+ items: base.items.map((item) => item.kind === "tool_call" && item.id === event.executionId ? {
253
+ ...item,
254
+ status: "failed",
255
+ executionId: event.executionId,
256
+ result: {
257
+ text: `${event.reason}: ${event.error}`,
258
+ isError: true
259
+ },
260
+ logs: event.logs ?? item.logs
261
+ } : item)
262
+ };
263
+ case "file_delivered": return {
264
+ ...base,
265
+ items: [...base.items, {
266
+ kind: "file_delivered",
267
+ id: `file-${event.seq}`,
268
+ path: event.path,
269
+ bytes: event.bytes,
270
+ description: event.description
271
+ }]
272
+ };
273
+ case "session_error": return {
274
+ ...base,
275
+ items: [...base.items, {
276
+ kind: "notice",
277
+ id: `err-${event.seq}`,
278
+ level: "error",
279
+ text: event.message
280
+ }]
281
+ };
282
+ case "session_closed": return {
283
+ ...base,
284
+ items: [...base.items, {
285
+ kind: "notice",
286
+ id: `closed-${event.seq}`,
287
+ level: "info",
288
+ text: `Session closed (${event.reason})`
289
+ }]
290
+ };
291
+ default: return base;
292
+ }
293
+ }
294
+ //#endregion
295
+ //#region src/use-session.ts
296
+ /** Session events drive the reducer; the attach snapshot seeds fields (permission
297
+ * mode, model) that a promptless session's event stream doesn't carry yet. */
298
+ function reduce(state, action) {
299
+ return action.type === "attached" ? seedFromSessionInfo(state, action.session) : applyEvent(state, action);
300
+ }
301
+ /** Attach to a session and maintain live transcript state. Detaches on unmount. */
302
+ function useClaudeSession(client, sessionId, options) {
303
+ const [state, dispatch] = useReducer(reduce, initialTranscriptState);
304
+ const [connected, setConnected] = useState(false);
305
+ const [handleState, setHandleState] = useState();
306
+ const handleRef = useRef(null);
307
+ const onProtocolErrorRef = useRef(options?.onProtocolError);
308
+ onProtocolErrorRef.current = options?.onProtocolError;
309
+ useEffect(() => {
310
+ if (!sessionId) return;
311
+ const handle = client.attach(sessionId);
312
+ handleRef.current = handle;
313
+ setHandleState(handle);
314
+ const offEvent = handle.on("event", (event) => dispatch(event));
315
+ const offAttached = handle.on("attached", (frame) => dispatch(frame));
316
+ const offConn = handle.on("connectionChange", setConnected);
317
+ const offProtocolError = handle.on("protocolError", (message) => {
318
+ onProtocolErrorRef.current?.(message);
319
+ });
320
+ return () => {
321
+ offEvent();
322
+ offAttached();
323
+ offConn();
324
+ offProtocolError();
325
+ handle.detach();
326
+ handleRef.current = null;
327
+ setHandleState(void 0);
328
+ };
329
+ }, [client, sessionId]);
330
+ return useMemo(() => ({
331
+ state,
332
+ connected,
333
+ handle: handleState,
334
+ send: (text) => handleRef.current?.send(text),
335
+ approve: (requestId, updatedInput) => handleRef.current?.approve(requestId, updatedInput),
336
+ deny: (requestId, message) => handleRef.current?.deny(requestId, message),
337
+ interrupt: () => handleRef.current?.interrupt(),
338
+ setPermissionMode: (mode) => handleRef.current?.setPermissionMode(mode),
339
+ setModel: (model) => handleRef.current?.setModel(model),
340
+ closeSession: () => handleRef.current?.closeSession()
341
+ }), [
342
+ state,
343
+ connected,
344
+ handleState
345
+ ]);
346
+ }
347
+ //#endregion
348
+ //#region src/tool-host.ts
349
+ /**
350
+ * Answers server-bridged tool calls by executing them in this browser tab.
351
+ * Framework-free — {@link useToolCallHost} is a thin React wrapper.
352
+ *
353
+ * The point is data locality: documents fetched or held client-side can be
354
+ * evaluated here and never touch the server. The engine loads lazily, so a page
355
+ * that never bridges a call never pays for the WASM guest.
356
+ */
357
+ function createToolCallHost(handle, options = {}) {
358
+ const inFlight = /* @__PURE__ */ new Map();
359
+ let enginePromise;
360
+ let disposed = false;
361
+ const track = (execution) => options.onExecution?.(execution);
362
+ const refuse = (frame, reason, error, startedAt) => {
363
+ handle.sendToolCallError(frame.executionId, reason, error);
364
+ track({
365
+ executionId: frame.executionId,
366
+ toolName: frame.toolName,
367
+ status: "failed",
368
+ reason,
369
+ startedAt,
370
+ endedAt: Date.now()
371
+ });
372
+ };
373
+ const run = async (frame) => {
374
+ const startedAt = Date.now();
375
+ if (!(options.tools ?? ["eval_script"]).includes(frame.toolName)) {
376
+ refuse(frame, "unsupported_tool", `this client does not execute '${frame.toolName}'`, startedAt);
377
+ return;
378
+ }
379
+ const script = frame.input?.script;
380
+ if (typeof script !== "string") {
381
+ refuse(frame, "invalid_input", "expected a string `script` input", startedAt);
382
+ return;
383
+ }
384
+ const controller = new AbortController();
385
+ inFlight.set(frame.executionId, controller);
386
+ track({
387
+ executionId: frame.executionId,
388
+ toolName: frame.toolName,
389
+ status: "running",
390
+ startedAt
391
+ });
392
+ try {
393
+ const sandbox = await import("@workerdeck/sandbox");
394
+ const vfs = sandbox.createVfs(frame.vfsSeed);
395
+ const timeoutMs = Math.min(frame.limits?.timeoutMs ?? Number.POSITIVE_INFINITY, options.timeoutMs ?? 5e3);
396
+ const memoryLimitBytes = Math.min(frame.limits?.memoryLimitBytes ?? Number.POSITIVE_INFINITY, options.memoryLimitBytes ?? 64 * 1024 * 1024);
397
+ const result = options.execute ? await options.execute({
398
+ script,
399
+ vfs,
400
+ timeoutMs,
401
+ memoryLimitBytes,
402
+ signal: controller.signal
403
+ }) : await (async () => {
404
+ enginePromise ??= (options.loadEngine ?? defaultLoadEngine)();
405
+ return sandbox.runScript(await enginePromise, {
406
+ script,
407
+ vfs,
408
+ timeoutMs,
409
+ memoryLimitBytes,
410
+ signal: controller.signal,
411
+ fetchText: options.fetchText
412
+ });
413
+ })();
414
+ if (disposed || !inFlight.has(frame.executionId)) return;
415
+ const logs = result.logs.map((l) => `[${l.level}] ${l.text}`);
416
+ if (result.ok) {
417
+ handle.sendToolCallResult(frame.executionId, {
418
+ type: "json",
419
+ value: result.value
420
+ }, logs);
421
+ track({
422
+ executionId: frame.executionId,
423
+ toolName: frame.toolName,
424
+ status: "settled",
425
+ startedAt,
426
+ endedAt: Date.now()
427
+ });
428
+ } else {
429
+ handle.sendToolCallError(frame.executionId, result.reason, result.error, logs);
430
+ track({
431
+ executionId: frame.executionId,
432
+ toolName: frame.toolName,
433
+ status: "failed",
434
+ reason: result.reason,
435
+ startedAt,
436
+ endedAt: Date.now()
437
+ });
438
+ }
439
+ } catch (error) {
440
+ if (disposed || !inFlight.has(frame.executionId)) return;
441
+ refuse(frame, "host_error", error instanceof Error ? error.message : String(error), startedAt);
442
+ } finally {
443
+ inFlight.delete(frame.executionId);
444
+ }
445
+ };
446
+ const offRequest = handle.on("toolCallRequest", (frame) => void run(frame));
447
+ const offCancel = handle.on("toolCallCanceled", ({ executionId, reason }) => {
448
+ const controller = inFlight.get(executionId);
449
+ if (!controller) return;
450
+ controller.abort();
451
+ inFlight.delete(executionId);
452
+ track({
453
+ executionId,
454
+ toolName: "",
455
+ status: "canceled",
456
+ reason,
457
+ startedAt: Date.now(),
458
+ endedAt: Date.now()
459
+ });
460
+ });
461
+ return { dispose: () => {
462
+ disposed = true;
463
+ offRequest();
464
+ offCancel();
465
+ for (const controller of inFlight.values()) controller.abort();
466
+ inFlight.clear();
467
+ } };
468
+ }
469
+ /** The single-file browser build keeps this to one lazy chunk — no separate
470
+ * .wasm fetch, and nothing at all until the first bridged call. */
471
+ async function defaultLoadEngine() {
472
+ const [sandbox, variant] = await Promise.all([import("@workerdeck/sandbox"), import("@jitl/quickjs-singlefile-browser-release-asyncify")]);
473
+ return sandbox.loadEngine(variant);
474
+ }
475
+ //#endregion
476
+ //#region src/use-tool-host.ts
477
+ /**
478
+ * React wrapper around {@link createToolCallHost}: subscribes while mounted and
479
+ * exposes recent executions for rendering. All the logic lives in the
480
+ * framework-free host — this only manages the subscription's lifetime.
481
+ */
482
+ function useToolCallHost(handle, options = {}) {
483
+ const [executions, setExecutions] = useState([]);
484
+ const optionsRef = useRef(options);
485
+ optionsRef.current = options;
486
+ useEffect(() => {
487
+ if (!handle || options.enabled === false) return;
488
+ const host = createToolCallHost(handle, {
489
+ get tools() {
490
+ return optionsRef.current.tools;
491
+ },
492
+ get timeoutMs() {
493
+ return optionsRef.current.timeoutMs;
494
+ },
495
+ get memoryLimitBytes() {
496
+ return optionsRef.current.memoryLimitBytes;
497
+ },
498
+ get loadEngine() {
499
+ return optionsRef.current.loadEngine;
500
+ },
501
+ get execute() {
502
+ return optionsRef.current.execute;
503
+ },
504
+ get fetchText() {
505
+ return optionsRef.current.fetchText;
506
+ },
507
+ onExecution: (execution) => {
508
+ optionsRef.current.onExecution?.(execution);
509
+ const limit = optionsRef.current.historyLimit ?? 50;
510
+ setExecutions((prev) => [...prev.filter((e) => e.executionId !== execution.executionId), execution].slice(-limit));
511
+ }
512
+ });
513
+ return () => host.dispose();
514
+ }, [handle, options.enabled]);
515
+ return { executions };
516
+ }
517
+ //#endregion
518
+ export { applyEvent, createToolCallHost, initialTranscriptState, seedFromSessionInfo, useClaudeSession, useToolCallHost };
519
+
520
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/transcript.ts","../src/use-session.ts","../src/tool-host.ts","../src/use-tool-host.ts"],"sourcesContent":["import type {\n ContentBlock,\n ContextUsage,\n ModelOption,\n PermissionMode,\n PermissionRequest,\n ProfileEngine,\n RateLimitInfo,\n SessionEvent,\n SessionInfo,\n SessionStatus,\n SlashCommandInfo,\n ToolExecutionBackend,\n ToolExecutionOutput,\n ToolResultBlock,\n} from '@workerdeck/protocol'\n\n/**\n * Pure transcript state machine over the wire-protocol event stream. Framework-free\n * so it can be unit-tested and reused outside React.\n */\n\nexport type TranscriptItem =\n | { kind: 'user'; id: string; text: string }\n | {\n kind: 'assistant_text'\n id: string\n text: string\n streaming: boolean\n parentToolUseId: string | null\n }\n | { kind: 'thinking'; id: string; text: string; parentToolUseId: string | null }\n | {\n kind: 'tool_call'\n id: string\n name: string\n input: unknown\n parentToolUseId: string | null\n /**\n * - `running` — the model called it; execution has not been reported\n * - `pending` — dispatched to an executor (bridged to this client, queued)\n * - `deferred` — parked beyond this turn; may outlive the session's liveness\n * - `settled` / `failed` — terminal\n *\n * Derive UI from this, not from `result` being present: a pending or\n * deferred call has no result yet and is not the same as a running one.\n */\n status: 'running' | 'pending' | 'deferred' | 'settled' | 'failed'\n result?: { text: string; isError: boolean }\n /** Correlation id when this call is executed outside the model loop. */\n executionId?: string\n /** Which backend is executing it, when known. */\n backend?: ToolExecutionBackend\n /** Logs captured by the executor (guest console output). */\n logs?: string[]\n }\n | {\n kind: 'turn_result'\n id: string\n subtype: string\n isError: boolean\n durationMs: number\n totalCostUsd: number\n errors?: string[]\n }\n | { kind: 'notice'; id: string; level: 'info' | 'error'; text: string }\n /** The agent handed over a session file (`file_delivered`). Render a download\n * card; the file is served by GET /sessions/:id/files/<path> while the\n * session lives. */\n | { kind: 'file_delivered'; id: string; path: string; bytes: number; description?: string }\n\nexport type TranscriptState = {\n status: SessionStatus\n statusDetail?: string\n model?: string\n cwd?: string\n sdkSessionId?: string\n /** Engine running the session, from the attach snapshot. Gates CLI-only\n * affordances; absent (an older server) reads as 'claude'. */\n engine?: ProfileEngine\n /** Models the session can switch to (from the `capabilities` event). */\n models?: ModelOption[]\n /** Slash commands the CLI accepts (from the `capabilities` event). */\n commands?: SlashCommandInfo[]\n /** Seeded from `system_init`, updated on `permission_mode_changed`. */\n permissionMode?: PermissionMode\n /** Latest context-window snapshot; absent until the first turn completes. */\n contextUsage?: ContextUsage\n /** Latest rate-limit snapshot per window ('five_hour', 'seven_day', ...).\n * Absent for API-key sessions — render nothing, not 0%. */\n rateLimits?: Record<string, RateLimitInfo>\n items: TranscriptItem[]\n pendingApprovals: PermissionRequest[]\n totalCostUsd: number\n lastSeq: number\n}\n\nexport const initialTranscriptState: TranscriptState = {\n status: 'starting',\n items: [],\n pendingApprovals: [],\n totalCostUsd: 0,\n lastSeq: 0,\n}\n\nconst STREAMING_ID = 'streaming'\nconst STREAMING_THINKING_ID = 'streaming-thinking'\n\nfunction blockText(content: ToolResultBlock['content']): string {\n if (content === undefined) return ''\n if (typeof content === 'string') return content\n return content\n .map((part) => (typeof part.text === 'string' ? part.text : ''))\n .filter(Boolean)\n .join('\\n')\n}\n\nfunction contentToBlocks(content: string | ContentBlock[]): ContentBlock[] {\n return typeof content === 'string' ? [{ type: 'text', text: content }] : content\n}\n\n/** Render an execution's by-value output for the transcript. */\nfunction outputText(output: ToolExecutionOutput): string {\n if (output.type === 'text') return output.value\n try {\n return JSON.stringify(output.value)\n } catch {\n return String(output.value)\n }\n}\n\n/** CLI-side command output arrives as user text wrapped in local-command tags. */\nconst LOCAL_COMMAND_OUTPUT = /^<local-command-(stdout|stderr)>([\\s\\S]*?)<\\/local-command-\\1>$/\n\nfunction upsert(items: TranscriptItem[], item: TranscriptItem): TranscriptItem[] {\n const index = items.findIndex((existing) => existing.id === item.id && existing.kind === item.kind)\n if (index === -1) return [...items, item]\n const next = [...items]\n next[index] = item\n return next\n}\n\n/**\n * Seed transcript state from the attach snapshot (the `attached` frame's SessionInfo).\n * A promptless session emits no `system_init` until its first message, so fields like\n * `permissionMode` and `model` would otherwise stay empty — fill only what events\n * haven't set yet; the event stream stays authoritative.\n */\nexport function seedFromSessionInfo(state: TranscriptState, info: SessionInfo): TranscriptState {\n return {\n ...state,\n // Before any event has arrived, the snapshot status is fresher than 'starting'.\n status: state.lastSeq === 0 ? info.status : state.status,\n model: state.model ?? info.model,\n permissionMode: state.permissionMode ?? info.permissionMode,\n cwd: state.cwd ?? info.cwd,\n sdkSessionId: state.sdkSessionId ?? info.sdkSessionId,\n // Never changes for a live session, and no event carries it — the snapshot is\n // the only source, so take it whenever it is present.\n engine: info.engine ?? state.engine,\n }\n}\n\nexport function applyEvent(state: TranscriptState, event: SessionEvent): TranscriptState {\n if (event.seq <= state.lastSeq) return state\n const base: TranscriptState = { ...state, lastSeq: event.seq }\n\n switch (event.type) {\n case 'system_init':\n return {\n ...base,\n model: event.model,\n cwd: event.cwd,\n sdkSessionId: event.sdkSessionId,\n permissionMode: event.permissionMode,\n }\n\n case 'status_changed':\n return { ...base, status: event.status, statusDetail: event.detail }\n\n case 'capabilities':\n return { ...base, models: event.models, commands: event.commands }\n\n case 'model_changed':\n // undefined = reset to the server default; keep showing the last known model.\n return event.model === undefined ? base : { ...base, model: event.model }\n\n case 'permission_mode_changed':\n return { ...base, permissionMode: event.mode }\n\n case 'context_usage':\n return { ...base, contextUsage: event.usage }\n\n case 'rate_limit': {\n // Keyed by window so five_hour and seven_day updates don't clobber each other.\n const key = event.info.rateLimitType\n if (!key) return base\n return { ...base, rateLimits: { ...base.rateLimits, [key]: event.info } }\n }\n\n case 'user_message': {\n let items = base.items\n for (const block of contentToBlocks(event.message.content)) {\n if (block.type === 'tool_result') {\n const toolResult = block as ToolResultBlock\n const isError = toolResult.is_error === true\n items = items.map((item) =>\n item.kind === 'tool_call' && item.id === toolResult.tool_use_id\n ? {\n ...item,\n status: isError ? 'failed' : 'settled',\n result: { text: blockText(toolResult.content), isError },\n }\n : item,\n )\n } else if (block.type === 'text' && !event.synthetic) {\n const text = (block as { text: string }).text\n const localOutput = LOCAL_COMMAND_OUTPUT.exec(text.trim())\n if (localOutput) {\n items = upsert(items, {\n kind: 'notice',\n id: event.uuid ?? `user-${event.seq}`,\n level: localOutput[1] === 'stderr' ? 'error' : 'info',\n text: localOutput[2].trim(),\n })\n } else {\n items = upsert(items, {\n kind: 'user',\n id: event.uuid ?? `user-${event.seq}`,\n text,\n })\n }\n }\n }\n return { ...base, items }\n }\n\n case 'assistant_message': {\n // Encrypted thinking arrives as a signature-only block on the final message: `thinking`\n // is '' and the human-readable summary, when the model surfaces one at all, exists only\n // in the thinking_delta stream. Carry the streamed text over rather than let the full\n // message overwrite it with nothing.\n let streamedThinking =\n base.items.find(\n (item): item is Extract<TranscriptItem, { kind: 'thinking' }> =>\n item.kind === 'thinking' && item.id === STREAMING_THINKING_ID,\n )?.text ?? ''\n // The full message supersedes any in-flight streamed text/thinking.\n let items = base.items.filter(\n (item) =>\n !(item.kind === 'assistant_text' && item.id === STREAMING_ID) &&\n !(item.kind === 'thinking' && item.id === STREAMING_THINKING_ID),\n )\n const blocks = contentToBlocks(event.message.content)\n blocks.forEach((block, index) => {\n const id = `${event.uuid}-${index}`\n if (block.type === 'text') {\n items = upsert(items, {\n kind: 'assistant_text',\n id,\n text: (block as { text: string }).text,\n streaming: false,\n parentToolUseId: event.parentToolUseId,\n })\n } else if (block.type === 'thinking') {\n const text = (block as { thinking: string }).thinking || streamedThinking\n // One streamed thought backfills at most one block, so a multi-block message\n // doesn't repeat it.\n streamedThinking = ''\n // No summary anywhere: drop the block instead of leaving a \"Thought process\" row\n // that expands to nothing (and, across consecutive messages, stacks up).\n if (text.trim() === '') return\n items = upsert(items, {\n kind: 'thinking',\n id,\n text,\n parentToolUseId: event.parentToolUseId,\n })\n } else if (block.type === 'tool_use') {\n const toolUse = block as { id: string; name: string; input: unknown }\n items = upsert(items, {\n kind: 'tool_call',\n id: toolUse.id,\n name: toolUse.name,\n input: toolUse.input,\n parentToolUseId: event.parentToolUseId,\n status: 'running',\n })\n }\n })\n return { ...base, items }\n }\n\n case 'stream_delta': {\n const delta = event.event as {\n type: string\n delta?: { type?: string; text?: string; thinking?: string }\n }\n if (delta.type !== 'content_block_delta') return base\n if (delta.delta?.type === 'text_delta') {\n const existing = base.items.find(\n (item): item is Extract<TranscriptItem, { kind: 'assistant_text' }> =>\n item.kind === 'assistant_text' && item.id === STREAMING_ID,\n )\n const item: TranscriptItem = {\n kind: 'assistant_text',\n id: STREAMING_ID,\n text: (existing?.text ?? '') + (delta.delta.text ?? ''),\n streaming: true,\n parentToolUseId: event.parentToolUseId,\n }\n return { ...base, items: upsert(base.items, item) }\n }\n if (delta.delta?.type === 'thinking_delta') {\n const existing = base.items.find(\n (item): item is Extract<TranscriptItem, { kind: 'thinking' }> =>\n item.kind === 'thinking' && item.id === STREAMING_THINKING_ID,\n )\n const item: TranscriptItem = {\n kind: 'thinking',\n id: STREAMING_THINKING_ID,\n text: (existing?.text ?? '') + (delta.delta.thinking ?? ''),\n parentToolUseId: event.parentToolUseId,\n }\n return { ...base, items: upsert(base.items, item) }\n }\n return base\n }\n\n case 'turn_result':\n return {\n ...base,\n // total_cost_usd is session-cumulative on each SDK result message.\n totalCostUsd: event.totalCostUsd,\n items: [\n ...base.items,\n {\n kind: 'turn_result',\n id: `turn-${event.seq}`,\n subtype: event.subtype,\n isError: event.isError,\n durationMs: event.durationMs,\n totalCostUsd: event.totalCostUsd,\n errors: event.errors,\n },\n ],\n }\n\n case 'permission_requested':\n return { ...base, pendingApprovals: [...base.pendingApprovals, event.request] }\n\n case 'permission_resolved':\n return {\n ...base,\n pendingApprovals: base.pendingApprovals.filter((r) => r.id !== event.requestId),\n }\n\n // Execution lifecycle for tool calls that run outside the model loop\n // (bridged to this client, queued, or deferred). Keyed by executionId, which\n // equals the tool_use id for calls the model made. Events for an unknown id\n // are ignored rather than fabricating an item: the tool_use that explains it\n // may simply not have arrived (or belongs to another session).\n case 'execution_dispatched':\n return {\n ...base,\n items: base.items.map((item) =>\n item.kind === 'tool_call' && item.id === event.executionId\n ? {\n ...item,\n status: event.deferred ? 'deferred' : 'pending',\n executionId: event.executionId,\n backend: event.backend,\n }\n : item,\n ),\n }\n\n case 'execution_result':\n return {\n ...base,\n items: base.items.map((item) =>\n item.kind === 'tool_call' && item.id === event.executionId\n ? {\n ...item,\n status: 'settled',\n executionId: event.executionId,\n result: { text: outputText(event.output), isError: false },\n logs: event.logs ?? item.logs,\n }\n : item,\n ),\n }\n\n case 'execution_failed':\n return {\n ...base,\n items: base.items.map((item) =>\n item.kind === 'tool_call' && item.id === event.executionId\n ? {\n ...item,\n status: 'failed',\n executionId: event.executionId,\n result: { text: `${event.reason}: ${event.error}`, isError: true },\n logs: event.logs ?? item.logs,\n }\n : item,\n ),\n }\n\n case 'file_delivered':\n return {\n ...base,\n items: [\n ...base.items,\n {\n kind: 'file_delivered',\n id: `file-${event.seq}`,\n path: event.path,\n bytes: event.bytes,\n description: event.description,\n },\n ],\n }\n\n case 'session_error':\n return {\n ...base,\n items: [\n ...base.items,\n { kind: 'notice', id: `err-${event.seq}`, level: 'error', text: event.message },\n ],\n }\n\n case 'session_closed':\n return {\n ...base,\n items: [\n ...base.items,\n {\n kind: 'notice',\n id: `closed-${event.seq}`,\n level: 'info',\n text: `Session closed (${event.reason})`,\n },\n ],\n }\n\n case 'sdk_event':\n default:\n return base\n }\n}\n","import { useEffect, useMemo, useReducer, useRef, useState } from 'react'\nimport type { WorkerDeckClient, SessionHandle } from '@workerdeck/client'\nimport type { AttachedFrame, PermissionMode, SessionEvent } from '@workerdeck/protocol'\nimport {\n applyEvent,\n initialTranscriptState,\n seedFromSessionInfo,\n type TranscriptState,\n} from './transcript.ts'\n\n/** Session events drive the reducer; the attach snapshot seeds fields (permission\n * mode, model) that a promptless session's event stream doesn't carry yet. */\nfunction reduce(state: TranscriptState, action: SessionEvent | AttachedFrame): TranscriptState {\n return action.type === 'attached'\n ? seedFromSessionInfo(state, action.session)\n : applyEvent(state, action)\n}\n\nexport type UseClaudeSessionOptions = {\n /** Called when the server rejects a command with a protocol_error frame — e.g. a\n * permission-mode switch the CLI refuses. Without a handler these are dropped\n * silently and the UI looks like \"nothing happened\". */\n onProtocolError?: (message: string) => void\n}\n\nexport type UseClaudeSessionResult = {\n state: TranscriptState\n connected: boolean\n /** The live attach handle, for wiring companions that must ride the SAME\n * socket — e.g. useToolCallHost: the bridge asks the first attached client,\n * so a host on a second handle would never see the requests. Undefined until\n * attached and after unmount. */\n handle: SessionHandle | undefined\n send: (text: string) => void\n approve: (requestId: string, updatedInput?: Record<string, unknown>) => void\n deny: (requestId: string, message?: string) => void\n interrupt: () => void\n setPermissionMode: (mode: PermissionMode) => void\n setModel: (model?: string) => void\n closeSession: () => void\n}\n\n/** Attach to a session and maintain live transcript state. Detaches on unmount. */\nexport function useClaudeSession(\n client: WorkerDeckClient,\n sessionId: string | undefined,\n options?: UseClaudeSessionOptions,\n): UseClaudeSessionResult {\n const [state, dispatch] = useReducer(reduce, initialTranscriptState)\n const [connected, setConnected] = useState(false)\n // Ref for the stable callbacks below; state so consumers of `handle` re-render\n // when the socket opens or the session switches.\n const [handleState, setHandleState] = useState<SessionHandle | undefined>()\n const handleRef = useRef<SessionHandle | null>(null)\n // Ref'd so a new inline callback doesn't tear down and reopen the socket.\n const onProtocolErrorRef = useRef(options?.onProtocolError)\n onProtocolErrorRef.current = options?.onProtocolError\n\n useEffect(() => {\n if (!sessionId) return\n const handle = client.attach(sessionId)\n handleRef.current = handle\n setHandleState(handle)\n const offEvent = handle.on('event', (event: SessionEvent) => dispatch(event))\n const offAttached = handle.on('attached', (frame: AttachedFrame) => dispatch(frame))\n const offConn = handle.on('connectionChange', setConnected)\n const offProtocolError = handle.on('protocolError', (message: string) => {\n onProtocolErrorRef.current?.(message)\n })\n return () => {\n offEvent()\n offAttached()\n offConn()\n offProtocolError()\n handle.detach()\n handleRef.current = null\n setHandleState(undefined)\n }\n }, [client, sessionId])\n\n return useMemo(\n () => ({\n state,\n connected,\n handle: handleState,\n send: (text) => handleRef.current?.send(text),\n approve: (requestId, updatedInput) => handleRef.current?.approve(requestId, updatedInput),\n deny: (requestId, message) => handleRef.current?.deny(requestId, message),\n interrupt: () => handleRef.current?.interrupt(),\n setPermissionMode: (mode) => handleRef.current?.setPermissionMode(mode),\n setModel: (model) => handleRef.current?.setModel(model),\n closeSession: () => handleRef.current?.closeSession(),\n }),\n [state, connected, handleState],\n )\n}\n","import type { SessionHandle } from '@workerdeck/client'\nimport type { RunScriptResult, SandboxEngine, SandboxVfs } from '@workerdeck/sandbox'\nimport type { ToolCallRequestFrame } from '@workerdeck/protocol'\n\n/** What the host was asked to do and how it went (for UI/telemetry). */\nexport type ToolHostExecution = {\n executionId: string\n toolName: string\n status: 'running' | 'settled' | 'failed' | 'canceled'\n reason?: string\n startedAt: number\n endedAt?: number\n}\n\nexport type ToolHostRunner = (request: {\n script: string\n vfs: SandboxVfs\n timeoutMs: number\n memoryLimitBytes: number\n signal: AbortSignal\n}) => Promise<RunScriptResult>\n\nexport type ToolCallHostOptions = {\n /** Tools this client will execute. Anything else is refused, so a server can\n * never talk this tab into running something it didn't opt into.\n * Default: `['eval_script']`. */\n tools?: string[]\n /** Guest wall-clock limit, unless the request asks for less. Default 5000. */\n timeoutMs?: number\n /** Guest allocator cap, unless the request asks for less. Default 64 MiB. */\n memoryLimitBytes?: number\n /**\n * Load the WASM guest engine. Called at most once, on the first bridged call\n * — nothing is downloaded or parsed until a session actually bridges one.\n * Defaults to `@workerdeck/sandbox` with the single-file browser build.\n */\n loadEngine?: () => Promise<SandboxEngine>\n /**\n * Run the script. Defaults to executing on this thread, which is fine for the\n * short, time-boxed evaluations this is built for. Supply your own (a Web\n * Worker running the same engine) to keep long evaluations off the UI thread\n * — the guest deadline preempts the interpreter, but only between bytecode\n * ops on whichever thread it runs on.\n */\n execute?: ToolHostRunner\n /** Host-gated fetch for the guest. Omitted = the guest has no network at all. */\n fetchText?: (url: string) => Promise<string>\n /** Observe executions (rendering, logging). */\n onExecution?: (execution: ToolHostExecution) => void\n}\n\n/**\n * Answers server-bridged tool calls by executing them in this browser tab.\n * Framework-free — {@link useToolCallHost} is a thin React wrapper.\n *\n * The point is data locality: documents fetched or held client-side can be\n * evaluated here and never touch the server. The engine loads lazily, so a page\n * that never bridges a call never pays for the WASM guest.\n */\nexport function createToolCallHost(\n handle: SessionHandle,\n options: ToolCallHostOptions = {},\n): { dispose: () => void } {\n const inFlight = new Map<string, AbortController>()\n let enginePromise: Promise<SandboxEngine> | undefined\n let disposed = false\n\n const track = (execution: ToolHostExecution) => options.onExecution?.(execution)\n\n const refuse = (frame: ToolCallRequestFrame, reason: string, error: string, startedAt: number) => {\n handle.sendToolCallError(frame.executionId, reason, error)\n track({\n executionId: frame.executionId,\n toolName: frame.toolName,\n status: 'failed',\n reason,\n startedAt,\n endedAt: Date.now(),\n })\n }\n\n const run = async (frame: ToolCallRequestFrame): Promise<void> => {\n const startedAt = Date.now()\n const allowed = options.tools ?? ['eval_script']\n if (!allowed.includes(frame.toolName)) {\n refuse(frame, 'unsupported_tool', `this client does not execute '${frame.toolName}'`, startedAt)\n return\n }\n const script = (frame.input as { script?: unknown } | undefined)?.script\n if (typeof script !== 'string') {\n refuse(frame, 'invalid_input', 'expected a string `script` input', startedAt)\n return\n }\n\n const controller = new AbortController()\n inFlight.set(frame.executionId, controller)\n track({ executionId: frame.executionId, toolName: frame.toolName, status: 'running', startedAt })\n\n try {\n const sandbox = await import('@workerdeck/sandbox')\n const vfs = sandbox.createVfs(frame.vfsSeed)\n // Never exceed what the server asked for: it owns the deadline it will\n // give up at, and answering after that is wasted work.\n const timeoutMs = Math.min(\n frame.limits?.timeoutMs ?? Number.POSITIVE_INFINITY,\n options.timeoutMs ?? 5000,\n )\n const memoryLimitBytes = Math.min(\n frame.limits?.memoryLimitBytes ?? Number.POSITIVE_INFINITY,\n options.memoryLimitBytes ?? 64 * 1024 * 1024,\n )\n\n const result = options.execute\n ? await options.execute({ script, vfs, timeoutMs, memoryLimitBytes, signal: controller.signal })\n : await (async () => {\n enginePromise ??= (options.loadEngine ?? defaultLoadEngine)()\n return sandbox.runScript(await enginePromise, {\n script,\n vfs,\n timeoutMs,\n memoryLimitBytes,\n signal: controller.signal,\n fetchText: options.fetchText,\n })\n })()\n\n // Cancelled or torn down while we worked: the server is no longer waiting.\n if (disposed || !inFlight.has(frame.executionId)) return\n const logs = result.logs.map((l) => `[${l.level}] ${l.text}`)\n if (result.ok) {\n handle.sendToolCallResult(frame.executionId, { type: 'json', value: result.value }, logs)\n track({\n executionId: frame.executionId,\n toolName: frame.toolName,\n status: 'settled',\n startedAt,\n endedAt: Date.now(),\n })\n } else {\n handle.sendToolCallError(frame.executionId, result.reason, result.error, logs)\n track({\n executionId: frame.executionId,\n toolName: frame.toolName,\n status: 'failed',\n reason: result.reason,\n startedAt,\n endedAt: Date.now(),\n })\n }\n } catch (error) {\n if (disposed || !inFlight.has(frame.executionId)) return\n // Engine load failures land here — tell the server so the agent can adapt\n // instead of waiting out the deadline.\n refuse(frame, 'host_error', error instanceof Error ? error.message : String(error), startedAt)\n } finally {\n inFlight.delete(frame.executionId)\n }\n }\n\n const offRequest = handle.on('toolCallRequest', (frame) => void run(frame))\n const offCancel = handle.on('toolCallCanceled', ({ executionId, reason }) => {\n const controller = inFlight.get(executionId)\n if (!controller) return\n controller.abort()\n inFlight.delete(executionId)\n track({\n executionId,\n toolName: '',\n status: 'canceled',\n reason,\n startedAt: Date.now(),\n endedAt: Date.now(),\n })\n })\n\n return {\n dispose: () => {\n disposed = true\n offRequest()\n offCancel()\n for (const controller of inFlight.values()) controller.abort()\n inFlight.clear()\n },\n }\n}\n\n/** The single-file browser build keeps this to one lazy chunk — no separate\n * .wasm fetch, and nothing at all until the first bridged call. */\nasync function defaultLoadEngine(): Promise<SandboxEngine> {\n const [sandbox, variant] = await Promise.all([\n import('@workerdeck/sandbox'),\n import('@jitl/quickjs-singlefile-browser-release-asyncify'),\n ])\n return sandbox.loadEngine(variant as never)\n}\n","import { useEffect, useRef, useState } from 'react'\nimport type { SessionHandle } from '@workerdeck/client'\nimport {\n createToolCallHost,\n type ToolCallHostOptions,\n type ToolHostExecution,\n} from './tool-host.ts'\n\nexport type UseToolCallHostOptions = ToolCallHostOptions & {\n /** Turn the host off without unmounting. Default true. */\n enabled?: boolean\n /** How many recent executions to keep for rendering. Default 50. */\n historyLimit?: number\n}\n\n/**\n * React wrapper around {@link createToolCallHost}: subscribes while mounted and\n * exposes recent executions for rendering. All the logic lives in the\n * framework-free host — this only manages the subscription's lifetime.\n */\nexport function useToolCallHost(\n handle: SessionHandle | undefined,\n options: UseToolCallHostOptions = {},\n): { executions: ToolHostExecution[] } {\n const [executions, setExecutions] = useState<ToolHostExecution[]>([])\n // Read options at call time so re-renders never tear down the subscription.\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n useEffect(() => {\n if (!handle || options.enabled === false) return\n const host = createToolCallHost(handle, {\n // Delegate every option through the ref, so a caller passing inline\n // objects/closures (the common case) doesn't resubscribe each render.\n get tools() {\n return optionsRef.current.tools\n },\n get timeoutMs() {\n return optionsRef.current.timeoutMs\n },\n get memoryLimitBytes() {\n return optionsRef.current.memoryLimitBytes\n },\n get loadEngine() {\n return optionsRef.current.loadEngine\n },\n get execute() {\n return optionsRef.current.execute\n },\n get fetchText() {\n return optionsRef.current.fetchText\n },\n onExecution: (execution) => {\n optionsRef.current.onExecution?.(execution)\n const limit = optionsRef.current.historyLimit ?? 50\n setExecutions((prev) => [\n ...prev.filter((e) => e.executionId !== execution.executionId),\n execution,\n ].slice(-limit))\n },\n })\n return () => host.dispose()\n }, [handle, options.enabled])\n\n return { executions }\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@workerdeck/react",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "description": "Headless React layer for WorkerDeck: useClaudeSession hook + pure transcript reducer. No styling opinion — @workerdeck/ui is the styled layer on top.",
6
+ "license": "MIT",
7
+ "main": "./build/index.mjs",
8
+ "types": "./build/index.d.mts",
9
+ "files": [
10
+ "build"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "@workerdeck/source": "./src/index.ts",
15
+ "types": "./build/index.d.mts",
16
+ "default": "./build/index.mjs"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "@workerdeck/client": "0.6.0",
21
+ "@workerdeck/sandbox": "0.6.0",
22
+ "@workerdeck/protocol": "0.6.0"
23
+ },
24
+ "peerDependencies": {
25
+ "react": "^18.0.0 || ^19.0.0",
26
+ "@jitl/quickjs-singlefile-browser-release-asyncify": "^0.31.0"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "@jitl/quickjs-singlefile-browser-release-asyncify": {
30
+ "optional": true
31
+ }
32
+ },
33
+ "devDependencies": {
34
+ "@jitl/quickjs-ng-wasmfile-release-asyncify": "^0.31.0",
35
+ "@jitl/quickjs-singlefile-browser-release-asyncify": "^0.31.0",
36
+ "@types/node": "^22.10.0",
37
+ "@types/react": "^19.2.0",
38
+ "@types/ws": "^8.18.1",
39
+ "react": "^19.2.0",
40
+ "rimraf": "^6.1.3",
41
+ "tsdown": "^0.21.10",
42
+ "vitest": "^3.2.0",
43
+ "ws": "^8.21.1",
44
+ "@workerdeck/core": "0.6.0",
45
+ "@workerdeck/server": "0.6.0"
46
+ },
47
+ "author": "Tobias Strebitzer",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/workerdeck/workerdeck.git",
51
+ "directory": "packages/react"
52
+ },
53
+ "homepage": "https://workerdeck.github.io/workerdeck/",
54
+ "bugs": "https://github.com/workerdeck/workerdeck/issues",
55
+ "keywords": [
56
+ "claude",
57
+ "claude-code",
58
+ "anthropic",
59
+ "agent",
60
+ "react",
61
+ "hooks",
62
+ "headless"
63
+ ],
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "scripts": {
68
+ "clean": "rimraf build",
69
+ "build": "tsdown",
70
+ "typecheck": "tsgo -p tsconfig.json && tsgo -p tsconfig.test.json",
71
+ "test": "vitest run"
72
+ }
73
+ }