@workerdeck/react 0.15.0 → 0.16.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
@@ -152,6 +152,16 @@ input by construction: fine for the user's own data, never a source of server-au
152
152
  - **The recap is counted, never written.** `summarizeSince` returns numbers because generating
153
153
  prose would spend a turn on a summary nobody asked for, and would be worst exactly where it
154
154
  matters most: a session that failed while unattended.
155
+ - **Detached transcripts stay warm by default.** `useClaudeSession` keeps a bounded, module-scope
156
+ cache of the last few transcripts it held, so switching back to a session paints in the mount
157
+ frame and re-attaches with `afterSeq`, replaying only what it missed. Entries are keyed by the
158
+ client's `identityKey` — gateway base URL plus auth headers — never the session id alone (a
159
+ session id is unique only within one gateway), and a cached `afterSeq` pointed at a log the
160
+ server no longer has (a dormant rebuild, a restart) is detected off the attach frame
161
+ (`staleAttach`) and discarded with a full resync, because that attach would otherwise deliver
162
+ nothing and the stale rows would stand forever. Opt out with `cacheTranscript: false` if your
163
+ principal varies on one base URL by means the client cannot see, and call
164
+ `clearTranscriptCache()` on an in-place logout.
155
165
 
156
166
  ## License
157
167
 
package/build/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { ContextUsage, EngineCapabilities, HostDirEntry, HostFileMatch, MessageAttachment, ModelOption, PermissionMode, PermissionRequest, ProfileEngine, RateLimitInfo, SessionEvent, SessionInfo, SessionStatus, SkillInfo, SlashCommandInfo, ToolExecutionBackend } from "@workerdeck/protocol";
1
+ import { AttachedFrame, ContextUsage, EngineCapabilities, FilePatch, HostDirEntry, HostFileMatch, MessageAttachment, ModelOption, PermissionMode, PermissionRequest, ProfileEngine, ProfileUsage, RateLimitInfo, SessionEvent, SessionInfo, SessionStatus, SkillInfo, SlashCommandInfo, ToolExecutionBackend, UsageWindowRow } from "@workerdeck/protocol";
2
2
  import { SessionHandle, WorkerDeckClient } from "@workerdeck/client";
3
3
  import { RunScriptResult, SandboxEngine, SandboxVfs } from "@workerdeck/sandbox";
4
4
 
5
- //#region src/transcript.d.ts
5
+ //#region src/lib/transcript.d.ts
6
6
  /**
7
7
  * Pure transcript state machine over the wire-protocol event stream. Framework-free
8
8
  * so it can be unit-tested and reused outside React.
@@ -42,7 +42,17 @@ type TranscriptItem = {
42
42
  result?: {
43
43
  text: string;
44
44
  isError: boolean;
45
- }; /** Correlation id when this call is executed outside the model loop. */
45
+ };
46
+ /**
47
+ * What this call changed on disk, when it was a file edit — the engine's
48
+ * own hunks and line numbers (see protocol's {@link FilePatch}).
49
+ *
50
+ * Only ever set from the wire. A client cannot derive it: it has never
51
+ * seen the file, so a diff it computed from the tool's *input* would have
52
+ * no line numbers, and one parsed out of the result prose would be welded
53
+ * to an engine's text formatting.
54
+ */
55
+ patch?: FilePatch; /** Correlation id when this call is executed outside the model loop. */
46
56
  executionId?: string; /** Which backend is executing it, when known. */
47
57
  backend?: ToolExecutionBackend; /** Logs captured by the executor (guest console output). */
48
58
  logs?: string[];
@@ -156,18 +166,15 @@ declare function seedFromSessionInfo(state: TranscriptState, info: SessionInfo):
156
166
  * The session's rate-limit windows in reading order: the session window, the
157
167
  * weekly window, then whichever per-model weekly windows it reports.
