@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/build/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
2
- import { ENGINE_CAPABILITIES, PROTOCOL_VERSION } from "@workerdeck/protocol";
2
+ import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, mergeUsage, orderUsageWindows } from "@workerdeck/protocol";
3
3
  import { WorkerDeckError } from "@workerdeck/client";
4
- //#region src/transcript.ts
4
+ //#region src/lib/transcript.ts
5
5
  const initialTranscriptState = {
6
6
  status: "starting",
7
7
  capabilities: ENGINE_CAPABILITIES.claude,
@@ -34,6 +34,27 @@ function outputText(output) {
34
34
  }
35
35
  /** CLI-side command output arrives as user text wrapped in local-command tags. */
36
36
  const LOCAL_COMMAND_OUTPUT = /^<local-command-(stdout|stderr)>([\s\S]*?)<\/local-command-\1>$/;
37
+ /**
38
+ * A slash command the person ran, as the CLI writes it into the transcript:
39
+ * `<command-message>…</command-message><command-name>/wrapup</command-name>
40
+ * <command-args>…</command-args>`, in whichever order.
41
+ *
42
+ * Rendered as the command line rather than hidden. It *is* a person's turn — it
43
+ * is the reason everything after it happened — but the raw wrapper is markup
44
+ * nobody typed, and it showed up verbatim in every resumed transcript. Not
45
+ * suppressed in the runner for that same reason: hiding it would erase the
46
+ * turn's cause and, since `transcriptActivity` counts a non-synthetic user
47
+ * message as one row, silently disagree with the unread count.
48
+ */
49
+ const COMMAND_NAME = /<command-name>([\s\S]*?)<\/command-name>/;
50
+ const COMMAND_ARGS = /<command-args>([\s\S]*?)<\/command-args>/;
51
+ /** The typed command line, or undefined when this is ordinary prose. */
52
+ function slashCommandText(text) {
53
+ const name = COMMAND_NAME.exec(text)?.[1]?.trim();
54
+ if (!name) return void 0;
55
+ const args = COMMAND_ARGS.exec(text)?.[1]?.trim();
56
+ return args ? `${name} ${args}` : name;
57
+ }
37
58
  function upsert(items, item) {
38
59
  const index = items.findIndex((existing) => existing.id === item.id && existing.kind === item.kind);
39
60
  if (index === -1) return [...items, item];
@@ -65,19 +86,16 @@ function seedFromSessionInfo(state, info) {
65
86
  * The session's rate-limit windows in reading order: the session window, the
66
87
  * weekly window, then whichever per-model weekly windows it reports.
67
88
  *
68
- * Discovered rather than hardcoded the SDK's set of windows is an open union
69
- * and has grown before but ordered, so the first two always mean the same
70
- * thing. A window with no `utilization` is *unknown*, not zero, and is dropped
71
- * entirely rather than drawn as an empty bar that reads as "plenty left".
89
+ * The ordering and the drop-the-unknown rule are protocol's `orderUsageWindows`
90
+ * — the dashboard renders the same windows straight off `ProfileInfo.usage`,
91
+ * with no transcript anywhere near it, and two orderings would be one account
92
+ * described two ways. This stays as the transcript-shaped door to it.
72
93
  */
73
94
  function rateLimitWindows(state) {
74
- const all = Object.entries(state.rateLimits ?? {}).filter(([, info]) => info.utilization !== void 0).map(([key, info]) => ({
75
- key,
76
- info
77
- }));
78
- const named = ["five_hour", "seven_day"].flatMap((key) => all.filter((w) => w.key === key));
79
- const perModel = all.filter((w) => w.key.startsWith("seven_day_")).sort((a, b) => a.key.localeCompare(b.key));
80
- return [...named, ...perModel];
95
+ return orderUsageWindows(mergeUsage({
96
+ rateLimits: state.rateLimits,
97
+ updatedAt: state.rateLimitsUpdatedAt
98
+ }, void 0));
81
99
  }
82
100
  function applyEvent(state, event) {
83
101
  if (event.seq <= state.lastSeq) return state;
@@ -147,6 +165,12 @@ function applyEvent(state, event) {
147
165
  ...base,
148
166
  subscriptionType: event.subscriptionType
149
167
  };
168
+ case "conversation_reset": return {
169
+ ...base,
170
+ items: [],
171
+ contextUsage: void 0,
172
+ sdkSessionId: event.sdkSessionId ?? base.sdkSessionId
173
+ };
150
174
  case "user_message": {
151
175
  let items = base.items;
152
176
  for (const block of contentToBlocks(event.message.content)) if (block.type === "tool_result") {
@@ -158,7 +182,8 @@ function applyEvent(state, event) {
158
182
  result: {
159
183
  text: blockText(toolResult.content),
160
184
  isError
161
- }
185
+ },
186
+ ...event.patch && { patch: event.patch }
162
187
  } : item);
163
188
  } else if (block.type === "text" && !event.synthetic) {
164
189
  const text = block.text;
@@ -172,7 +197,7 @@ function applyEvent(state, event) {
172
197
  else items = upsert(items, {
173
198
  kind: "user",
174
199
  id: event.uuid ?? `user-${event.seq}`,
175
- text,
200
+ text: slashCommandText(text) ?? text,
176
201
  attachments: event.attachments
177
202
  });
178
203
  }
@@ -237,10 +262,12 @@ function applyEvent(state, event) {
237
262
  };
238
263
  }
239
264
  if (delta.delta?.type === "thinking_delta") {
265
+ const text = (base.items.find((item) => item.kind === "thinking" && item.id === STREAMING_THINKING_ID)?.text ?? "") + (delta.delta.thinking ?? "");
266
+ if (text.trim() === "") return base;
240
267
  const item = {
241
268
  kind: "thinking",
242
269
  id: STREAMING_THINKING_ID,
243
- text: (base.items.find((item) => item.kind === "thinking" && item.id === STREAMING_THINKING_ID)?.text ?? "") + (delta.delta.thinking ?? ""),
270
+ text,
244
271
  parentToolUseId: event.parentToolUseId
245
272
  };
246
273
  return {
@@ -253,7 +280,14 @@ function applyEvent(state, event) {
253
280
  case "turn_result": return {
254
281
  ...base,
255
282
  totalCostUsd: event.totalCostUsd,
256
- items: [...base.items, {
283
+ items: [...base.items.map((item) => item.kind === "assistant_text" && item.id === STREAMING_ID ? {
284
+ ...item,
285
+ id: `text-${event.seq}`,
286
+ streaming: false
287
+ } : item.kind === "thinking" && item.id === STREAMING_THINKING_ID ? {
288
+ ...item,
289
+ id: `thinking-${event.seq}`
290
+ } : item), {
257
291
  kind: "turn_result",
258
292
  id: `turn-${event.seq}`,
259
293
  subtype: event.subtype,
@@ -338,38 +372,204 @@ function applyEvent(state, event) {
338
372
  }
339
373
  }
340
374
  //#endregion
341
- //#region src/use-session.ts
375
+ //#region src/lib/transcript-cache.ts
376
+ /**
377
+ * Module-scope cache of detached transcript states, so switching back to a
378
+ * recently viewed session paints its transcript in the mount frame and
379
+ * re-attaches with `afterSeq: lastSeq` — the wire replays only what happened
380
+ * while the panel was away, instead of the whole event log.
381
+ *
382
+ * Module-scope for the same reason `useSessions` and the watermarks are: the
383
+ * consumers that need it (the VS Code panel, the dashboard's session route)
384
+ * remount `SessionPanel` per session, so any per-hook copy would die with the
385
+ * unmount that is the entire point of surviving.
386
+ *
387
+ * Entries are the same `TranscriptState` objects the reducer held — retention,
388
+ * not duplication — and the bound is what keeps retention from becoming a
389
+ * leak. Eviction is least-recently-STORED: every detach stores, so store
390
+ * recency is viewing recency, and reads don't need to reorder.
391
+ *
392
+ * Keys come from {@link transcriptCacheKey} and carry the client's
393
+ * `identityKey` (gateway + auth headers), never the session id alone: a
394
+ * session id is unique only within one gateway, and an entry must never be
395
+ * readable through a client speaking as a different principal.
396
+ */
397
+ /**
398
+ * How many detached transcripts stay warm.
399
+ *
400
+ * Five covers the working set the feature exists for — an operator alternating
401
+ * between the handful of sessions that are simultaneously working or awaiting
402
+ * them — while keeping the pathological case (five `perf`-fixture-sized
403
+ * transcripts of ~4k items each) in the tens of megabytes, no more than a few
404
+ * times what the one mounted panel already holds. Too small degrades to
405
+ * today's behaviour (a replay on switch-back); too large is memory held
406
+ * forever in a webview — the asymmetry favours small.
407
+ */
408
+ const MAX_ENTRIES = 5;
409
+ const entries = /* @__PURE__ */ new Map();
410
+ /** Cache key for one session as seen through one (gateway, principal). The
411
+ * NUL separator is unambiguous: the identity key is `JSON.stringify` output,
412
+ * which escapes control characters, so no two (identity, session) pairs can
413
+ * spell the same key. */
414
+ function transcriptCacheKey(client, sessionId) {
415
+ return `${client.identityKey}\u0000${sessionId}`;
416
+ }
417
+ function readTranscriptCache(key) {
418
+ return entries.get(key);
419
+ }
420
+ function writeTranscriptCache(key, state) {
421
+ entries.delete(key);
422
+ entries.set(key, state);
423
+ if (entries.size > MAX_ENTRIES) {
424
+ const oldest = entries.keys().next().value;
425
+ if (oldest !== void 0) entries.delete(oldest);
426
+ }
427
+ }
428
+ function deleteTranscriptCache(key) {
429
+ entries.delete(key);
430
+ }
431
+ /**
432
+ * Drop every cached transcript. For an embedder changing principals in place
433
+ * (a logout that keeps the page alive) — entries are unreachable through the
434
+ * new principal's client either way, but scrubbing them is free and final.
435
+ */
436
+ function clearTranscriptCache() {
437
+ entries.clear();
438
+ }
439
+ //#endregion
440
+ //#region src/hooks/use-session.ts
342
441
  /** Session events drive the reducer; the attach snapshot seeds fields (permission
343
442
  * mode, model) that a promptless session's event stream doesn't carry yet. */
344
443
  function reduce(state, action) {
444
+ if (action.type === "transcript_seed") return action.state;
345
445
  return action.type === "attached" ? seedFromSessionInfo(state, action.session) : applyEvent(state, action);
346
446
  }
347
447
  /** Failed attempts in a row before "reconnecting…" stops being the honest word.
348
448
  * Three is ~3.5s of backoff — past a blip. Matches the iOS client. */
349
449
  const OFFLINE_AFTER_ATTEMPTS = 3;
450
+ /**
451
+ * The seq the initial attach replay ends on, or undefined when there is nothing
452
+ * to hold for.
453
+ *
454
+ * This is an exact signal, not a heuristic: the `attached` frame is sent before
455
+ * any replayed `event` frame and carries the runner's seq at attach time
456
+ * (`session.lastSeq`), so the moment the frame arrives the client knows
457
+ * precisely which seq the replay ends on. Every runner keeps its full event log
458
+ * and always delivers the highest-seq event on a fresh replay (the
459
+ * `conversation_reset` skip is strictly-below-the-reset, and the reset's seq is
460
+ * itself ≤ lastSeq), so `TranscriptState.lastSeq >= target` means the replay
461
+ * has landed. No quiet window or other arrival heuristic belongs here.
462
+ *
463
+ * Only a FRESH attach yields a target (`replayingFrom === 0`): a reconnect
464
+ * replays into a transcript the reader is already looking at, and blanking it
465
+ * mid-turn would be a worse bug than the flicker the hold exists to fix. A
466
+ * brand-new session (`lastSeq === 0`) has nothing to replay and never holds.
467
+ */
468
+ function initialReplayTarget(frame) {
469
+ return frame.replayingFrom === 0 && frame.session.lastSeq > 0 ? frame.session.lastSeq : void 0;
470
+ }
471
+ /**
472
+ * Whether an attach frame describes a DIFFERENT event log than the transcript
473
+ * `held` was built from — in which case attaching with `afterSeq: held.lastSeq`
474
+ * has already gone wrong: every event in the new log has seq ≤ afterSeq, so
475
+ * nothing will ever arrive and the stale rows would stand forever, with no
476
+ * error. The only recovery is to forget the state and re-attach from seq 0.
477
+ *
478
+ * A log resets on routine paths, not corner cases: a dormant session
479
+ * (claude/codex surviving a gateway restart) is rebuilt with a brand-new
480
+ * runner whose log starts at 0 and refills from the engine's own store. Two
481
+ * checks, each of which the other misses:
482
+ *
483
+ * - `session.lastSeq < held.lastSeq` — the server's log is shorter than what
484
+ * we hold. Within one log seq only grows, so this is proof of a reset. It
485
+ * catches a rebuilt runner that has not yet re-run far — but not one whose
486
+ * backfill already advanced past us.
487
+ * - `session.createdAt !== held.session.createdAt` — a different runner
488
+ * incarnation. The claude and codex runners stamp `Date.now()` at
489
+ * construction, so a dormant rebuild always changes it; the provider runner
490
+ * restores `createdAt` from its snapshot precisely when it also restores
491
+ * the event log and seq counter (ai-sdk-runner's `#restore`), so equality
492
+ * truthfully means "same log" for every engine.
493
+ *
494
+ * A full replay (`replayingFrom === 0`) is never stale — it carries the whole
495
+ * log, so the caller heals by resetting state and applying it — and holding
496
+ * nothing (`held.lastSeq === 0`) has nothing to be stale about. That first
497
+ * clause is also what makes the recovery loop-proof: the re-attach from 0 can
498
+ * never re-trigger this predicate.
499
+ *
500
+ * Not cache-specific: a live handle reconnecting after a gateway restart
501
+ * re-attaches with its own advanced `afterSeq` against the rebuilt log and
502
+ * hits the identical silence, so the hook applies this to every attach frame.
503
+ */
504
+ function staleAttach(frame, held) {
505
+ if (frame.replayingFrom === 0 || held.lastSeq === 0) return false;
506
+ if (frame.session.lastSeq < held.lastSeq) return true;
507
+ return held.session !== void 0 && frame.session.createdAt !== held.session.createdAt;
508
+ }
509
+ /**
510
+ * Backstop for the replay hold: if the target seq has not landed after this
511
+ * long, reveal what has arrived. On a healthy attach the target is always
512
+ * reached (see {@link initialReplayTarget}); the backstop exists because a
513
+ * blank panel forever would be a much worse failure than a visible stream, so
514
+ * the hold is bounded no matter what a future filter or a lossy path does. It
515
+ * runs from the attach — a per-event re-arm would be a quiet-window heuristic
516
+ * in a new costume.
517
+ */
518
+ const REPLAY_HOLD_MAX_MS = 1500;
350
519
  /** Attach to a session and maintain live transcript state. Detaches on unmount. */
351
520
  function useClaudeSession(client, sessionId, options) {
352
- const [state, dispatch] = useReducer(reduce, initialTranscriptState);
521
+ const [state, dispatch] = useReducer(reduce, void 0, () => (options?.cacheTranscript !== false && sessionId !== void 0 ? readTranscriptCache(transcriptCacheKey(client, sessionId)) : void 0) ?? initialTranscriptState);
353
522
  const [connection, setConnection] = useState("reconnecting");
354
523
  const [protocolMismatch, setProtocolMismatch] = useState();
524
+ /** Where the current attach's replay ends, while one is being held for. */
525
+ const [replayTarget, setReplayTarget] = useState();
526
+ /** Bumped to force a fresh attach from seq 0 after a stale-log detection. */
527
+ const [resyncSeq, setResyncSeq] = useState(0);
355
528
  const [handleState, setHandleState] = useState();
356
529
  const handleRef = useRef(null);
357
- const onProtocolErrorRef = useRef(options?.onProtocolError);
358
- onProtocolErrorRef.current = options?.onProtocolError;
530
+ const optionsRef = useRef(options);
531
+ optionsRef.current = options;
532
+ const stateRef = useRef(state);
533
+ stateRef.current = state;
534
+ const seededForRef = useRef(`0:${sessionId === void 0 ? "" : transcriptCacheKey(client, sessionId)}`);
535
+ const skipCacheRef = useRef(false);
359
536
  useEffect(() => {
360
537
  if (!sessionId) return;
361
- const handle = client.attach(sessionId);
538
+ const cache = optionsRef.current?.cacheTranscript !== false;
539
+ const key = transcriptCacheKey(client, sessionId);
540
+ const seedToken = `${resyncSeq}:${key}`;
541
+ const warm = cache && !skipCacheRef.current ? readTranscriptCache(key) : void 0;
542
+ skipCacheRef.current = false;
543
+ let held;
544
+ if (seededForRef.current === seedToken) held = stateRef.current;
545
+ else {
546
+ held = warm ?? initialTranscriptState;
547
+ dispatch({
548
+ type: "transcript_seed",
549
+ state: held
550
+ });
551
+ seededForRef.current = seedToken;
552
+ }
553
+ const handle = client.attach(sessionId, held.lastSeq > 0 ? { afterSeq: held.lastSeq } : {});
362
554
  handleRef.current = handle;
363
555
  setHandleState(handle);
364
556
  const offEvent = handle.on("event", (event) => dispatch(event));
365
557
  const offAttached = handle.on("attached", (frame) => {
558
+ if (staleAttach(frame, stateRef.current)) {
559
+ offEvent();
560
+ deleteTranscriptCache(key);
561
+ skipCacheRef.current = true;
562
+ setResyncSeq((n) => n + 1);
563
+ return;
564
+ }
366
565
  dispatch(frame);
566
+ setReplayTarget(initialReplayTarget(frame));
367
567
  setProtocolMismatch(frame.protocolVersion === PROTOCOL_VERSION ? void 0 : frame.protocolVersion);
368
568
  });
369
569
  const offConn = handle.on("connectionChange", (open) => setConnection(open ? "live" : "reconnecting"));
370
570
  const offRetry = handle.on("reconnectAttempt", (attempts) => setConnection(attempts >= OFFLINE_AFTER_ATTEMPTS ? "offline" : "reconnecting"));
371
571
  const offProtocolError = handle.on("protocolError", (message) => {
372
- onProtocolErrorRef.current?.(message);
572
+ optionsRef.current?.onProtocolError?.(message);
373
573
  });
374
574
  return () => {
375
575
  offEvent();
@@ -382,15 +582,32 @@ function useClaudeSession(client, sessionId, options) {
382
582
  setHandleState(void 0);
383
583
  setConnection("reconnecting");
384
584
  setProtocolMismatch(void 0);
585
+ setReplayTarget(void 0);
586
+ const parting = stateRef.current;
587
+ if (cache && !skipCacheRef.current && parting.lastSeq > 0 && parting.session) writeTranscriptCache(key, parting);
385
588
  };
386
- }, [client, sessionId]);
589
+ }, [
590
+ client,
591
+ sessionId,
592
+ resyncSeq
593
+ ]);
594
+ useEffect(() => {
595
+ if (replayTarget === void 0) return;
596
+ const timer = setTimeout(() => setReplayTarget(void 0), REPLAY_HOLD_MAX_MS);
597
+ return () => clearTimeout(timer);
598
+ }, [replayTarget]);
599
+ useEffect(() => {
600
+ if (replayTarget !== void 0 && state.lastSeq >= replayTarget) setReplayTarget(void 0);
601
+ }, [replayTarget, state.lastSeq]);
387
602
  const models = useProfileModelFallback(client, sessionId, state);
388
603
  const connected = connection === "live";
604
+ const replaying = replayTarget !== void 0 && state.lastSeq < replayTarget;
389
605
  const reconnectNow = useCallback(() => handleRef.current?.reconnectNow(), []);
390
606
  return useMemo(() => ({
391
607
  state,
392
608
  connected,
393
609
  connection,
610
+ replaying,
394
611
  protocolMismatch,
395
612
  models,
396
613
  effectiveModel: state.model ?? state.defaultModel,
@@ -407,6 +624,7 @@ function useClaudeSession(client, sessionId, options) {
407
624
  state,
408
625
  connected,
409
626
  connection,
627
+ replaying,
410
628
  protocolMismatch,
411
629
  models,
412
630
  handleState,
@@ -445,7 +663,7 @@ function useProfileModelFallback(client, sessionId, state) {
445
663
  return hasReported ? reported : catalog;
446
664
  }
447
665
  //#endregion
448
- //#region src/use-attachments.ts
666
+ //#region src/hooks/use-attachments.ts
449
667
  /**
450
668
  * How a media type reaches a model, in the capability record's vocabulary.
451
669
  * `undefined` means this build can't classify it — the upload still goes,
@@ -681,7 +899,7 @@ async function prepare(file) {
681
899
  }
682
900
  }
683
901
  //#endregion
684
- //#region src/prompt-tokens.ts
902
+ //#region src/lib/prompt-tokens.ts
685
903
  /** Characters a command name may contain after the slash. Deliberately excludes
686
904
  * `/`, so an absolute path pasted into a message (`/Users/me/…`) is not mistaken
687
905
  * for a command; `:` is in because namespaced skills (`dev:wrapup`) are spelled
@@ -731,7 +949,7 @@ function scanPromptTokens(text) {
731
949
  return tokens;
732
950
  }
733
951
  //#endregion
734
- //#region src/host-tree.ts
952
+ //#region src/lib/host-tree.ts
735
953
  /**
736
954
  * Flatten the loaded directories into the rows the tree shows.
737
955
  *
@@ -796,7 +1014,7 @@ function ancestorsWithin(root, path) {
796
1014
  return out;
797
1015
  }
798
1016
  //#endregion
799
- //#region src/use-host-files.ts
1017
+ //#region src/hooks/use-host-files.ts
800
1018
  /**
801
1019
  * Fuzzy file search rooted at a session's working directory — what an `@file`
802
1020
  * picker needs.
@@ -978,7 +1196,73 @@ function useHostFileTree(client, cwd) {
978
1196
  };
979
1197
  }
980
1198
  //#endregion
981
- //#region src/use-session-info.ts
1199
+ //#region src/hooks/use-profile-usage.ts
1200
+ /**
1201
+ * The gateway's per-profile plan usage, over REST.
1202
+ *
1203
+ * The session's own event stream carries a `rate_limit` reading only when the
1204
+ * engine volunteers one — for claude that is at a turn's edges and nowhere else,
1205
+ * so a session idle since yesterday replays yesterday's number, and a session
1206
+ * opened today knows nothing of what a sibling on the same account spent an hour
1207
+ * ago. `GET /profiles` answers the account-wide question, which is why this is a
1208
+ * poll and not a subscription: nothing pushes it.
1209
+ *
1210
+ * Polling and not attaching, deliberately — a second WebSocket per surface is
1211
+ * exactly what the bridge's "asks the first attached client" rule forbids, and
1212
+ * this is one small GET a minute.
1213
+ *
1214
+ * Self-disabling on a 404, like {@link useHostFileSearch}: a gateway without the
1215
+ * route will never grow one mid-session, so stop asking rather than log a miss
1216
+ * every minute.
1217
+ */
1218
+ function useProfileUsage(client, profile, options = {}) {
1219
+ const { intervalMs = 6e4, enabled = true } = options;
1220
+ const [usage, setUsage] = useState();
1221
+ const [unsupported, setUnsupported] = useState(false);
1222
+ const [nonce, setNonce] = useState(0);
1223
+ const refresh = useCallback(() => setNonce((n) => n + 1), []);
1224
+ useEffect(() => setUsage(void 0), [client, profile]);
1225
+ const alive = useRef(true);
1226
+ useEffect(() => {
1227
+ alive.current = true;
1228
+ return () => {
1229
+ alive.current = false;
1230
+ };
1231
+ }, []);
1232
+ useEffect(() => {
1233
+ if (!profile || !enabled || unsupported) return;
1234
+ let cancelled = false;
1235
+ const load = () => {
1236
+ if (globalThis.document?.hidden) return;
1237
+ client.listProfiles().then((res) => {
1238
+ if (cancelled || !alive.current) return;
1239
+ setUsage(res.profiles.find((p) => p.name === profile)?.usage);
1240
+ }).catch((e) => {
1241
+ if (cancelled || !alive.current) return;
1242
+ if (e instanceof WorkerDeckError && e.status === 404) setUnsupported(true);
1243
+ });
1244
+ };
1245
+ load();
1246
+ const timer = setInterval(load, intervalMs);
1247
+ return () => {
1248
+ cancelled = true;
1249
+ clearInterval(timer);
1250
+ };
1251
+ }, [
1252
+ client,
1253
+ profile,
1254
+ enabled,
1255
+ unsupported,
1256
+ intervalMs,
1257
+ nonce
1258
+ ]);
1259
+ return {
1260
+ usage,
1261
+ refresh
1262
+ };
1263
+ }
1264
+ //#endregion
1265
+ //#region src/hooks/use-session-info.ts
982
1266
  /**
983
1267
  * The registry's record of one session, over REST.
984
1268
  *
@@ -1026,7 +1310,7 @@ function useSessionInfo(client, sessionId) {
1026
1310
  };
1027
1311
  }
1028
1312
  //#endregion
1029
- //#region src/open-files.ts
1313
+ //#region src/lib/open-files.ts
1030
1314
  /** Whether a tab has edits that are not on disk. Derived, so typing something
1031
1315
  * and undoing it back leaves the tab clean — which is what an editor should do
1032
1316
  * and what a boolean flag set on first keystroke would get wrong. */
@@ -1174,7 +1458,7 @@ function baseName(path) {
1174
1458
  return trimmed.slice(trimmed.lastIndexOf("/") + 1) || trimmed || path;
1175
1459
  }
1176
1460
  //#endregion
1177
- //#region src/use-open-files.ts
1461
+ //#region src/hooks/use-open-files.ts
1178
1462
  /**
1179
1463
  * The open-file tabs of a workspace: which files are open, which one is focused,
1180
1464
  * the bytes behind each, and the edits on top of them.
@@ -1352,7 +1636,7 @@ function useOpenFiles(client) {
1352
1636
  };
1353
1637
  }
1354
1638
  //#endregion
1355
- //#region src/tool-host.ts
1639
+ //#region src/lib/tool-host.ts
1356
1640
  /**
1357
1641
  * Answers server-bridged tool calls by executing them in this browser tab.
1358
1642
  * Framework-free — {@link useToolCallHost} is a thin React wrapper.
@@ -1480,7 +1764,7 @@ async function defaultLoadEngine() {
1480
1764
  return sandbox.loadEngine(variant);
1481
1765
  }
1482
1766
  //#endregion
1483
- //#region src/use-tool-host.ts
1767
+ //#region src/hooks/use-tool-host.ts
1484
1768
  /**
1485
1769
  * React wrapper around {@link createToolCallHost}: subscribes while mounted and
1486
1770
  * exposes recent executions for rendering. All the logic lives in the
@@ -1522,7 +1806,7 @@ function useToolCallHost(handle, options = {}) {
1522
1806
  return { executions };
1523
1807
  }
1524
1808
  //#endregion
1525
- //#region src/recap.ts
1809
+ //#region src/lib/recap.ts
1526
1810
  /**
1527
1811
  * Summarize the items from `fromIndex` onward — the boundary being the number
1528
1812
  * of items that existed when the session was last looked at.
@@ -1601,6 +1885,6 @@ function plural(count, one, many = `${one}s`) {
1601
1885
  return `${count} ${count === 1 ? one : many}`;
1602
1886
  }
1603
1887
  //#endregion
1604
- export { ancestorsWithin, applyEvent, attachmentKind, createToolCallHost, currentText, flattenHostTree, initialOpenFilesState, initialTranscriptState, isDirty, openFilesReducer, rateLimitWindows, recapLine, scanPromptTokens, seedFromSessionInfo, summarizeSince, useAttachments, useClaudeSession, useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, useSessionInfo, useToolCallHost };
1888
+ export { REPLAY_HOLD_MAX_MS, 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 };
1605
1889
 
1606
1890
  //# sourceMappingURL=index.mjs.map