@workerdeck/react 2.15.0 → 3.1.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,6 +1,6 @@
1
- import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
2
- import { DEFAULT_PRICING, ENGINE_CAPABILITIES, PEER_MENTION_SIGIL, PROMPT_SENTENCE_TAIL, PROTOCOL_VERSION, isPeerMentionBody, mergePricing, mergeUsage, orderUsageWindows, peerMentionKey, peerMentionSlug, sessionLabel } from "@workerdeck/protocol";
1
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
3
2
  import { WorkerDeckError } from "@workerdeck/client";
3
+ import { DEFAULT_PRICING, ENGINE_CAPABILITIES, PEER_MENTION_SIGIL, PROMPT_SENTENCE_TAIL, PROTOCOL_VERSION, errorMessage, isPeerMentionBody, mergePricing, mergeUsage, orderUsageWindows, peerMentionKey, peerMentionSlug, sessionLabel } from "@workerdeck/protocol";
4
4
  //#region src/lib/transcript.ts
5
5
  const initialTranscriptState = {
6
6
  status: "starting",
@@ -94,13 +94,22 @@ function slashCommandText(text) {
94
94
  const args = COMMAND_ARGS.exec(text)?.[1]?.trim();
95
95
  return args ? `${name} ${args}` : name;
96
96
  }
97
- function upsert(items, item) {
98
- const index = items.findIndex((existing) => existing.id === item.id && existing.kind === item.kind);
97
+ function findLastIndexOf(items, kind, id) {
98
+ for (let index = items.length - 1; index >= 0; index--) {
99
+ const item = items[index];
100
+ if (item?.id === id && item.kind === kind) return index;
101
+ }
102
+ return -1;
103
+ }
104
+ function replaceAt(items, index, item) {
99
105
  if (index === -1) return [...items, item];
100
106
  const next = [...items];
101
107
  next[index] = item;
102
108
  return next;
103
109
  }
110
+ function upsert(items, item) {
111
+ return replaceAt(items, items.findIndex((existing) => existing.id === item.id && existing.kind === item.kind), item);
112
+ }
104
113
  function seedFromSessionInfo(state, info) {
105
114
  const engine = info.engine ?? state.engine;
106
115
  return {
@@ -372,21 +381,23 @@ function applyEvent(state, event) {
372
381
  if (delta.type !== "content_block_delta") return base;
373
382
  if (delta.delta?.type === "text_delta") {
374
383
  const id = streamingTextId(event.parentToolUseId);
384
+ const index = findLastIndexOf(base.items, "assistant_text", id);
375
385
  const item = {
376
386
  kind: "assistant_text",
377
387
  id,
378
- text: (base.items.find((item) => item.kind === "assistant_text" && item.id === id)?.text ?? "") + (delta.delta.text ?? ""),
388
+ text: ((index === -1 ? void 0 : base.items[index])?.text ?? "") + (delta.delta.text ?? ""),
379
389
  streaming: true,
380
390
  parentToolUseId: event.parentToolUseId
381
391
  };
382
392
  return {
383
393
  ...base,
384
- items: upsert(base.items, item)
394
+ items: replaceAt(base.items, index, item)
385
395
  };
386
396
  }
387
397
  if (delta.delta?.type === "thinking_delta") {
388
398
  const id = streamingThinkingId(event.parentToolUseId);
389
- const text = (base.items.find((item) => item.kind === "thinking" && item.id === id)?.text ?? "") + (delta.delta.thinking ?? "");
399
+ const index = findLastIndexOf(base.items, "thinking", id);
400
+ const text = ((index === -1 ? void 0 : base.items[index])?.text ?? "") + (delta.delta.thinking ?? "");
390
401
  if (text.trim() === "") return base;
391
402
  const item = {
392
403
  kind: "thinking",
@@ -396,7 +407,7 @@ function applyEvent(state, event) {
396
407
  };
397
408
  return {
398
409
  ...base,
399
- items: upsert(base.items, item)
410
+ items: replaceAt(base.items, index, item)
400
411
  };
401
412
  }
402
413
  return base;
@@ -697,8 +708,8 @@ function useClaudeSession(client, sessionId, options) {
697
708
  text
698
709
  });
699
710
  return true;
700
- } catch {
701
- dispatch({
711
+ } catch (error) {
712
+ if (isNotFound(error)) dispatch({
702
713
  type: "transcript_hydrate_shell",
703
714
  shellId,
704
715
  shell: void 0
@@ -707,8 +718,9 @@ function useClaudeSession(client, sessionId, options) {
707
718
  }
708
719
  }, [client, sessionId]);
709
720
  const verifyShell = useCallback(async (shellId) => {
710
- if (!sessionId || verifiedRef.current.has(shellId)) return false;
711
- verifiedRef.current.add(shellId);
721
+ const verified = verifiedRef.current;
722
+ if (!sessionId || verified.has(shellId)) return false;
723
+ verified.add(shellId);
712
724
  try {
713
725
  const record = await client.getShell(sessionId, shellId);
714
726
  dispatch({
@@ -717,12 +729,13 @@ function useClaudeSession(client, sessionId, options) {
717
729
  shell: record
718
730
  });
719
731
  return true;
720
- } catch {
721
- dispatch({
732
+ } catch (error) {
733
+ if (isNotFound(error)) dispatch({
722
734
  type: "transcript_hydrate_shell",
723
735
  shellId,
724
736
  shell: void 0
725
737
  });
738
+ else verified.delete(shellId);
726
739
  return false;
727
740
  }
728
741
  }, [client, sessionId]);
@@ -740,6 +753,31 @@ function useClaudeSession(client, sessionId, options) {
740
753
  return false;
741
754
  }
742
755
  }, [client, sessionId]);
756
+ const setShellAgentWrite = useCallback(async (shellId, enabled) => {
757
+ if (!sessionId) return false;
758
+ try {
759
+ const record = await client.setShellAgentWrite(sessionId, shellId, enabled);
760
+ dispatch({
761
+ type: "transcript_hydrate_shell",
762
+ shellId,
763
+ shell: record
764
+ });
765
+ return true;
766
+ } catch {
767
+ return false;
768
+ }
769
+ }, [client, sessionId]);
770
+ const actions = useMemo(() => ({
771
+ send: (text, attachmentIds) => handleRef.current?.send(text, attachmentIds),
772
+ approve: (requestId, updatedInput) => handleRef.current?.approve(requestId, updatedInput),
773
+ deny: (requestId, message, interrupt) => handleRef.current?.deny(requestId, message, interrupt),
774
+ interrupt: () => handleRef.current?.interrupt(),
775
+ clearContext: () => handleRef.current?.clearContext(),
776
+ runShell: (command) => handleRef.current?.runShell(command),
777
+ setPermissionMode: (mode) => handleRef.current?.setPermissionMode(mode),
778
+ setModel: (model) => handleRef.current?.setModel(model),
779
+ closeSession: () => handleRef.current?.closeSession()
780
+ }), []);
743
781
  return useMemo(() => ({
744
782
  state,
745
783
  connected,
@@ -751,20 +789,13 @@ function useClaudeSession(client, sessionId, options) {
751
789
  models,
752
790
  effectiveModel: state.model ?? state.defaultModel,
753
791
  handle: handleState,
754
- send: (text, attachmentIds) => handleRef.current?.send(text, attachmentIds),
755
- approve: (requestId, updatedInput) => handleRef.current?.approve(requestId, updatedInput),
756
- deny: (requestId, message, interrupt) => handleRef.current?.deny(requestId, message, interrupt),
757
- interrupt: () => handleRef.current?.interrupt(),
758
- clearContext: () => handleRef.current?.clearContext(),
759
- runShell: (command) => handleRef.current?.runShell(command),
760
- setPermissionMode: (mode) => handleRef.current?.setPermissionMode(mode),
761
- setModel: (model) => handleRef.current?.setModel(model),
762
- closeSession: () => handleRef.current?.closeSession(),
792
+ ...actions,
763
793
  reconnectNow,
764
794
  loadFullResult,
765
795
  loadShellOutput,
766
796
  verifyShell,
767
- killShell
797
+ killShell,
798
+ setShellAgentWrite
768
799
  }), [
769
800
  state,
770
801
  connected,
@@ -775,13 +806,18 @@ function useClaudeSession(client, sessionId, options) {
775
806
  pricing,
776
807
  models,
777
808
  handleState,
809
+ actions,
778
810
  reconnectNow,
779
811
  loadFullResult,
780
812
  loadShellOutput,
781
813
  verifyShell,
782
- killShell
814
+ killShell,
815
+ setShellAgentWrite
783
816
  ]);
784
817
  }
818
+ function isNotFound(error) {
819
+ return error instanceof WorkerDeckError && error.status === 404;
820
+ }
785
821
  function useProfileModelFallback(client, sessionId, state) {
786
822
  const [catalog, setCatalog] = useState([]);
787
823
  const profile = state.session?.profile;
@@ -825,7 +861,7 @@ function clearProfileUsageCache() {
825
861
  const KEY = "workerdeck.drafts.v1";
826
862
  const MAX_DRAFTS = 20;
827
863
  let memory;
828
- function storage() {
864
+ function storage$1() {
829
865
  try {
830
866
  return globalThis.localStorage;
831
867
  } catch {
@@ -835,7 +871,7 @@ function storage() {
835
871
  function load() {
836
872
  if (memory) return memory;
837
873
  memory = {};
838
- const raw = storage()?.getItem(KEY);
874
+ const raw = storage$1()?.getItem(KEY);
839
875
  if (raw) try {
840
876
  const parsed = JSON.parse(raw);
841
877
  for (const [key, draft] of Object.entries(parsed)) if (typeof draft?.text === "string" && typeof draft.savedAt === "number") memory[key] = draft;
@@ -849,7 +885,7 @@ function persist(drafts) {
849
885
  for (const [key] of entries.slice(MAX_DRAFTS)) delete drafts[key];
850
886
  }
851
887
  try {
852
- storage()?.setItem(KEY, JSON.stringify(drafts));
888
+ storage$1()?.setItem(KEY, JSON.stringify(drafts));
853
889
  } catch {}
854
890
  }
855
891
  function draftKey(client, sessionId) {
@@ -872,7 +908,7 @@ function writeDraft(key, text, now = Date.now()) {
872
908
  function clearDrafts() {
873
909
  memory = {};
874
910
  try {
875
- storage()?.removeItem(KEY);
911
+ storage$1()?.removeItem(KEY);
876
912
  } catch {}
877
913
  }
878
914
  //#endregion
@@ -935,7 +971,7 @@ function useAttachments(client, sessionId, { capabilities, engine }) {
935
971
  } catch (e) {
936
972
  patch(key, {
937
973
  status: "failed",
938
- error: e instanceof Error ? e.message : "Upload failed"
974
+ error: errorMessage(e, "Upload failed")
939
975
  });
940
976
  }
941
977
  })();
@@ -1124,6 +1160,58 @@ function useAliveRef() {
1124
1160
  function isRouteUnsupported(e) {
1125
1161
  return e instanceof WorkerDeckError && e.status === 404;
1126
1162
  }
1163
+ function useAsync(load, deps, { enabled = true, pollMs } = {}) {
1164
+ const [data, setData] = useState();
1165
+ const [error, setError] = useState();
1166
+ const [loading, setLoading] = useState(enabled);
1167
+ const generation = useRef(0);
1168
+ const loadRef = useRef(load);
1169
+ loadRef.current = load;
1170
+ const run = useCallback(async () => {
1171
+ const id = ++generation.current;
1172
+ setLoading(true);
1173
+ try {
1174
+ const next = await loadRef.current();
1175
+ if (id === generation.current) {
1176
+ setData(() => next);
1177
+ setError(void 0);
1178
+ }
1179
+ } catch (e) {
1180
+ if (id === generation.current) setError(e);
1181
+ } finally {
1182
+ if (id === generation.current) setLoading(false);
1183
+ }
1184
+ }, []);
1185
+ useEffect(() => {
1186
+ setData(void 0);
1187
+ setError(void 0);
1188
+ }, deps);
1189
+ useEffect(() => {
1190
+ if (!enabled) {
1191
+ generation.current++;
1192
+ setLoading(false);
1193
+ return;
1194
+ }
1195
+ run();
1196
+ const timer = pollMs ? setInterval(() => void run(), pollMs) : void 0;
1197
+ return () => {
1198
+ generation.current++;
1199
+ clearInterval(timer);
1200
+ };
1201
+ }, [
1202
+ enabled,
1203
+ pollMs,
1204
+ run,
1205
+ ...deps
1206
+ ]);
1207
+ return {
1208
+ data,
1209
+ error,
1210
+ loading,
1211
+ reload: run,
1212
+ setData
1213
+ };
1214
+ }
1127
1215
  //#endregion
1128
1216
  //#region src/lib/host-tree.ts
1129
1217
  function flattenHostTree(root, dirs, expanded) {
@@ -1271,7 +1359,7 @@ function useHostFileTree(client, cwd) {
1271
1359
  setUnsupported(true);
1272
1360
  return;
1273
1361
  }
1274
- setError(e instanceof Error ? e.message : "Could not read that directory");
1362
+ setError(errorMessage(e, "Could not read that directory"));
1275
1363
  });
1276
1364
  }, [client, unsupported]);
1277
1365
  useEffect(() => {
@@ -1321,7 +1409,7 @@ function useHostFileTree(client, cwd) {
1321
1409
  }
1322
1410
  //#endregion
1323
1411
  //#region src/hooks/use-peer-sessions.ts
1324
- const EMPTY = {
1412
+ const EMPTY$1 = {
1325
1413
  available: false,
1326
1414
  peers: [],
1327
1415
  names: []
@@ -1354,7 +1442,7 @@ function usePeerSessions(client, sessionId, enabled = true) {
1354
1442
  unsupported
1355
1443
  ]);
1356
1444
  return useMemo(() => {
1357
- if (!enabled || unsupported || !sessions) return EMPTY;
1445
+ if (!enabled || unsupported || !sessions) return EMPTY$1;
1358
1446
  const peers = sessions.filter((info) => info.id !== sessionId).sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt)).map((info) => ({
1359
1447
  id: info.id,
1360
1448
  slug: peerMentionSlug(info.title, info.id),
@@ -1408,6 +1496,111 @@ function useProjectIcons(rows, clientFor) {
1408
1496
  return resolved;
1409
1497
  }
1410
1498
  //#endregion
1499
+ //#region src/lib/bookmark-store.ts
1500
+ const BOOKMARKS_STORAGE_KEY = "workerdeck.bookmarks.v1";
1501
+ const EMPTY = Object.freeze([]);
1502
+ const listeners = /* @__PURE__ */ new Set();
1503
+ let loaded;
1504
+ let detachStorageEvent;
1505
+ function storage() {
1506
+ try {
1507
+ return globalThis.localStorage ?? void 0;
1508
+ } catch {
1509
+ return;
1510
+ }
1511
+ }
1512
+ function storageEvents() {
1513
+ const target = globalThis.window;
1514
+ return typeof target?.addEventListener === "function" ? target : void 0;
1515
+ }
1516
+ function readStored() {
1517
+ try {
1518
+ const raw = storage()?.getItem(BOOKMARKS_STORAGE_KEY);
1519
+ if (raw == null) return {};
1520
+ const parsed = JSON.parse(raw);
1521
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
1522
+ const map = {};
1523
+ for (const [key, value] of Object.entries(parsed)) if (Array.isArray(value)) map[key] = value.filter((id) => typeof id === "string");
1524
+ return map;
1525
+ } catch {
1526
+ return;
1527
+ }
1528
+ }
1529
+ function sameIds(a, b) {
1530
+ return a.length === b.length && a.every((id, index) => id === b[index]);
1531
+ }
1532
+ function adopt(next) {
1533
+ const previous = loaded ?? {};
1534
+ const merged = {};
1535
+ for (const [key, ids] of Object.entries(next)) {
1536
+ const held = previous[key];
1537
+ merged[key] = held && sameIds(held, ids) ? held : ids;
1538
+ }
1539
+ loaded = merged;
1540
+ }
1541
+ function current() {
1542
+ if (loaded === void 0) adopt(readStored() ?? {});
1543
+ return loaded ?? {};
1544
+ }
1545
+ function notify() {
1546
+ for (const listener of listeners) listener();
1547
+ }
1548
+ function bookmarksFor(sessionKey) {
1549
+ return current()[sessionKey] ?? EMPTY;
1550
+ }
1551
+ function toggleBookmark(sessionKey, itemId) {
1552
+ const base = readStored() ?? current();
1553
+ const held = base[sessionKey] ?? EMPTY;
1554
+ const ids = held.includes(itemId) ? held.filter((id) => id !== itemId) : [...held, itemId];
1555
+ const next = {
1556
+ ...base,
1557
+ [sessionKey]: ids
1558
+ };
1559
+ if (ids.length === 0) delete next[sessionKey];
1560
+ try {
1561
+ storage()?.setItem(BOOKMARKS_STORAGE_KEY, JSON.stringify(next));
1562
+ } catch {}
1563
+ adopt(next);
1564
+ notify();
1565
+ }
1566
+ function onStorage(event) {
1567
+ if (event.key !== null && event.key !== "workerdeck.bookmarks.v1") return;
1568
+ const stored = readStored();
1569
+ if (stored) {
1570
+ adopt(stored);
1571
+ notify();
1572
+ }
1573
+ }
1574
+ function subscribeBookmarks(listener) {
1575
+ listeners.add(listener);
1576
+ const target = storageEvents();
1577
+ if (listeners.size === 1 && target) {
1578
+ target.addEventListener("storage", onStorage);
1579
+ detachStorageEvent = () => target.removeEventListener("storage", onStorage);
1580
+ }
1581
+ return () => {
1582
+ listeners.delete(listener);
1583
+ if (listeners.size === 0) {
1584
+ detachStorageEvent?.();
1585
+ detachStorageEvent = void 0;
1586
+ }
1587
+ };
1588
+ }
1589
+ //#endregion
1590
+ //#region src/hooks/use-bookmarks.ts
1591
+ const NONE = Object.freeze([]);
1592
+ function useBookmarks(sessionKey) {
1593
+ const read = useCallback(() => sessionKey ? bookmarksFor(sessionKey) : NONE, [sessionKey]);
1594
+ const bookmarks = useSyncExternalStore(subscribeBookmarks, read, () => NONE);
1595
+ const toggle = useCallback((itemId) => {
1596
+ if (sessionKey) toggleBookmark(sessionKey, itemId);
1597
+ }, [sessionKey]);
1598
+ return useMemo(() => ({
1599
+ bookmarks,
1600
+ toggle
1601
+ }), [bookmarks, toggle]);
1602
+ }
1603
+ //#endregion
1411
1604
  //#region src/hooks/use-draft.ts
1412
1605
  function useDraft(client, sessionId) {
1413
1606
  const key = sessionId ? draftKey(client, sessionId) : void 0;
@@ -1477,37 +1670,11 @@ function useProfileUsage(client, profile, options = {}) {
1477
1670
  //#endregion
1478
1671
  //#region src/hooks/use-session-info.ts
1479
1672
  function useSessionInfo(client, sessionId) {
1480
- const [info, setInfo] = useState();
1481
- const [loading, setLoading] = useState(!!sessionId);
1482
- const [error, setError] = useState();
1483
- useEffect(() => {
1484
- if (!sessionId) {
1485
- setInfo(void 0);
1486
- setLoading(false);
1487
- setError(void 0);
1488
- return;
1489
- }
1490
- let cancelled = false;
1491
- setLoading(true);
1492
- setError(void 0);
1493
- setInfo(void 0);
1494
- client.getSession(sessionId).then((next) => {
1495
- if (cancelled) return;
1496
- setInfo(next);
1497
- setLoading(false);
1498
- }).catch((e) => {
1499
- if (cancelled) return;
1500
- setError(e instanceof Error ? e.message : "Session not found");
1501
- setLoading(false);
1502
- });
1503
- return () => {
1504
- cancelled = true;
1505
- };
1506
- }, [client, sessionId]);
1673
+ const { data, error, loading } = useAsync(() => client.getSession(sessionId), [client, sessionId], { enabled: !!sessionId });
1507
1674
  return {
1508
- info,
1675
+ info: data,
1509
1676
  loading,
1510
- error
1677
+ error: error === void 0 ? void 0 : errorMessage(error, "Session not found")
1511
1678
  };
1512
1679
  }
1513
1680
  //#endregion
@@ -1666,7 +1833,7 @@ function useOpenFiles(client) {
1666
1833
  dispatch({
1667
1834
  type: "failed",
1668
1835
  path,
1669
- error: e instanceof Error ? e.message : "Could not read that file"
1836
+ error: errorMessage(e, "Could not read that file")
1670
1837
  });
1671
1838
  });
1672
1839
  }
@@ -1710,7 +1877,7 @@ function useOpenFiles(client) {
1710
1877
  dispatch({
1711
1878
  type: "failed",
1712
1879
  path,
1713
- error: e instanceof Error ? e.message : "Could not re-read that file"
1880
+ error: errorMessage(e, "Could not re-read that file")
1714
1881
  });
1715
1882
  });
1716
1883
  }, [read]);
@@ -1737,7 +1904,7 @@ function useOpenFiles(client) {
1737
1904
  type: "saveFailed",
1738
1905
  path,
1739
1906
  conflict,
1740
- error: conflict ? "This file changed on disk since you opened it." : e instanceof Error ? e.message : "Could not save that file"
1907
+ error: conflict ? "This file changed on disk since you opened it." : errorMessage(e, "Could not save that file")
1741
1908
  });
1742
1909
  }
1743
1910
  }, [client]);
@@ -1767,7 +1934,7 @@ function useOpenFiles(client) {
1767
1934
  dispatch({
1768
1935
  type: "saveFailed",
1769
1936
  path,
1770
- error: e instanceof Error ? e.message : "Could not save that file"
1937
+ error: errorMessage(e, "Could not save that file")
1771
1938
  });
1772
1939
  }
1773
1940
  }, [client, write]);
@@ -1796,19 +1963,33 @@ function createToolCallHost(handle, options = {}) {
1796
1963
  let enginePromise;
1797
1964
  let disposed = false;
1798
1965
  const track = (execution) => options.onExecution?.(execution);
1799
- const refuse = (frame, reason, error, startedAt) => {
1800
- handle.sendToolCallError(frame.executionId, reason, error);
1801
- track({
1802
- executionId: frame.executionId,
1803
- toolName: frame.toolName,
1804
- status: "failed",
1805
- reason,
1806
- startedAt,
1807
- endedAt: Date.now()
1808
- });
1966
+ const settle = (frame, startedAt, outcome) => {
1967
+ const { executionId, toolName } = frame;
1968
+ if (outcome.ok) {
1969
+ handle.sendToolCallResult(executionId, {
1970
+ type: "json",
1971
+ value: outcome.value
1972
+ }, outcome.logs);
1973
+ track({
1974
+ executionId,
1975
+ toolName,
1976
+ status: "settled",
1977
+ startedAt,
1978
+ endedAt: Date.now()
1979
+ });
1980
+ } else {
1981
+ handle.sendToolCallError(executionId, outcome.reason, outcome.error, outcome.logs);
1982
+ track({
1983
+ executionId,
1984
+ toolName,
1985
+ status: "failed",
1986
+ reason: outcome.reason,
1987
+ startedAt,
1988
+ endedAt: Date.now()
1989
+ });
1990
+ }
1809
1991
  };
1810
- const runClientTool = async (frame, handler) => {
1811
- const startedAt = Date.now();
1992
+ const execute = async (frame, startedAt, work) => {
1812
1993
  const controller = new AbortController();
1813
1994
  inFlight.set(frame.executionId, controller);
1814
1995
  track({
@@ -1817,116 +1998,94 @@ function createToolCallHost(handle, options = {}) {
1817
1998
  status: "running",
1818
1999
  startedAt
1819
2000
  });
2001
+ let outcome;
1820
2002
  try {
1821
- const result = await handler(frame.input, {
1822
- executionId: frame.executionId,
1823
- signal: controller.signal
1824
- });
1825
- if (disposed || !inFlight.has(frame.executionId)) return;
1826
- if ("error" in result) {
1827
- handle.sendToolCallError(frame.executionId, result.reason ?? "client_error", result.error);
1828
- track({
1829
- executionId: frame.executionId,
1830
- toolName: frame.toolName,
1831
- status: "failed",
1832
- reason: result.reason ?? "client_error",
1833
- startedAt,
1834
- endedAt: Date.now()
1835
- });
1836
- } else {
1837
- handle.sendToolCallResult(frame.executionId, {
1838
- type: "json",
1839
- value: result.value
1840
- });
1841
- track({
1842
- executionId: frame.executionId,
1843
- toolName: frame.toolName,
1844
- status: "settled",
1845
- startedAt,
1846
- endedAt: Date.now()
1847
- });
1848
- }
2003
+ outcome = await work(controller.signal);
1849
2004
  } catch (error) {
1850
- if (disposed || !inFlight.has(frame.executionId)) return;
1851
- refuse(frame, "host_error", error instanceof Error ? error.message : String(error), startedAt);
2005
+ outcome = {
2006
+ ok: false,
2007
+ reason: "host_error",
2008
+ error: errorMessage(error)
2009
+ };
2010
+ }
2011
+ try {
2012
+ if (!disposed && inFlight.has(frame.executionId)) settle(frame, startedAt, outcome);
1852
2013
  } finally {
1853
2014
  inFlight.delete(frame.executionId);
1854
2015
  }
1855
2016
  };
2017
+ const runClientTool = (frame, handler, startedAt) => execute(frame, startedAt, async (signal) => {
2018
+ const result = await handler(frame.input, {
2019
+ executionId: frame.executionId,
2020
+ signal
2021
+ });
2022
+ return "error" in result ? {
2023
+ ok: false,
2024
+ reason: result.reason ?? "client_error",
2025
+ error: result.error
2026
+ } : {
2027
+ ok: true,
2028
+ value: result.value
2029
+ };
2030
+ });
2031
+ const runScript = (frame, script, startedAt) => execute(frame, startedAt, async (signal) => {
2032
+ const sandbox = await import("@workerdeck/sandbox");
2033
+ const vfs = sandbox.createVfs(frame.vfsSeed);
2034
+ const timeoutMs = Math.min(frame.limits?.timeoutMs ?? Number.POSITIVE_INFINITY, options.timeoutMs ?? 5e3);
2035
+ const memoryLimitBytes = Math.min(frame.limits?.memoryLimitBytes ?? Number.POSITIVE_INFINITY, options.memoryLimitBytes ?? 67108864);
2036
+ let result;
2037
+ if (options.execute) result = await options.execute({
2038
+ script,
2039
+ vfs,
2040
+ timeoutMs,
2041
+ memoryLimitBytes,
2042
+ signal
2043
+ });
2044
+ else {
2045
+ enginePromise ??= (options.loadEngine ?? defaultLoadEngine)();
2046
+ result = await sandbox.runScript(await enginePromise, {
2047
+ script,
2048
+ vfs,
2049
+ timeoutMs,
2050
+ memoryLimitBytes,
2051
+ signal,
2052
+ fetchText: options.fetchText
2053
+ });
2054
+ }
2055
+ const logs = result.logs.map((l) => `[${l.level}] ${l.text}`);
2056
+ return result.ok ? {
2057
+ ok: true,
2058
+ value: result.value,
2059
+ logs
2060
+ } : {
2061
+ ok: false,
2062
+ reason: result.reason,
2063
+ error: result.error,
2064
+ logs
2065
+ };
2066
+ });
1856
2067
  const run = async (frame) => {
1857
2068
  const startedAt = Date.now();
1858
2069
  const clientHandler = options.clientTools?.[frame.toolName];
1859
- if (clientHandler) return runClientTool(frame, clientHandler);
2070
+ if (clientHandler) return runClientTool(frame, clientHandler, startedAt);
1860
2071
  if (!(options.tools ?? ["eval_script"]).includes(frame.toolName)) {
1861
- refuse(frame, "unsupported_tool", `this client does not execute '${frame.toolName}'`, startedAt);
2072
+ settle(frame, startedAt, {
2073
+ ok: false,
2074
+ reason: "unsupported_tool",
2075
+ error: `this client does not execute '${frame.toolName}'`
2076
+ });
1862
2077
  return;
1863
2078
  }
1864
2079
  const script = frame.input?.script;
1865
2080
  if (typeof script !== "string") {
1866
- refuse(frame, "invalid_input", "expected a string `script` input", startedAt);
2081
+ settle(frame, startedAt, {
2082
+ ok: false,
2083
+ reason: "invalid_input",
2084
+ error: "expected a string `script` input"
2085
+ });
1867
2086
  return;
1868
2087
  }
1869
- const controller = new AbortController();
1870
- inFlight.set(frame.executionId, controller);
1871
- track({
1872
- executionId: frame.executionId,
1873
- toolName: frame.toolName,
1874
- status: "running",
1875
- startedAt
1876
- });
1877
- try {
1878
- const sandbox = await import("@workerdeck/sandbox");
1879
- const vfs = sandbox.createVfs(frame.vfsSeed);
1880
- const timeoutMs = Math.min(frame.limits?.timeoutMs ?? Number.POSITIVE_INFINITY, options.timeoutMs ?? 5e3);
1881
- const memoryLimitBytes = Math.min(frame.limits?.memoryLimitBytes ?? Number.POSITIVE_INFINITY, options.memoryLimitBytes ?? 67108864);
1882
- const result = options.execute ? await options.execute({
1883
- script,
1884
- vfs,
1885
- timeoutMs,
1886
- memoryLimitBytes,
1887
- signal: controller.signal
1888
- }) : await (async () => {
1889
- enginePromise ??= (options.loadEngine ?? defaultLoadEngine)();
1890
- return sandbox.runScript(await enginePromise, {
1891
- script,
1892
- vfs,
1893
- timeoutMs,
1894
- memoryLimitBytes,
1895
- signal: controller.signal,
1896
- fetchText: options.fetchText
1897
- });
1898
- })();
1899
- if (disposed || !inFlight.has(frame.executionId)) return;
1900
- const logs = result.logs.map((l) => `[${l.level}] ${l.text}`);
1901
- if (result.ok) {
1902
- handle.sendToolCallResult(frame.executionId, {
1903
- type: "json",
1904
- value: result.value
1905
- }, logs);
1906
- track({
1907
- executionId: frame.executionId,
1908
- toolName: frame.toolName,
1909
- status: "settled",
1910
- startedAt,
1911
- endedAt: Date.now()
1912
- });
1913
- } else {
1914
- handle.sendToolCallError(frame.executionId, result.reason, result.error, logs);
1915
- track({
1916
- executionId: frame.executionId,
1917
- toolName: frame.toolName,
1918
- status: "failed",
1919
- reason: result.reason,
1920
- startedAt,
1921
- endedAt: Date.now()
1922
- });
1923
- }
1924
- } catch (error) {
1925
- if (disposed || !inFlight.has(frame.executionId)) return;
1926
- refuse(frame, "host_error", error instanceof Error ? error.message : String(error), startedAt);
1927
- } finally {
1928
- inFlight.delete(frame.executionId);
1929
- }
2088
+ return runScript(frame, script, startedAt);
1930
2089
  };
1931
2090
  const offRequest = handle.on("toolCallRequest", (frame) => void run(frame));
1932
2091
  const offCancel = handle.on("toolCallCanceled", ({ executionId, reason }) => {
@@ -2166,6 +2325,6 @@ function useShellTerminal(handle, shellId, options) {
2166
2325
  ]);
2167
2326
  }
2168
2327
  //#endregion
2169
- export { REPLAY_HOLD_MAX_MS, ancestorsWithin, applyEvent, attachmentKind, clearDrafts, clearProfileUsageCache, clearTranscriptCache, createToolCallHost, currentText, flattenHostTree, hydrateShellOutput, hydrateShellRow, hydrateToolResult, initialOpenFilesState, initialReplayTarget, initialTranscriptState, isDirty, openFilesReducer, rateLimitWindows, recapLine, scanPromptTokens, seedFromSessionInfo, shellRowText, staleAttach, summarizeSince, useAttachments, useClaudeSession, useDraft, useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, usePeerSessions, useProfileUsage, useProjectIcons, useSessionInfo, useShellTerminal, useToolCallHost };
2328
+ export { BOOKMARKS_STORAGE_KEY, REPLAY_HOLD_MAX_MS, ancestorsWithin, applyEvent, attachmentKind, clearDrafts, clearProfileUsageCache, clearTranscriptCache, createToolCallHost, currentText, flattenHostTree, hydrateShellOutput, hydrateShellRow, hydrateToolResult, initialOpenFilesState, initialReplayTarget, initialTranscriptState, isDirty, openFilesReducer, rateLimitWindows, recapLine, scanPromptTokens, seedFromSessionInfo, shellRowText, staleAttach, summarizeSince, useAsync, useAttachments, useBookmarks, useClaudeSession, useDraft, useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, usePeerSessions, useProfileUsage, useProjectIcons, useSessionInfo, useShellTerminal, useToolCallHost };
2170
2329
 
2171
2330
  //# sourceMappingURL=index.mjs.map