@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 +21 -0
- package/README.md +111 -0
- package/build/index.d.mts +197 -0
- package/build/index.mjs +520 -0
- package/build/index.mjs.map +1 -0
- package/package.json +73 -0
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
|
package/build/index.mjs
ADDED
|
@@ -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
|
+
}
|