@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 +10 -0
- package/build/index.d.mts +159 -23
- package/build/index.mjs +319 -35
- package/build/index.mjs.map +1 -1
- package/package.json +6 -6
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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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/
|
|
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
|
|
358
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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-
|
|
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
|