@workerdeck/react 0.7.0 → 0.10.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/README.md +32 -2
- package/build/index.d.mts +527 -8
- package/build/index.mjs +1020 -13
- package/build/index.mjs.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -77,11 +77,41 @@ stream doesn't carry yet; events stay authoritative once they arrive.
|
|
|
77
77
|
- `pendingApprovals` — permission requests awaiting an approve/deny decision.
|
|
78
78
|
- `status` / `statusDetail`, `model`, `cwd`, `sdkSessionId`, `permissionMode`.
|
|
79
79
|
- `models` and `commands` — what the session can switch to / accepts (from `capabilities`).
|
|
80
|
+
- `capabilities` — **the engine's capability record**, always present: the runner-reported copy
|
|
81
|
+
from the attach snapshot, else the protocol's static default for the engine. Render affordances
|
|
82
|
+
from this rather than switching on the engine name; an absent capability means the control is
|
|
83
|
+
*hidden*, never one that silently does nothing.
|
|
84
|
+
- `session` — the whole attach snapshot, for the facts no event carries (profile, `apiKeySource`,
|
|
85
|
+
`canBypassPermissions`, `createdAt`).
|
|
80
86
|
- `contextUsage`, `rateLimits` (keyed by window; absent for API-key sessions — render nothing,
|
|
81
|
-
not 0%)
|
|
87
|
+
not 0%) with `rateLimitsUpdatedAt`, `totalCostUsd` (session-cumulative), and `lastSeq` for
|
|
88
|
+
replay dedupe.
|
|
82
89
|
|
|
83
90
|
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.
|
|
91
|
+
unit-tested. Keep rendering logic out of it. `rateLimitWindows(state)` and `scanPromptTokens(text)`
|
|
92
|
+
are the other pure helpers here, for the same reason: ordered usage windows and `@file` /
|
|
93
|
+
`/command` token recognition are string-and-shape work every client needs and every client should
|
|
94
|
+
agree on.
|
|
95
|
+
|
|
96
|
+
## Composing a message
|
|
97
|
+
|
|
98
|
+
Two more hooks cover what a composer needs beyond text, both capability-aware:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
const { state, send } = useClaudeSession(client, sessionId)
|
|
102
|
+
// Staging + upload, filtered by `capabilities.attachments`: a kind the engine
|
|
103
|
+
// forswears is refused locally instead of 415'ing at the gateway.
|
|
104
|
+
const attachments = useAttachments(client, sessionId, {
|
|
105
|
+
capabilities: state.capabilities,
|
|
106
|
+
engine: state.engine,
|
|
107
|
+
})
|
|
108
|
+
// `@file` search rooted at the session's cwd. `available` is false when the
|
|
109
|
+
// gateway serves no host files — don't advertise what isn't there.
|
|
110
|
+
const files = useHostFileSearch(client, state.cwd)
|
|
111
|
+
|
|
112
|
+
attachments.add(pickedFiles) // uploads start immediately
|
|
113
|
+
send(text, attachments.readyIds) // the message names ids; bytes never enter the event log
|
|
114
|
+
```
|
|
85
115
|
|
|
86
116
|
## Running tool calls in the tab
|
|
87
117
|
|
package/build/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { ContextUsage, EngineCapabilities, HostDirEntry, HostFileMatch, MessageAttachment, ModelOption, PermissionMode, PermissionRequest, ProfileEngine, RateLimitInfo, SessionEvent, SessionInfo, SessionStatus, SkillInfo, SlashCommandInfo, ToolExecutionBackend } from "@workerdeck/protocol";
|
|
1
2
|
import { SessionHandle, WorkerDeckClient } from "@workerdeck/client";
|
|
2
|
-
import { ContextUsage, ModelOption, PermissionMode, PermissionRequest, ProfileEngine, RateLimitInfo, SessionEvent, SessionInfo, SessionStatus, SlashCommandInfo, ToolExecutionBackend } from "@workerdeck/protocol";
|
|
3
3
|
import { RunScriptResult, SandboxEngine, SandboxVfs } from "@workerdeck/sandbox";
|
|
4
4
|
|
|
5
5
|
//#region src/transcript.d.ts
|
|
@@ -11,6 +11,7 @@ type TranscriptItem = {
|
|
|
11
11
|
kind: 'user';
|
|
12
12
|
id: string;
|
|
13
13
|
text: string;
|
|
14
|
+
attachments?: MessageAttachment[];
|
|
14
15
|
} | {
|
|
15
16
|
kind: 'assistant_text';
|
|
16
17
|
id: string;
|
|
@@ -69,6 +70,12 @@ type TranscriptItem = {
|
|
|
69
70
|
bytes: number;
|
|
70
71
|
description?: string;
|
|
71
72
|
};
|
|
73
|
+
/** A `file_produced` announcement, as the transcript keeps it. */
|
|
74
|
+
type ProducedFileRef = {
|
|
75
|
+
fileId: string;
|
|
76
|
+
mediaType?: string;
|
|
77
|
+
bytes?: number;
|
|
78
|
+
};
|
|
72
79
|
type TranscriptState = {
|
|
73
80
|
status: SessionStatus;
|
|
74
81
|
statusDetail?: string;
|
|
@@ -77,14 +84,61 @@ type TranscriptState = {
|
|
|
77
84
|
sdkSessionId?: string;
|
|
78
85
|
/** Engine running the session, from the attach snapshot. Gates CLI-only
|
|
79
86
|
* affordances; absent (an older server) reads as 'claude'. */
|
|
80
|
-
engine?: ProfileEngine;
|
|
87
|
+
engine?: ProfileEngine;
|
|
88
|
+
/**
|
|
89
|
+
* What this session's engine does and does not do: the runner-reported record
|
|
90
|
+
* from the attach snapshot when present, else {@link ENGINE_CAPABILITIES} for
|
|
91
|
+
* the engine. Always defined, so a surface can render every affordance from it
|
|
92
|
+
* rather than switching on the engine name — an absent capability means the
|
|
93
|
+
* affordance is *hidden*, never a control that silently does nothing.
|
|
94
|
+
*/
|
|
95
|
+
capabilities: EngineCapabilities;
|
|
96
|
+
/**
|
|
97
|
+
* The most recent attach snapshot, whole. The session-level facts no event
|
|
98
|
+
* carries — profile, apiKeySource, canBypassPermissions, createdAt, numTurns —
|
|
99
|
+
* live only here. Unlike the fields above it is replaced on every attach: it is
|
|
100
|
+
* the server's answer, not something the event stream refines.
|
|
101
|
+
*/
|
|
102
|
+
session?: SessionInfo; /** Models the session can switch to (from the `capabilities` event). */
|
|
81
103
|
models?: ModelOption[]; /** Slash commands the CLI accepts (from the `capabilities` event). */
|
|
82
|
-
commands?: SlashCommandInfo[];
|
|
104
|
+
commands?: SlashCommandInfo[];
|
|
105
|
+
/**
|
|
106
|
+
* Skills the engine can reach (from the `skills` event), replaced whole each
|
|
107
|
+
* time. Absent until the engine has enumerated them — which for codex is on
|
|
108
|
+
* its first turn, since listing needs a live child. So gate the affordance on
|
|
109
|
+
* *this being defined*, not on `capabilities.skillsList` alone: the flag says
|
|
110
|
+
* the engine can answer, this says it has.
|
|
111
|
+
*
|
|
112
|
+
* Not commands, and must not be offered as such — see the protocol's
|
|
113
|
+
* `SkillInfo`.
|
|
114
|
+
*/
|
|
115
|
+
skills?: SkillInfo[];
|
|
116
|
+
/**
|
|
117
|
+
* Files the engine wrote on the host, keyed by the absolute path it reported
|
|
118
|
+
* (from `file_produced`). A tool card holding a `savedPath` looks itself up
|
|
119
|
+
* here to turn that path into a fetchable id — `client.producedFileUrl` — so
|
|
120
|
+
* the picture renders without the operator having declared a host-file root.
|
|
121
|
+
*/
|
|
122
|
+
producedFiles?: Record<string, ProducedFileRef>;
|
|
123
|
+
/** What this session's default model resolves to (from `capabilities`). Known
|
|
124
|
+
* before the first turn, which `model` is not — a promptless session has no
|
|
125
|
+
* `system_init` until it is spoken to. */
|
|
126
|
+
defaultModel?: string; /** Seeded from `system_init`, updated on `permission_mode_changed`. */
|
|
83
127
|
permissionMode?: PermissionMode; /** Latest context-window snapshot; absent until the first turn completes. */
|
|
84
128
|
contextUsage?: ContextUsage;
|
|
85
129
|
/** Latest rate-limit snapshot per window ('five_hour', 'seven_day', ...).
|
|
86
130
|
* Absent for API-key sessions — render nothing, not 0%. */
|
|
87
131
|
rateLimits?: Record<string, RateLimitInfo>;
|
|
132
|
+
/**
|
|
133
|
+
* When the newest window reading was *taken* (the event's `ts`), not when this
|
|
134
|
+
* client received it — so a reading replayed on attach is dated honestly
|
|
135
|
+
* rather than as "just now". Updates come one per turn at best, which makes a
|
|
136
|
+
* stale reading normal and worth saying out loud.
|
|
137
|
+
*/
|
|
138
|
+
rateLimitsUpdatedAt?: number;
|
|
139
|
+
/** claude.ai plan the rate-limit windows belong to ('pro', 'max', ...), from
|
|
140
|
+
* `plan_info`. Absent for API-key sessions, like the windows themselves. */
|
|
141
|
+
subscriptionType?: string;
|
|
88
142
|
items: TranscriptItem[];
|
|
89
143
|
pendingApprovals: PermissionRequest[];
|
|
90
144
|
totalCostUsd: number;
|
|
@@ -98,9 +152,32 @@ declare const initialTranscriptState: TranscriptState;
|
|
|
98
152
|
* haven't set yet; the event stream stays authoritative.
|
|
99
153
|
*/
|
|
100
154
|
declare function seedFromSessionInfo(state: TranscriptState, info: SessionInfo): TranscriptState;
|
|
155
|
+
/**
|
|
156
|
+
* The session's rate-limit windows in reading order: the session window, the
|
|
157
|
+
* weekly window, then whichever per-model weekly windows it reports.
|
|
158
|
+
*
|
|
159
|
+
* Discovered rather than hardcoded — the SDK's set of windows is an open union
|
|
160
|
+
* and has grown before — but ordered, so the first two always mean the same
|
|
161
|
+
* thing. A window with no `utilization` is *unknown*, not zero, and is dropped
|
|
162
|
+
* entirely rather than drawn as an empty bar that reads as "plenty left".
|
|
163
|
+
*/
|
|
164
|
+
declare function rateLimitWindows(state: TranscriptState): Array<{
|
|
165
|
+
key: string;
|
|
166
|
+
info: RateLimitInfo;
|
|
167
|
+
}>;
|
|
101
168
|
declare function applyEvent(state: TranscriptState, event: SessionEvent): TranscriptState;
|
|
102
169
|
//#endregion
|
|
103
170
|
//#region src/use-session.d.ts
|
|
171
|
+
/**
|
|
172
|
+
* How the client is doing at reaching the gateway — deliberately not the session's
|
|
173
|
+
* status. The two are orthogonal, and while the socket is down the status a client
|
|
174
|
+
* holds is *stale*, so a surface that merges them must say so rather than keep
|
|
175
|
+
* claiming "idle".
|
|
176
|
+
*
|
|
177
|
+
* The handle retries forever, so `offline` is a judgement about how long it has
|
|
178
|
+
* been failing rather than a state the transport reports.
|
|
179
|
+
*/
|
|
180
|
+
type ConnectionState = 'live' | 'reconnecting' | 'offline';
|
|
104
181
|
type UseClaudeSessionOptions = {
|
|
105
182
|
/** Called when the server rejects a command with a protocol_error frame — e.g. a
|
|
106
183
|
* permission-mode switch the CLI refuses. Without a handler these are dropped
|
|
@@ -109,23 +186,465 @@ type UseClaudeSessionOptions = {
|
|
|
109
186
|
};
|
|
110
187
|
type UseClaudeSessionResult = {
|
|
111
188
|
state: TranscriptState;
|
|
189
|
+
/** True while the socket is open. {@link UseClaudeSessionResult.connection}
|
|
190
|
+
* carries the same fact with the "has it been failing a while" distinction. */
|
|
112
191
|
connected: boolean;
|
|
192
|
+
connection: ConnectionState;
|
|
193
|
+
/** The server's `PROTOCOL_VERSION` when it disagrees with the one this build
|
|
194
|
+
* mirrors — undefined when they match. Some events may not render. */
|
|
195
|
+
protocolMismatch?: number;
|
|
196
|
+
/**
|
|
197
|
+
* What a model picker should offer. Two sources, and which is authoritative
|
|
198
|
+
* depends on the engine: the `capabilities` event is the CLI asked what it
|
|
199
|
+
* supports, so for claude it wins; codex never sends one — its models are a
|
|
200
|
+
* catalog shipped with the release and served on the profile — so without the
|
|
201
|
+
* fallback its picker would be permanently empty and the session unswitchable.
|
|
202
|
+
*/
|
|
203
|
+
models: ModelOption[];
|
|
204
|
+
/** The model this session answers as: the one it reported, or, before it has
|
|
205
|
+
* reported anything, the default it will use. */
|
|
206
|
+
effectiveModel?: string;
|
|
113
207
|
/** The live attach handle, for wiring companions that must ride the SAME
|
|
114
208
|
* socket — e.g. useToolCallHost: the bridge asks the first attached client,
|
|
115
209
|
* so a host on a second handle would never see the requests. Undefined until
|
|
116
210
|
* attached and after unmount. */
|
|
117
|
-
handle: SessionHandle | undefined;
|
|
118
|
-
send: (text: string) => void;
|
|
211
|
+
handle: SessionHandle | undefined; /** Attachment ids come from `client.uploadAttachment`, in send order. */
|
|
212
|
+
send: (text: string, attachmentIds?: string[]) => void;
|
|
119
213
|
approve: (requestId: string, updatedInput?: Record<string, unknown>) => void;
|
|
120
|
-
|
|
214
|
+
/** `message` is fed back to the agent, which can then try something else;
|
|
215
|
+
* `interrupt` also stops the turn ("deny & stop"). */
|
|
216
|
+
deny: (requestId: string, message?: string, interrupt?: boolean) => void;
|
|
121
217
|
interrupt: () => void;
|
|
122
218
|
setPermissionMode: (mode: PermissionMode) => void;
|
|
123
219
|
setModel: (model?: string) => void;
|
|
124
|
-
closeSession: () => void;
|
|
220
|
+
closeSession: () => void; /** Skip the reconnect backoff — what a tab returning to the foreground does. */
|
|
221
|
+
reconnectNow: () => void;
|
|
125
222
|
};
|
|
126
223
|
/** Attach to a session and maintain live transcript state. Detaches on unmount. */
|
|
127
224
|
declare function useClaudeSession(client: WorkerDeckClient, sessionId: string | undefined, options?: UseClaudeSessionOptions): UseClaudeSessionResult;
|
|
128
225
|
//#endregion
|
|
226
|
+
//#region src/use-attachments.d.ts
|
|
227
|
+
/**
|
|
228
|
+
* Files staged for the next message.
|
|
229
|
+
*
|
|
230
|
+
* The upload happens as soon as something is picked, not at send time — the
|
|
231
|
+
* message names attachment *ids*, so the bytes must already be the server's
|
|
232
|
+
* before a turn can reference them, and the wait is spent while the user is
|
|
233
|
+
* still typing rather than after they hit send. It also keeps base64 out of the
|
|
234
|
+
* event log entirely, which is the protocol's rule.
|
|
235
|
+
*/
|
|
236
|
+
type StagedAttachment = {
|
|
237
|
+
/** Local identity, stable across a retry — the React key while uploading. */key: string;
|
|
238
|
+
name: string;
|
|
239
|
+
mediaType: string;
|
|
240
|
+
bytes: number; /** Object URL for an image thumbnail, revoked when the item goes away. */
|
|
241
|
+
previewUrl?: string;
|
|
242
|
+
status: 'uploading' | 'ready' | 'failed'; /** The server's id once uploaded — what `send` names. */
|
|
243
|
+
id?: string; /** Why the upload failed, verbatim from the gateway (413, 415, …). */
|
|
244
|
+
error?: string;
|
|
245
|
+
};
|
|
246
|
+
/** The kind vocabulary of {@link EngineCapabilities.attachments}. */
|
|
247
|
+
type AttachmentKind = 'image' | 'pdf' | 'text';
|
|
248
|
+
/**
|
|
249
|
+
* How a media type reaches a model, in the capability record's vocabulary.
|
|
250
|
+
* `undefined` means this build can't classify it — the upload still goes,
|
|
251
|
+
* because the gateway's vocabulary is the authoritative one.
|
|
252
|
+
*/
|
|
253
|
+
declare function attachmentKind(mediaType: string): AttachmentKind | undefined;
|
|
254
|
+
type UseAttachmentsOptions = {
|
|
255
|
+
/** The session's capability record — its `attachments` list decides which
|
|
256
|
+
* kinds are offered and which are refused locally. */
|
|
257
|
+
capabilities: EngineCapabilities;
|
|
258
|
+
/** Named in a local refusal, so "the codex engine does not take pdf
|
|
259
|
+
* attachments" says which engine meant it. */
|
|
260
|
+
engine?: ProfileEngine;
|
|
261
|
+
};
|
|
262
|
+
type UseAttachmentsResult = {
|
|
263
|
+
items: StagedAttachment[]; /** Uploaded ids in staging order — what {@link UseClaudeSessionResult.send} names. */
|
|
264
|
+
readyIds: string[]; /** An id that hasn't landed can't be named, so send waits. */
|
|
265
|
+
uploading: boolean; /** A refused file must be dealt with before the message goes. */
|
|
266
|
+
hasFailure: boolean; /** Accept attribute for a file input, narrowed to what the engine takes. */
|
|
267
|
+
accept: string;
|
|
268
|
+
/** True when the engine takes no attachments at all — hide the affordance
|
|
269
|
+
* entirely rather than offer one with no meaning. */
|
|
270
|
+
disabled: boolean;
|
|
271
|
+
add: (files: Iterable<File>) => void;
|
|
272
|
+
retry: (key: string) => void;
|
|
273
|
+
remove: (key: string) => void;
|
|
274
|
+
clear: () => void; /** A local refusal (wrong kind), surfaced once rather than silently dropped. */
|
|
275
|
+
error?: string;
|
|
276
|
+
dismissError: () => void;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Stage, upload and track files for the next message of a session.
|
|
280
|
+
*
|
|
281
|
+
* Refusals happen as early as they can be known: a kind the capability record
|
|
282
|
+
* forswears never reaches the network (the gateway would 415 it), and everything
|
|
283
|
+
* else is the gateway's call — its vocabulary is authoritative, so an unknown
|
|
284
|
+
* media type is uploaded rather than guessed at.
|
|
285
|
+
*/
|
|
286
|
+
declare function useAttachments(client: WorkerDeckClient, sessionId: string | undefined, {
|
|
287
|
+
capabilities,
|
|
288
|
+
engine
|
|
289
|
+
}: UseAttachmentsOptions): UseAttachmentsResult;
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/prompt-tokens.d.ts
|
|
292
|
+
/**
|
|
293
|
+
* The two prompt tokens the CLI understands — `@file` and `/command` — found in
|
|
294
|
+
* text that has already been sent.
|
|
295
|
+
*
|
|
296
|
+
* The mirror of the iOS client's `PromptTokens.scan`, and deliberately the same
|
|
297
|
+
* rules: a message should read the same after sending as it did in the composer,
|
|
298
|
+
* on either client. It lives here, beside the transcript reducer, for the same
|
|
299
|
+
* reason its Swift twin lives in the kit rather than the app — every interesting
|
|
300
|
+
* case is an edge (an `@` mid-word, an email address, a slash that is really an
|
|
301
|
+
* absolute path), so it is the part that gets unit-tested.
|
|
302
|
+
*
|
|
303
|
+
* Only the finished-text half is here; the composer's completion is the
|
|
304
|
+
* prompt-area's own trigger machinery.
|
|
305
|
+
*/
|
|
306
|
+
type PromptToken = {
|
|
307
|
+
kind: 'file' | 'command'; /** Offsets into the scanned string, prefix included. */
|
|
308
|
+
start: number;
|
|
309
|
+
end: number;
|
|
310
|
+
text: string;
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* Every token in a sent message.
|
|
314
|
+
*
|
|
315
|
+
* Stricter than what a composer completes: a bare `@` is a token being typed, but
|
|
316
|
+
* in a sent message it is just an at sign.
|
|
317
|
+
*/
|
|
318
|
+
declare function scanPromptTokens(text: string): PromptToken[];
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/host-tree.d.ts
|
|
321
|
+
/**
|
|
322
|
+
* One directory as the tree knows it: what `/fs/list` answered, plus whether the
|
|
323
|
+
* server held entries back.
|
|
324
|
+
*
|
|
325
|
+
* A directory that has never been asked for is simply absent from the map — which
|
|
326
|
+
* is not the same as an empty directory, and the difference is what tells the
|
|
327
|
+
* renderer to show a spinner rather than "nothing here".
|
|
328
|
+
*/
|
|
329
|
+
type HostDirState = {
|
|
330
|
+
entries: HostDirEntry[]; /** The directory held more entries than the server will return. */
|
|
331
|
+
truncated?: boolean;
|
|
332
|
+
};
|
|
333
|
+
/** One rendered row of the tree — a flat list is what a scroll container wants,
|
|
334
|
+
* and indentation is a number, not a nesting of DOM. */
|
|
335
|
+
type HostTreeRow = {
|
|
336
|
+
entry: HostDirEntry; /** 0 for the root's own children. */
|
|
337
|
+
depth: number; /** Directories only: whether this row's children are showing. */
|
|
338
|
+
expanded?: boolean; /** Set on an expanded directory whose listing hasn't arrived yet. */
|
|
339
|
+
loading?: boolean; /** Set on an expanded directory the server truncated. */
|
|
340
|
+
truncated?: boolean;
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Flatten the loaded directories into the rows the tree shows.
|
|
344
|
+
*
|
|
345
|
+
* Pure, so the interesting part of a file tree — which nodes are visible at what
|
|
346
|
+
* depth once a few directories are expanded and one of them is still loading —
|
|
347
|
+
* is testable without a DOM or a gateway.
|
|
348
|
+
*
|
|
349
|
+
* Only *expanded* directories contribute children, and only if their listing has
|
|
350
|
+
* arrived. An expanded-but-unlisted directory yields its own row with
|
|
351
|
+
* `loading: true` and no children: expansion is a request the user already made,
|
|
352
|
+
* so the row must say the answer is coming rather than look like an empty folder.
|
|
353
|
+
*/
|
|
354
|
+
declare function flattenHostTree(root: string, dirs: ReadonlyMap<string, HostDirState>, expanded: ReadonlySet<string>): HostTreeRow[];
|
|
355
|
+
/**
|
|
356
|
+
* Every ancestor of `path` below `root`, outermost first — the directories that
|
|
357
|
+
* must be expanded for `path` to be on screen.
|
|
358
|
+
*
|
|
359
|
+
* Returns `[]` when `path` is not under `root` rather than guessing: revealing a
|
|
360
|
+
* file the tree cannot contain is a no-op, not an error worth raising, and the
|
|
361
|
+
* caller has no better answer either.
|
|
362
|
+
*
|
|
363
|
+
* The prefix test is on a **path boundary** (`root` + `/`), so `/src/app` is not
|
|
364
|
+
* treated as living under `/src/a`.
|
|
365
|
+
*/
|
|
366
|
+
declare function ancestorsWithin(root: string, path: string): string[];
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/use-host-files.d.ts
|
|
369
|
+
type UseHostFileSearchResult = {
|
|
370
|
+
/**
|
|
371
|
+
* Whether `@file` completion is on offer at all: the session's cwd is known
|
|
372
|
+
* and this gateway hasn't already 404'd the search. Read it before advertising
|
|
373
|
+
* the affordance — a server without host files configured has none.
|
|
374
|
+
*/
|
|
375
|
+
available: boolean;
|
|
376
|
+
/**
|
|
377
|
+
* Run one search. Safe to call per keystroke — the route is built for it
|
|
378
|
+
* (bounded walk, build directories skipped) — and it answers `[]` rather than
|
|
379
|
+
* throwing, because a failed lookup is not worth an error banner over an
|
|
380
|
+
* affordance the user can ignore.
|
|
381
|
+
*/
|
|
382
|
+
search: (query: string, options?: {
|
|
383
|
+
limit?: number;
|
|
384
|
+
signal?: AbortSignal;
|
|
385
|
+
}) => Promise<HostFileMatch[]>;
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* Fuzzy file search rooted at a session's working directory — what an `@file`
|
|
389
|
+
* picker needs.
|
|
390
|
+
*
|
|
391
|
+
* Deliberately session-scoped: the server's `hostFiles.roots` are the security
|
|
392
|
+
* boundary, but what someone wants while talking to an agent is *this* project's
|
|
393
|
+
* tree, so this never offers the roots list.
|
|
394
|
+
*
|
|
395
|
+
* A gateway that answers 404 once has answered for the session: host files are
|
|
396
|
+
* either configured or they aren't, and the answer will not change while the cwd
|
|
397
|
+
* holds. Asking again on every character would be a request per keystroke for a
|
|
398
|
+
* feature that does not exist here.
|
|
399
|
+
*/
|
|
400
|
+
declare function useHostFileSearch(client: WorkerDeckClient, cwd: string | undefined): UseHostFileSearchResult;
|
|
401
|
+
type UseHostFileRootsResult = {
|
|
402
|
+
/** Whether this gateway serves host files at all. */available: boolean;
|
|
403
|
+
/**
|
|
404
|
+
* Whether `PUT /fs/write` is enabled here.
|
|
405
|
+
*
|
|
406
|
+
* Read it before offering an editor. Writing is a **separate** server opt-in
|
|
407
|
+
* from reading and defaults off, so a gateway that happily lists and reads a
|
|
408
|
+
* tree may still refuse every save — and finding that out at save time, with
|
|
409
|
+
* edits already made, is the worst moment for it.
|
|
410
|
+
*/
|
|
411
|
+
canWrite: boolean;
|
|
412
|
+
};
|
|
413
|
+
/**
|
|
414
|
+
* Whether host files are served here, and whether they may be written.
|
|
415
|
+
*
|
|
416
|
+
* One request per client, cached for the life of the hook: the roots and the
|
|
417
|
+
* write flag are gateway configuration, not session state, and they do not
|
|
418
|
+
* change while the tab is open.
|
|
419
|
+
*/
|
|
420
|
+
declare function useHostFileRoots(client: WorkerDeckClient): UseHostFileRootsResult;
|
|
421
|
+
type UseHostFileTreeResult = {
|
|
422
|
+
/**
|
|
423
|
+
* Whether a tree can be shown at all: the cwd is known and this gateway serves
|
|
424
|
+
* host files. Read it before rendering the rail — a gateway with no
|
|
425
|
+
* `hostFiles` configured has no tree, and that is a layout decision, not an
|
|
426
|
+
* error to display.
|
|
427
|
+
*/
|
|
428
|
+
available: boolean; /** The directory the tree is rooted at — the session's cwd. */
|
|
429
|
+
root: string | undefined; /** The visible tree, flattened. Empty until the root listing arrives. */
|
|
430
|
+
rows: HostTreeRow[]; /** True while the root listing is outstanding and there is nothing to show. */
|
|
431
|
+
loading: boolean; /** A listing that failed, verbatim from the gateway. */
|
|
432
|
+
error: string | undefined; /** Expand or collapse a directory. Expanding lists it once and remembers. */
|
|
433
|
+
toggle: (path: string) => void; /** Expand every directory between the root and this path, so it is on screen. */
|
|
434
|
+
reveal: (path: string) => void; /** Re-list one directory (default: the root), keeping what is expanded. */
|
|
435
|
+
refresh: (path?: string) => void;
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* An expandable file tree rooted at a session's working directory.
|
|
439
|
+
*
|
|
440
|
+
* Rooted at the cwd rather than at `/fs/roots` for the same reason
|
|
441
|
+
* {@link useHostFileSearch} is: the roots are the *security* boundary the server
|
|
442
|
+
* enforces on every request, but what someone wants while watching an agent work
|
|
443
|
+
* is this project's tree. The roots may well be broader; showing them would
|
|
444
|
+
* offer navigation to directories the session has nothing to do with.
|
|
445
|
+
*
|
|
446
|
+
* Listings are cached per directory and kept across a collapse, so reopening a
|
|
447
|
+
* folder is instant and does not re-ask. That staleness is deliberate and
|
|
448
|
+
* bounded: `refresh` exists, and knowing when to call it is the *next* problem
|
|
449
|
+
* (the agent is editing this same tree), not something a tree can guess.
|
|
450
|
+
*
|
|
451
|
+
* Like the search hook, a 404 is answered once for the session: host files are
|
|
452
|
+
* either configured here or they are not.
|
|
453
|
+
*/
|
|
454
|
+
declare function useHostFileTree(client: WorkerDeckClient, cwd: string | undefined): UseHostFileTreeResult;
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/use-session-info.d.ts
|
|
457
|
+
type UseSessionInfoResult = {
|
|
458
|
+
info: SessionInfo | undefined; /** True until the first answer — distinguishes "still asking" from "no such session". */
|
|
459
|
+
loading: boolean; /** Set when the gateway refused; `info` stays undefined. */
|
|
460
|
+
error: string | undefined;
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* The registry's record of one session, over REST.
|
|
464
|
+
*
|
|
465
|
+
* Separate from {@link useClaudeSession} on purpose: that hook attaches a
|
|
466
|
+
* WebSocket and streams a transcript, which is far more than a caller needs to
|
|
467
|
+
* know a session's `cwd` or title — and a second attach would be a second
|
|
468
|
+
* client on the bridge, which is the one thing the bridge's "asks the first
|
|
469
|
+
* attached client" rule cannot tolerate.
|
|
470
|
+
*
|
|
471
|
+
* Fetched once per session id. The record is registry state, not a live feed;
|
|
472
|
+
* anything that changes during a run arrives on the session's event stream.
|
|
473
|
+
*/
|
|
474
|
+
declare function useSessionInfo(client: WorkerDeckClient, sessionId: string | undefined): UseSessionInfoResult;
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/open-files.d.ts
|
|
477
|
+
/**
|
|
478
|
+
* One open file, in whatever state its read got to.
|
|
479
|
+
*
|
|
480
|
+
* A tab exists from the moment it is opened, before any bytes arrive — the tab
|
|
481
|
+
* strip is the record of what the user asked for, not of what the gateway has
|
|
482
|
+
* answered, and a tab that only appeared once the read landed would make a slow
|
|
483
|
+
* read look like a dead click.
|
|
484
|
+
*/
|
|
485
|
+
type OpenFile = {
|
|
486
|
+
/** Absolute host path — the tab's identity. Opening the same path twice
|
|
487
|
+
* focuses the existing tab rather than making a second one. */
|
|
488
|
+
path: string; /** Last segment, for the tab label. */
|
|
489
|
+
name: string;
|
|
490
|
+
status: 'loading' | 'ready' | 'binary' | 'error'; /** The text **as last seen on disk** — never the user's edits. */
|
|
491
|
+
content?: string;
|
|
492
|
+
/**
|
|
493
|
+
* The user's unsaved text. Absent when nothing has been typed since the last
|
|
494
|
+
* read or save.
|
|
495
|
+
*
|
|
496
|
+
* Kept separate from `content` rather than overwriting it, because a
|
|
497
|
+
* conditional write needs to know both: what is being sent, and what the
|
|
498
|
+
* `hash` describes. Collapsing them would make "did this change?" unanswerable
|
|
499
|
+
* after the first keystroke.
|
|
500
|
+
*/
|
|
501
|
+
draft?: string;
|
|
502
|
+
bytes?: number;
|
|
503
|
+
/**
|
|
504
|
+
* sha256 of the bytes `content` was read from — the `expectedHash` for the
|
|
505
|
+
* next write.
|
|
506
|
+
*
|
|
507
|
+
* This is the whole safety mechanism: `/fs/write` is conditional *always*, so
|
|
508
|
+
* a tab that lost its hash could not save at all without re-reading, and
|
|
509
|
+
* re-reading to save is precisely the race the conditional write exists to
|
|
510
|
+
* prevent.
|
|
511
|
+
*/
|
|
512
|
+
hash?: string;
|
|
513
|
+
modifiedAt?: number; /** Why the read failed, verbatim from the gateway. */
|
|
514
|
+
error?: string; /** A write is in flight. */
|
|
515
|
+
saving?: boolean; /** Why the last write failed, verbatim from the gateway. */
|
|
516
|
+
saveError?: string;
|
|
517
|
+
/**
|
|
518
|
+
* The file changed on disk since this tab read it — the gateway answered 409.
|
|
519
|
+
*
|
|
520
|
+
* Held as a distinct flag rather than folded into `saveError` because it is
|
|
521
|
+
* the one failure with a *choice* attached (reload, overwrite, keep editing)
|
|
522
|
+
* rather than a message to read.
|
|
523
|
+
*/
|
|
524
|
+
conflict?: boolean;
|
|
525
|
+
};
|
|
526
|
+
/** Whether a tab has edits that are not on disk. Derived, so typing something
|
|
527
|
+
* and undoing it back leaves the tab clean — which is what an editor should do
|
|
528
|
+
* and what a boolean flag set on first keystroke would get wrong. */
|
|
529
|
+
declare function isDirty(file: OpenFile): boolean;
|
|
530
|
+
/** What a tab would write: its edits if it has any, else what it read. */
|
|
531
|
+
declare function currentText(file: OpenFile): string;
|
|
532
|
+
type OpenFilesState = {
|
|
533
|
+
/** Tab order, left to right. */files: OpenFile[]; /** Absolute path of the focused tab, or undefined when nothing is open. */
|
|
534
|
+
activePath?: string;
|
|
535
|
+
};
|
|
536
|
+
type OpenFilesAction = {
|
|
537
|
+
type: 'open';
|
|
538
|
+
path: string;
|
|
539
|
+
} | {
|
|
540
|
+
type: 'close';
|
|
541
|
+
path: string;
|
|
542
|
+
} | {
|
|
543
|
+
type: 'closeAll';
|
|
544
|
+
} | {
|
|
545
|
+
type: 'activate';
|
|
546
|
+
path: string;
|
|
547
|
+
} /** A read landed. Ignored if the tab was closed while it was in flight. */ | {
|
|
548
|
+
type: 'loaded';
|
|
549
|
+
path: string;
|
|
550
|
+
content: string;
|
|
551
|
+
encoding: 'utf8' | 'base64';
|
|
552
|
+
bytes: number;
|
|
553
|
+
hash: string;
|
|
554
|
+
modifiedAt: number;
|
|
555
|
+
} | {
|
|
556
|
+
type: 'failed';
|
|
557
|
+
path: string;
|
|
558
|
+
error: string;
|
|
559
|
+
} /** The user typed. */ | {
|
|
560
|
+
type: 'edit';
|
|
561
|
+
path: string;
|
|
562
|
+
content: string;
|
|
563
|
+
} /** Throw away unsaved edits and go back to what was read. */ | {
|
|
564
|
+
type: 'revert';
|
|
565
|
+
path: string;
|
|
566
|
+
} | {
|
|
567
|
+
type: 'saveStart';
|
|
568
|
+
path: string;
|
|
569
|
+
}
|
|
570
|
+
/** A write succeeded. `content` is **what was written**, not what the tab
|
|
571
|
+
* holds now — the user may have kept typing while it was in flight. */
|
|
572
|
+
| {
|
|
573
|
+
type: 'saved';
|
|
574
|
+
path: string;
|
|
575
|
+
content: string;
|
|
576
|
+
bytes: number;
|
|
577
|
+
hash: string;
|
|
578
|
+
modifiedAt: number;
|
|
579
|
+
} | {
|
|
580
|
+
type: 'saveFailed';
|
|
581
|
+
path: string;
|
|
582
|
+
error: string;
|
|
583
|
+
conflict?: boolean;
|
|
584
|
+
} /** Dismiss the conflict banner and carry on editing. */ | {
|
|
585
|
+
type: 'dismissConflict';
|
|
586
|
+
path: string;
|
|
587
|
+
};
|
|
588
|
+
declare const initialOpenFilesState: OpenFilesState;
|
|
589
|
+
/**
|
|
590
|
+
* The tab strip and the editor's whole behaviour, as a pure function.
|
|
591
|
+
*
|
|
592
|
+
* The rules worth stating, because they are the ones a naive implementation
|
|
593
|
+
* gets wrong:
|
|
594
|
+
*
|
|
595
|
+
* - **Opening an open path never re-reads it.** It focuses the tab. Re-reading
|
|
596
|
+
* would silently discard that tab's unsaved edits on a double click.
|
|
597
|
+
* - **Closing the focused tab focuses its right-hand neighbour**, falling back
|
|
598
|
+
* to the left when it was last. Focusing "the first tab" instead is what makes
|
|
599
|
+
* closing several tabs in a row jump the user around.
|
|
600
|
+
* - **A successful save is applied against the text that was sent**, not against
|
|
601
|
+
* the tab's current text. Typing during a save is normal; treating the write's
|
|
602
|
+
* completion as "the tab is now clean" would silently drop those keystrokes.
|
|
603
|
+
* - **Nothing here discards edits implicitly.** `revert` and `loaded` are the
|
|
604
|
+
* only two things that clear a draft, and both are the direct result of
|
|
605
|
+
* someone asking for it. The conditional write exists so a browser edit cannot
|
|
606
|
+
* clobber the agent mid-run; this holds the same line in the other direction.
|
|
607
|
+
*
|
|
608
|
+
* Late results are addressed by path and dropped if that tab is gone, so a slow
|
|
609
|
+
* read of a closed file cannot resurrect it.
|
|
610
|
+
*/
|
|
611
|
+
declare function openFilesReducer(state: OpenFilesState, action: OpenFilesAction): OpenFilesState;
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/use-open-files.d.ts
|
|
614
|
+
type UseOpenFilesResult = OpenFilesState & {
|
|
615
|
+
/** The focused file, resolved — what the editor renders. */active: OpenFile | undefined; /** Any tab with unsaved edits — what a close or unload guard asks. */
|
|
616
|
+
hasUnsaved: boolean; /** Open a path, or focus it if it is already open. */
|
|
617
|
+
open: (path: string) => void;
|
|
618
|
+
close: (path: string) => void;
|
|
619
|
+
closeAll: () => void;
|
|
620
|
+
activate: (path: string) => void; /** Record a keystroke. Pure state; nothing is written until `save`. */
|
|
621
|
+
edit: (path: string, content: string) => void; /** Write the tab's edits, conditional on the hash it read. No-op if clean. */
|
|
622
|
+
save: (path: string) => Promise<void>; /** Throw the tab's edits away and go back to what was read. */
|
|
623
|
+
revert: (path: string) => void;
|
|
624
|
+
/** Re-read from disk. **Discards unsaved edits** — only call on an explicit
|
|
625
|
+
* choice, never to "refresh". */
|
|
626
|
+
reload: (path: string) => void;
|
|
627
|
+
/** Resolve a conflict by taking this tab's version: re-read for the current
|
|
628
|
+
* hash, then write the draft against it. */
|
|
629
|
+
overwrite: (path: string) => Promise<void>; /** Dismiss the conflict banner without resolving it. */
|
|
630
|
+
dismissConflict: (path: string) => void;
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* The open-file tabs of a workspace: which files are open, which one is focused,
|
|
634
|
+
* the bytes behind each, and the edits on top of them.
|
|
635
|
+
*
|
|
636
|
+
* Reads are fired from an effect keyed on "which tabs are still loading" rather
|
|
637
|
+
* than from `open` itself, so the reducer stays pure and a tab that was opened,
|
|
638
|
+
* closed and reopened does not carry a stale in-flight request with it.
|
|
639
|
+
*
|
|
640
|
+
* Deliberately **not** given the session's cwd: a tab is an absolute host path,
|
|
641
|
+
* and where it came from — the tree, a search hit, a path in the transcript — is
|
|
642
|
+
* the caller's business. Containment is the server's job on every `/fs/read` and
|
|
643
|
+
* `/fs/write`, not something re-derived here from a directory this hook would
|
|
644
|
+
* have to trust.
|
|
645
|
+
*/
|
|
646
|
+
declare function useOpenFiles(client: WorkerDeckClient): UseOpenFilesResult;
|
|
647
|
+
//#endregion
|
|
129
648
|
//#region src/tool-host.d.ts
|
|
130
649
|
/** What the host was asked to do and how it went (for UI/telemetry). */
|
|
131
650
|
type ToolHostExecution = {
|
|
@@ -193,5 +712,5 @@ declare function useToolCallHost(handle: SessionHandle | undefined, options?: Us
|
|
|
193
712
|
executions: ToolHostExecution[];
|
|
194
713
|
};
|
|
195
714
|
//#endregion
|
|
196
|
-
export { type ToolCallHostOptions, type ToolHostExecution, type ToolHostRunner, type TranscriptItem, type TranscriptState, type UseClaudeSessionResult, type UseToolCallHostOptions, applyEvent, createToolCallHost, initialTranscriptState, seedFromSessionInfo, useClaudeSession, useToolCallHost };
|
|
715
|
+
export { type AttachmentKind, type ConnectionState, type HostDirState, type HostTreeRow, type OpenFile, type OpenFilesAction, type OpenFilesState, type ProducedFileRef, type PromptToken, type StagedAttachment, type ToolCallHostOptions, type ToolHostExecution, type ToolHostRunner, type TranscriptItem, type TranscriptState, type UseAttachmentsOptions, type UseAttachmentsResult, type UseClaudeSessionOptions, type UseClaudeSessionResult, type UseHostFileRootsResult, type UseHostFileSearchResult, type UseHostFileTreeResult, type UseOpenFilesResult, type UseSessionInfoResult, type UseToolCallHostOptions, ancestorsWithin, applyEvent, attachmentKind, createToolCallHost, currentText, flattenHostTree, initialOpenFilesState, initialTranscriptState, isDirty, openFilesReducer, rateLimitWindows, scanPromptTokens, seedFromSessionInfo, useAttachments, useClaudeSession, useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, useSessionInfo, useToolCallHost };
|
|
197
716
|
//# sourceMappingURL=index.d.mts.map
|