158
168
  *
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".
169
+ * The ordering and the drop-the-unknown rule are protocol's `orderUsageWindows`
170
+ * — the dashboard renders the same windows straight off `ProfileInfo.usage`,
171
+ * with no transcript anywhere near it, and two orderings would be one account
172
+ * described two ways. This stays as the transcript-shaped door to it.
163
173
  */
164
- declare function rateLimitWindows(state: TranscriptState): Array<{
165
- key: string;
166
- info: RateLimitInfo;
167
- }>;
174
+ declare function rateLimitWindows(state: TranscriptState): UsageWindowRow[];
168
175
  declare function applyEvent(state: TranscriptState, event: SessionEvent): TranscriptState;
169
176
  //#endregion
170
- //#region src/use-session.d.ts
177
+ //#region src/hooks/use-session.d.ts
171
178
  /**
172
179
  * How the client is doing at reaching the gateway — deliberately not the session's
173
180
  * status. The two are orthogonal, and while the socket is down the status a client
@@ -178,11 +185,88 @@ declare function applyEvent(state: TranscriptState, event: SessionEvent): Transc
178
185
  * been failing rather than a state the transport reports.
179
186
  */
180
187
  type ConnectionState = 'live' | 'reconnecting' | 'offline';
188
+ /**
189
+ * The seq the initial attach replay ends on, or undefined when there is nothing
190
+ * to hold for.
191
+ *
192
+ * This is an exact signal, not a heuristic: the `attached` frame is sent before
193
+ * any replayed `event` frame and carries the runner's seq at attach time
194
+ * (`session.lastSeq`), so the moment the frame arrives the client knows
195
+ * precisely which seq the replay ends on. Every runner keeps its full event log
196
+ * and always delivers the highest-seq event on a fresh replay (the
197
+ * `conversation_reset` skip is strictly-below-the-reset, and the reset's seq is
198
+ * itself ≤ lastSeq), so `TranscriptState.lastSeq >= target` means the replay
199
+ * has landed. No quiet window or other arrival heuristic belongs here.
200
+ *
201
+ * Only a FRESH attach yields a target (`replayingFrom === 0`): a reconnect
202
+ * replays into a transcript the reader is already looking at, and blanking it
203
+ * mid-turn would be a worse bug than the flicker the hold exists to fix. A
204
+ * brand-new session (`lastSeq === 0`) has nothing to replay and never holds.
205
+ */
206
+ declare function initialReplayTarget(frame: AttachedFrame): number | undefined;
207
+ /**
208
+ * Whether an attach frame describes a DIFFERENT event log than the transcript
209
+ * `held` was built from — in which case attaching with `afterSeq: held.lastSeq`
210
+ * has already gone wrong: every event in the new log has seq ≤ afterSeq, so
211
+ * nothing will ever arrive and the stale rows would stand forever, with no
212
+ * error. The only recovery is to forget the state and re-attach from seq 0.
213
+ *
214
+ * A log resets on routine paths, not corner cases: a dormant session
215
+ * (claude/codex surviving a gateway restart) is rebuilt with a brand-new
216
+ * runner whose log starts at 0 and refills from the engine's own store. Two
217
+ * checks, each of which the other misses:
218
+ *
219
+ * - `session.lastSeq < held.lastSeq` — the server's log is shorter than what
220
+ * we hold. Within one log seq only grows, so this is proof of a reset. It
221
+ * catches a rebuilt runner that has not yet re-run far — but not one whose
222
+ * backfill already advanced past us.
223
+ * - `session.createdAt !== held.session.createdAt` — a different runner
224
+ * incarnation. The claude and codex runners stamp `Date.now()` at
225
+ * construction, so a dormant rebuild always changes it; the provider runner
226
+ * restores `createdAt` from its snapshot precisely when it also restores
227
+ * the event log and seq counter (ai-sdk-runner's `#restore`), so equality
228
+ * truthfully means "same log" for every engine.
229
+ *
230
+ * A full replay (`replayingFrom === 0`) is never stale — it carries the whole
231
+ * log, so the caller heals by resetting state and applying it — and holding
232
+ * nothing (`held.lastSeq === 0`) has nothing to be stale about. That first
233
+ * clause is also what makes the recovery loop-proof: the re-attach from 0 can
234
+ * never re-trigger this predicate.
235
+ *
236
+ * Not cache-specific: a live handle reconnecting after a gateway restart
237
+ * re-attaches with its own advanced `afterSeq` against the rebuilt log and
238
+ * hits the identical silence, so the hook applies this to every attach frame.
239
+ */
240
+ declare function staleAttach(frame: AttachedFrame, held: TranscriptState): boolean;
241
+ /**
242
+ * Backstop for the replay hold: if the target seq has not landed after this
243
+ * long, reveal what has arrived. On a healthy attach the target is always
244
+ * reached (see {@link initialReplayTarget}); the backstop exists because a
245
+ * blank panel forever would be a much worse failure than a visible stream, so
246
+ * the hold is bounded no matter what a future filter or a lossy path does. It
247
+ * runs from the attach — a per-event re-arm would be a quiet-window heuristic
248
+ * in a new costume.
249
+ */
250
+ declare const REPLAY_HOLD_MAX_MS = 1500;
181
251
  type UseClaudeSessionOptions = {
182
252
  /** Called when the server rejects a command with a protocol_error frame — e.g. a
183
253
  * permission-mode switch the CLI refuses. Without a handler these are dropped
184
254
  * silently and the UI looks like "nothing happened". */
185
255
  onProtocolError?: (message: string) => void;
256
+ /**
257
+ * Keep this session's transcript warm after unmount (default true): the next
258
+ * mount of the same (client identity, session) paints the cached rows in its
259
+ * first frame and attaches with `afterSeq`, replaying only what it missed.
260
+ * Bounded module-scope LRU, keyed by the client's `identityKey` (gateway +
261
+ * auth headers) so nothing crosses gateways or credentials; if the attach
262
+ * frame shows a different event log (see {@link staleAttach}), the entry is
263
+ * discarded and the hook re-attaches from seq 0.
264
+ *
265
+ * Set `false` for an embedder whose principal varies on one base URL by
266
+ * means the client cannot see (a custom `fetchImpl` switching users, say) —
267
+ * or call `clearTranscriptCache()` on logout. Read at attach time.
268
+ */
269
+ cacheTranscript?: boolean;
186
270
  };
