@workerdeck/react 0.9.0 → 0.12.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 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%), `totalCostUsd` (session-cumulative), and `lastSeq` for replay dedupe.
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, MessageAttachment, 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
@@ -70,6 +70,12 @@ type TranscriptItem = {
70
70
  bytes: number;
71
71
  description?: string;
72
72
  };
73
+ /** A `file_produced` announcement, as the transcript keeps it. */
74
+ type ProducedFileRef = {
75
+ fileId: string;
76
+ mediaType?: string;
77
+ bytes?: number;
78
+ };
73
79
  type TranscriptState = {
74
80
  status: SessionStatus;
75
81
  statusDetail?: string;
@@ -78,9 +84,42 @@ type TranscriptState = {
78
84
  sdkSessionId?: string;
79
85
  /** Engine running the session, from the attach snapshot. Gates CLI-only
80
86
  * affordances; absent (an older server) reads as 'claude'. */
81
- engine?: ProfileEngine; /** Models the session can switch to (from the `capabilities` event). */
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). */
82
103
  models?: ModelOption[]; /** Slash commands the CLI accepts (from the `capabilities` event). */
83
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>;
84
123
  /** What this session's default model resolves to (from `capabilities`). Known
85
124
  * before the first turn, which `model` is not — a promptless session has no
86
125
  * `system_init` until it is spoken to. */
@@ -90,6 +129,13 @@ type TranscriptState = {
90
129
  /** Latest rate-limit snapshot per window ('five_hour', 'seven_day', ...).
91
130
  * Absent for API-key sessions — render nothing, not 0%. */
92
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;
93
139
  /** claude.ai plan the rate-limit windows belong to ('pro', 'max', ...), from
94
140
  * `plan_info`. Absent for API-key sessions, like the windows themselves. */
95
141
  subscriptionType?: string;
@@ -106,9 +152,32 @@ declare const initialTranscriptState: TranscriptState;
106
152
  * haven't set yet; the event stream stays authoritative.
107
153
  */
108
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
+ }>;
109
168
  declare function applyEvent(state: TranscriptState, event: SessionEvent): TranscriptState;
110
169
  //#endregion
111
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';
112
181
  type UseClaudeSessionOptions = {
113
182
  /** Called when the server rejects a command with a protocol_error frame — e.g. a
114
183
  * permission-mode switch the CLI refuses. Without a handler these are dropped
@@ -117,7 +186,24 @@ type UseClaudeSessionOptions = {
117
186
  };
118
187
  type UseClaudeSessionResult = {
119
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. */
120
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;
121
207
  /** The live attach handle, for wiring companions that must ride the SAME
122
208
  * socket — e.g. useToolCallHost: the bridge asks the first attached client,
123
209
  * so a host on a second handle would never see the requests. Undefined until
@@ -125,15 +211,440 @@ type UseClaudeSessionResult = {
125
211
  handle: SessionHandle | undefined; /** Attachment ids come from `client.uploadAttachment`, in send order. */
126
212
  send: (text: string, attachmentIds?: string[]) => void;
127
213
  approve: (requestId: string, updatedInput?: Record<string, unknown>) => void;
128
- deny: (requestId: string, message?: string) => void;
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;
129
217
  interrupt: () => void;
130
218
  setPermissionMode: (mode: PermissionMode) => void;
131
219
  setModel: (model?: string) => void;
132
- closeSession: () => void;
220
+ closeSession: () => void; /** Skip the reconnect backoff — what a tab returning to the foreground does. */
221
+ reconnectNow: () => void;
133
222
  };
134
223
  /** Attach to a session and maintain live transcript state. Detaches on unmount. */
135
224
  declare function useClaudeSession(client: WorkerDeckClient, sessionId: string | undefined, options?: UseClaudeSessionOptions): UseClaudeSessionResult;
136
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
137
648
  //#region src/tool-host.d.ts
138
649
  /** What the host was asked to do and how it went (for UI/telemetry). */
139
650
  type ToolHostExecution = {
@@ -201,5 +712,57 @@ declare function useToolCallHost(handle: SessionHandle | undefined, options?: Us
201
712
  executions: ToolHostExecution[];
202
713
  };
203
714
  //#endregion
204
- export { type ToolCallHostOptions, type ToolHostExecution, type ToolHostRunner, type TranscriptItem, type TranscriptState, type UseClaudeSessionResult, type UseToolCallHostOptions, applyEvent, createToolCallHost, initialTranscriptState, seedFromSessionInfo, useClaudeSession, useToolCallHost };
715
+ //#region src/recap.d.ts
716
+ /**
717
+ * "What happened while you were away", counted rather than written.
718
+ *
719
+ * Deterministic on purpose. A prose recap would mean spending a turn — tokens,
720
+ * context and latency — on a summary nobody asked the model for, and it would
721
+ * be wrong in the one case that matters most (a session that failed while
722
+ * unattended, where the model is exactly who you shouldn't ask). Everything
723
+ * here is already in the transcript; this only counts it.
724
+ *
725
+ * Framework-free and pure, like the reducer it reads from: both clients render
726
+ * the same recap from the same numbers.
727
+ */
728
+ type RecapSummary = {
729
+ /** Completed turns — `turn_result` rows, the engine's own unit of work. */turns: number; /** Messages the model wrote. Streaming ones count: they are on screen. */
730
+ replies: number; /** Tool calls started, and the distinct names, most-used first. */
731
+ tools: number;
732
+ toolNames: string[]; /** Files the agent handed over (`file_delivered`). */
733
+ files: number;
734
+ /** Failed turns and failed tool calls, together — what you'd want to know
735
+ * first on coming back. */
736
+ errors: number;
737
+ /** Approvals still waiting. Not a count of what happened, but the reason to
738
+ * look now rather than later. */
739
+ pending: number; /** Any of the above non-zero. A recap of nothing is noise. */
740
+ any: boolean;
741
+ };
742
+ /** The `TranscriptState` fields a recap reads — structural, so a caller can
743
+ * pass the whole state or just these. */
744
+ type RecapInput = {
745
+ items: readonly TranscriptItem[];
746
+ pendingApprovals?: readonly unknown[];
747
+ };
748
+ /**
749
+ * Summarize the items from `fromIndex` onward — the boundary being the number
750
+ * of items that existed when the session was last looked at.
751
+ *
752
+ * An out-of-range boundary is clamped rather than rejected: a transcript can
753
+ * *shrink* (a `/clear`, a fresh attach after a compaction), and the honest
754
+ * reading of "you last saw 40 items, there are now 12" is "everything here is
755
+ * new", not a negative count.
756
+ */
757
+ declare function summarizeSince(state: RecapInput, fromIndex: number): RecapSummary;
758
+ /**
759
+ * The recap as one line of text, in the order a person reads it: what got done,
760
+ * what it used, what went wrong, what is waiting.
761
+ *
762
+ * Returns `undefined` when there is nothing to say, so a caller can render the
763
+ * row or not on the value alone.
764
+ */
765
+ declare function recapLine(summary: RecapSummary): string | undefined;
766
+ //#endregion
767
+ export { type AttachmentKind, type ConnectionState, type HostDirState, type HostTreeRow, type OpenFile, type OpenFilesAction, type OpenFilesState, type ProducedFileRef, type PromptToken, type RecapInput, type RecapSummary, 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, recapLine, scanPromptTokens, seedFromSessionInfo, summarizeSince, useAttachments, useClaudeSession, useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, useSessionInfo, useToolCallHost };
205
768
  //# sourceMappingURL=index.d.mts.map