187
271
  type UseClaudeSessionResult = {
188
272
  state: TranscriptState;
@@ -190,6 +274,16 @@ type UseClaudeSessionResult = {
190
274
  * carries the same fact with the "has it been failing a while" distinction. */
191
275
  connected: boolean;
192
276
  connection: ConnectionState;
277
+ /**
278
+ * True while the initial attach replay is still landing: the `attached` frame
279
+ * said events up to `session.lastSeq` follow, and they have not all been
280
+ * applied yet. A surface can hold its paint on this — keep the rows mounted
281
+ * and measuring, show nothing — and reveal a settled transcript in one frame,
282
+ * instead of streaming hundreds of replayed rows past the reader. Always
283
+ * false on a reconnect (only a fresh attach holds; see
284
+ * {@link initialReplayTarget}) and bounded by {@link REPLAY_HOLD_MAX_MS}.
285
+ */
286
+ replaying: boolean;
193
287
  /** The server's `PROTOCOL_VERSION` when it disagrees with the one this build
194
288
  * mirrors — undefined when they match. Some events may not render. */
195
289
  protocolMismatch?: number;
@@ -223,7 +317,15 @@ type UseClaudeSessionResult = {
223
317
  /** Attach to a session and maintain live transcript state. Detaches on unmount. */
224
318
  declare function useClaudeSession(client: WorkerDeckClient, sessionId: string | undefined, options?: UseClaudeSessionOptions): UseClaudeSessionResult;
225
319
  //#endregion
226
- //#region src/use-attachments.d.ts
320
+ //#region src/lib/transcript-cache.d.ts
321
+ /**
322
+ * Drop every cached transcript. For an embedder changing principals in place
323
+ * (a logout that keeps the page alive) — entries are unreachable through the
324
+ * new principal's client either way, but scrubbing them is free and final.
325
+ */
326
+ declare function clearTranscriptCache(): void;
327
+ //#endregion
328
+ //#region src/hooks/use-attachments.d.ts
227
329
  /**
228
330
  * Files staged for the next message.
229
331
  *
@@ -288,7 +390,7 @@ declare function useAttachments(client: WorkerDeckClient, sessionId: string | un
288
390
  engine
289
391
  }: UseAttachmentsOptions): UseAttachmentsResult;
290
392
  //#endregion
291
- //#region src/prompt-tokens.d.ts
393
+ //#region src/lib/prompt-tokens.d.ts
292
394
  /**
293
395
  * The two prompt tokens the CLI understands — `@file` and `/command` — found in
294
396
  * text that has already been sent.
@@ -317,7 +419,7 @@ type PromptToken = {
317
419
  */
318
420
  declare function scanPromptTokens(text: string): PromptToken[];
319
421
  //#endregion
320
- //#region src/host-tree.d.ts
422
+ //#region src/lib/host-tree.d.ts
321
423
  /**
322
424
  * One directory as the tree knows it: what `/fs/list` answered, plus whether the
323
425
  * server held entries back.
@@ -365,7 +467,7 @@ declare function flattenHostTree(root: string, dirs: ReadonlyMap<string, HostDir
365
467
  */
366
468
  declare function ancestorsWithin(root: string, path: string): string[];
367
469
  //#endregion
368
- //#region src/use-host-files.d.ts
470
+ //#region src/hooks/use-host-files.d.ts
369
471
  type UseHostFileSearchResult = {
370
472
  /**
371
473
  * Whether `@file` completion is on offer at all: the session's cwd is known
@@ -453,7 +555,41 @@ type UseHostFileTreeResult = {
453
555
  */
454
556
  declare function useHostFileTree(client: WorkerDeckClient, cwd: string | undefined): UseHostFileTreeResult;
455
557
  //#endregion
456
- //#region src/use-session-info.d.ts
558
+ //#region src/hooks/use-profile-usage.d.ts
559
+ type UseProfileUsageOptions = {
560
+ /** How often to re-ask while enabled. Default 60s. */intervalMs?: number;
561
+ /** Set false to hold the poll — a panel that is off screen has nothing to
562
+ * refresh. Default true. */
563
+ enabled?: boolean;
564
+ };
565
+ type UseProfileUsageResult = {
566
+ /** The gateway's plan-usage state for this profile, or undefined when there
567
+ * is none to have: no profile, an older gateway, or nothing reported yet.
568
+ * Absent is **unknown, never 0%** — see `ProfileUsageWindow`. */
569
+ usage: ProfileUsage | undefined; /** Ask again now. */
570
+ refresh: () => void;
571
+ };
572
+ /**
573
+ * The gateway's per-profile plan usage, over REST.
574
+ *
575
+ * The session's own event stream carries a `rate_limit` reading only when the
576
+ * engine volunteers one — for claude that is at a turn's edges and nowhere else,
577
+ * so a session idle since yesterday replays yesterday's number, and a session
578
+ * opened today knows nothing of what a sibling on the same account spent an hour
579
+ * ago. `GET /profiles` answers the account-wide question, which is why this is a
580
+ * poll and not a subscription: nothing pushes it.
581
+ *
582
+ * Polling and not attaching, deliberately — a second WebSocket per surface is
583
+ * exactly what the bridge's "asks the first attached client" rule forbids, and
584
+ * this is one small GET a minute.
585
+ *
586
+ * Self-disabling on a 404, like {@link useHostFileSearch}: a gateway without the
587
+ * route will never grow one mid-session, so stop asking rather than log a miss
588
+ * every minute.
589
+ */
590
+ declare function useProfileUsage(client: WorkerDeckClient, profile: string | undefined, options?: UseProfileUsageOptions): UseProfileUsageResult;
591
+ //#endregion
592
+ //#region src/hooks/use-session-info.d.ts
457
593
  type UseSessionInfoResult = {
458
594
  info: SessionInfo | undefined; /** True until the first answer — distinguishes "still asking" from "no such session". */
459
595
  loading: boolean; /** Set when the gateway refused; `info` stays undefined. */
@@ -473,7 +609,7 @@ type UseSessionInfoResult = {
473
609
  */
474
610
  declare function useSessionInfo(client: WorkerDeckClient, sessionId: string | undefined): UseSessionInfoResult;
475
611
  //#endregion
476
- //#region src/open-files.d.ts
612
+ //#region src/lib/open-files.d.ts
477
613
  /**
478
614
  * One open file, in whatever state its read got to.
479
615
  *
@@ -610,7 +746,7 @@ declare const initialOpenFilesState: OpenFilesState;
610
746
  */
611
747
  declare function openFilesReducer(state: OpenFilesState, action: OpenFilesAction): OpenFilesState;
612
748
  //#endregion
613
- //#region src/use-open-files.d.ts
749
+ //#region src/hooks/use-open-files.d.ts
614
750
  type UseOpenFilesResult = OpenFilesState & {
615
751
  /** The focused file, resolved — what the editor renders. */active: OpenFile | undefined; /** Any tab with unsaved edits — what a close or unload guard asks. */
616
752
  hasUnsaved: boolean; /** Open a path, or focus it if it is already open. */
@@ -645,7 +781,7 @@ type UseOpenFilesResult = OpenFilesState & {
645
781
  */
646
782
  declare function useOpenFiles(client: WorkerDeckClient): UseOpenFilesResult;
647
783
  //#endregion
648
- //#region src/tool-host.d.ts
784
+ //#region src/lib/tool-host.d.ts
649
785
  /** What the host was asked to do and how it went (for UI/telemetry). */
650
786
  type ToolHostExecution = {
651
787
  executionId: string;
@@ -698,7 +834,7 @@ declare function createToolCallHost(handle: SessionHandle, options?: ToolCallHos
698
834
  dispose: () => void;
699
835
  };
700
836
  //#endregion
701
- //#region src/use-tool-host.d.ts
837
+ //#region src/hooks/use-tool-host.d.ts
702
838
  type UseToolCallHostOptions = ToolCallHostOptions & {
703
839
  /** Turn the host off without unmounting. Default true. */enabled?: boolean; /** How many recent executions to keep for rendering. Default 50. */
704
840
  historyLimit?: number;
@@ -712,7 +848,7 @@ declare function useToolCallHost(handle: SessionHandle | undefined, options?: Us
712
848
  executions: ToolHostExecution[];
713
849
  };
714
850
  //#endregion
715
- //#region src/recap.d.ts
851
+ //#region src/lib/recap.d.ts
716
852
  /**
717
853
  * "What happened while you were away", counted rather than written.
718
854
  *
@@ -764,5 +900,5 @@ declare function summarizeSince(state: RecapInput, fromIndex: number): RecapSumm
764
900
  */
765
901
  declare function recapLine(summary: RecapSummary): string | undefined;
766
902
  //#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 };
903
+ export { type AttachmentKind, type ConnectionState, type HostDirState, type HostTreeRow, type OpenFile, type OpenFilesAction, type OpenFilesState, type ProducedFileRef, type PromptToken, REPLAY_HOLD_MAX_MS, 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 UseProfileUsageOptions, type UseProfileUsageResult, type UseSessionInfoResult, type UseToolCallHostOptions, ancestorsWithin, applyEvent, attachmentKind, clearTranscriptCache, createToolCallHost, currentText, flattenHostTree, initialOpenFilesState, initialReplayTarget, initialTranscriptState, isDirty, openFilesReducer, rateLimitWindows, recapLine, scanPromptTokens, seedFromSessionInfo, staleAttach, summarizeSince, useAttachments, useClaudeSession, useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, useProfileUsage, useSessionInfo, useToolCallHost };
768
904
  //# sourceMappingURL=index.d.mts.map