@robota-sdk/agent-ui-web 3.1.2 → 3.3.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/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
+ let _robota_sdk_agent_transport = require("@robota-sdk/agent-transport");
24
25
  let react = require("react");
25
26
  react = __toESM(react, 1);
26
27
  let _robota_sdk_agent_transport_client = require("@robota-sdk/agent-transport/client");
@@ -332,6 +333,7 @@ const SERVER_MESSAGE_HANDLING = {
332
333
  background_task_control_result: "visible-notice",
333
334
  protocol_error: "visible-notice",
334
335
  resume_gap: "transport-control",
336
+ surface_role: "reducer-state",
335
337
  settings: "reducer-state",
336
338
  settings_error: "reducer-state"
337
339
  };
@@ -426,6 +428,7 @@ function nextRequestId$6(prefix) {
426
428
  return `${prefix}_${requestCounter$6}_${Date.now()}`;
427
429
  }
428
430
  const CONNECTION_CLOSED = "The connection to the session closed.";
431
+ const READ_ONLY = "This session is read-only.";
429
432
  /**
430
433
  * The key a finished call's app view is read and called by, taken from the tool row's `appView`
431
434
  * reference; `undefined` when the row gives none. Everything outside this module treats it as an
@@ -457,7 +460,7 @@ function callAppToolMessage(requestId, viewKey, name, args) {
457
460
  * keeps its own entry. A reply that lost its connection never arrives: the owner of the connection
458
461
  * rejects every outstanding request when it drops, is replaced or goes away (`rejectPendingAppRequests`).
459
462
  */
460
- function useAppViewRequests(send, isConnected) {
463
+ function useAppViewRequests(send, isConnected, isObserving) {
461
464
  const pendingRef = (0, react.useRef)(/* @__PURE__ */ new Map());
462
465
  const [requestOwner] = (0, react.useState)(() => nextRequestId$6("app_request"));
463
466
  const rejectPendingAppRequests = (0, react.useCallback)((reason = CONNECTION_CLOSED) => {
@@ -473,6 +476,10 @@ function useAppViewRequests(send, isConnected) {
473
476
  reject(/* @__PURE__ */ new Error(signal?.aborted ? "The app view closed." : CONNECTION_CLOSED));
474
477
  return;
475
478
  }
479
+ if (isObserving()) {
480
+ reject(/* @__PURE__ */ new Error(READ_ONLY));
481
+ return;
482
+ }
476
483
  const requestId = nextRequestId$6(`${requestOwner}_${prefix}`);
477
484
  const dispose = () => signal?.removeEventListener("abort", abort);
478
485
  const abort = () => {
@@ -491,6 +498,7 @@ function useAppViewRequests(send, isConnected) {
491
498
  }
492
499
  }), [
493
500
  isConnected,
501
+ isObserving,
494
502
  requestOwner,
495
503
  send
496
504
  ]);
@@ -540,13 +548,6 @@ function useAppViewRequests(send, isConnected) {
540
548
  }
541
549
  //#endregion
542
550
  //#region src/hooks/use-execution-detail.ts
543
- /**
544
- * #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
545
- * accumulated transcript records, and whether more can be loaded. Owned independently from chat
546
- * state, the same way `use-personal-usage.ts` owns the usage-report protocol: one `requestId` per
547
- * outstanding read, so a stale reply for an entry the operator already closed (or a page they moved
548
- * past) is dropped rather than clobbering what is now on screen.
549
- */
550
551
  let requestCounter$5 = 0;
551
552
  function nextRequestId$5() {
552
553
  requestCounter$5 += 1;
@@ -558,14 +559,27 @@ function useExecutionDetailState(send) {
558
559
  const [records, setRecords] = (0, react.useState)([]);
559
560
  const [error, setError] = (0, react.useState)(null);
560
561
  const [complete, setComplete] = (0, react.useState)(false);
562
+ const [mode, setMode] = (0, react.useState)("live");
563
+ const [unreadable, setUnreadable] = (0, react.useState)();
561
564
  const requestIdRef = (0, react.useRef)(null);
562
565
  const entryIdRef = (0, react.useRef)(null);
566
+ const modeRef = (0, react.useRef)("live");
563
567
  const nextCursorRef = (0, react.useRef)(void 0);
564
- const readPage = (0, react.useCallback)((entryId, cursor) => {
568
+ const queuedRef = (0, react.useRef)(null);
569
+ const timerRef = (0, react.useRef)(null);
570
+ const lastReadAtRef = (0, react.useRef)(0);
571
+ const readPage = (0, react.useCallback)((entryId, cursor, refresh = false) => {
572
+ if (requestIdRef.current !== null) {
573
+ if (!refresh || queuedRef.current === null || queuedRef.current.refresh) queuedRef.current = {
574
+ cursor,
575
+ refresh
576
+ };
577
+ return;
578
+ }
565
579
  const requestId = nextRequestId$5();
566
580
  requestIdRef.current = requestId;
567
- entryIdRef.current = entryId;
568
- setStatus("loading");
581
+ lastReadAtRef.current = Date.now();
582
+ if (!refresh) setStatus("loading");
569
583
  setError(null);
570
584
  send({
571
585
  type: "read-execution-detail",
@@ -574,51 +588,118 @@ function useExecutionDetailState(send) {
574
588
  cursor
575
589
  });
576
590
  }, [send]);
591
+ const clearPending = (0, react.useCallback)(() => {
592
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
593
+ timerRef.current = null;
594
+ queuedRef.current = null;
595
+ }, []);
596
+ (0, react.useEffect)(() => clearPending, [clearPending]);
597
+ const openExecutionDetail = (0, react.useCallback)((entryId) => {
598
+ clearPending();
599
+ requestIdRef.current = null;
600
+ entryIdRef.current = entryId;
601
+ modeRef.current = "live";
602
+ setMode("live");
603
+ setOpenEntryId(entryId);
604
+ setRecords([]);
605
+ setUnreadable(void 0);
606
+ setComplete(false);
607
+ nextCursorRef.current = void 0;
608
+ readPage(entryId);
609
+ }, [clearPending, readPage]);
610
+ const loadMoreExecutionDetail = (0, react.useCallback)(() => {
611
+ if (!openEntryId || status === "loading" || complete) return;
612
+ readPage(openEntryId, nextCursorRef.current);
613
+ }, [
614
+ openEntryId,
615
+ status,
616
+ complete,
617
+ readPage
618
+ ]);
619
+ const changeMode = (0, react.useCallback)((next) => {
620
+ const entryId = entryIdRef.current;
621
+ if (!entryId) return;
622
+ clearPending();
623
+ modeRef.current = next;
624
+ setMode(next);
625
+ setRecords([]);
626
+ setUnreadable(void 0);
627
+ setComplete(false);
628
+ setStatus("loading");
629
+ nextCursorRef.current = void 0;
630
+ readPage(entryId, next === "transcript" ? { offset: 0 } : void 0);
631
+ }, [clearPending, readPage]);
632
+ const showEarlierExecutionDetail = (0, react.useCallback)(() => changeMode("transcript"), [changeMode]);
633
+ const showLiveExecutionDetail = (0, react.useCallback)(() => changeMode("live"), [changeMode]);
634
+ const closeExecutionDetail = (0, react.useCallback)(() => {
635
+ clearPending();
636
+ setOpenEntryId(null);
637
+ requestIdRef.current = null;
638
+ entryIdRef.current = null;
639
+ setRecords([]);
640
+ setUnreadable(void 0);
641
+ setStatus("idle");
642
+ setError(null);
643
+ setComplete(false);
644
+ }, [clearPending]);
645
+ const scheduleRefresh = (0, react.useCallback)((entryId) => {
646
+ if (timerRef.current !== null) return;
647
+ const delay = Math.max(0, 250 - (Date.now() - lastReadAtRef.current));
648
+ timerRef.current = setTimeout(() => {
649
+ timerRef.current = null;
650
+ if (entryIdRef.current === entryId && modeRef.current === "live") readPage(entryId, void 0, true);
651
+ }, delay);
652
+ }, [readPage]);
653
+ const handleExecutionWorkspace = (0, react.useCallback)((snapshot) => {
654
+ const entryId = entryIdRef.current;
655
+ if (!entryId || modeRef.current !== "live") return;
656
+ const entry = snapshot.entries.find((item) => item.id === entryId);
657
+ if (entry?.resumeSessionId && entry.controls.includes("attach")) scheduleRefresh(entryId);
658
+ }, [scheduleRefresh]);
577
659
  return {
578
660
  openEntryId,
579
661
  executionDetailStatus: status,
580
662
  executionDetailRecords: records,
581
663
  executionDetailError: error,
582
664
  executionDetailComplete: complete,
583
- openExecutionDetail: (0, react.useCallback)((entryId) => {
584
- setOpenEntryId(entryId);
585
- setRecords([]);
586
- setComplete(false);
587
- nextCursorRef.current = void 0;
588
- readPage(entryId);
589
- }, [readPage]),
590
- loadMoreExecutionDetail: (0, react.useCallback)(() => {
591
- if (!openEntryId || status === "loading" || complete) return;
592
- readPage(openEntryId, nextCursorRef.current);
593
- }, [
594
- openEntryId,
595
- status,
596
- complete,
597
- readPage
598
- ]),
599
- closeExecutionDetail: (0, react.useCallback)(() => {
600
- setOpenEntryId(null);
601
- requestIdRef.current = null;
602
- entryIdRef.current = null;
603
- setRecords([]);
604
- setStatus("idle");
605
- setError(null);
606
- setComplete(false);
607
- }, []),
665
+ executionDetailMode: mode,
666
+ executionDetailUnreadable: unreadable,
667
+ openExecutionDetail,
668
+ loadMoreExecutionDetail,
669
+ showEarlierExecutionDetail,
670
+ showLiveExecutionDetail,
671
+ closeExecutionDetail,
608
672
  handleExecutionDetailMessage: (0, react.useCallback)((msg) => {
609
673
  if (msg.type !== "execution_detail" && msg.type !== "execution_detail_error") return false;
610
674
  if (requestIdRef.current !== msg.requestId) return true;
611
- if (msg.type === "execution_detail_error") {
675
+ requestIdRef.current = null;
676
+ const queued = queuedRef.current;
677
+ queuedRef.current = null;
678
+ if (!queued || queued.refresh) if (msg.type === "execution_detail_error") {
612
679
  setStatus("error");
613
680
  setError(msg.message);
614
- return true;
681
+ } else {
682
+ setRecords((previous) => msg.page.cursor === void 0 || msg.page.cursor.offset === 0 ? msg.page.records : [...previous, ...msg.page.records]);
683
+ setUnreadable((previous) => {
684
+ const current = msg.page.unreadable;
685
+ if (msg.page.cursor === void 0 || msg.page.cursor.offset === 0) return current;
686
+ if (!previous) return current;
687
+ if (!current) return previous;
688
+ return {
689
+ locked: previous.locked + current.locked,
690
+ noKeys: previous.noKeys + current.noKeys,
691
+ doesNotOpen: previous.doesNotOpen + current.doesNotOpen
692
+ };
693
+ });
694
+ nextCursorRef.current = msg.page.nextCursor;
695
+ setComplete(msg.page.nextCursor === void 0);
696
+ setStatus("ready");
615
697
  }
616
- setRecords((previous) => msg.page.cursor === void 0 ? msg.page.records : [...previous, ...msg.page.records]);
617
- nextCursorRef.current = msg.page.nextCursor;
618
- setComplete(msg.page.nextCursor === void 0);
619
- setStatus("ready");
698
+ if (queued && entryIdRef.current) if (queued.refresh) scheduleRefresh(entryIdRef.current);
699
+ else readPage(entryIdRef.current, queued.cursor);
620
700
  return true;
621
- }, [])
701
+ }, [readPage, scheduleRefresh]),
702
+ handleExecutionWorkspace
622
703
  };
623
704
  }
624
705
  //#endregion
@@ -970,7 +1051,7 @@ function initialSidebarOpen() {
970
1051
  * on its primary one. After a reconnect to such a host this surface goes back to the session it was
971
1052
  * on; an older host has one session for everyone, so there is nothing to go back to.
972
1053
  */
973
- function useSessionDirectoryState(send) {
1054
+ function useSessionDirectoryState(send, isObserving) {
974
1055
  const { storage: { browserNamespace } } = useProductIdentity();
975
1056
  const [sessionListing, setSessionListing] = (0, react.useState)(null);
976
1057
  const [sessionsError, setSessionsError] = (0, react.useState)(null);
@@ -1021,6 +1102,7 @@ function useSessionDirectoryState(send) {
1021
1102
  if (target === null) return;
1022
1103
  clearStoredRestoreId(browserNamespace);
1023
1104
  if (target === listing.currentSessionId) return;
1105
+ if (isObserving()) return;
1024
1106
  if (!keepsSessionsLive(listing)) return;
1025
1107
  if (!listing.sessions.some((row) => row.id === target)) return;
1026
1108
  send({
@@ -1028,7 +1110,11 @@ function useSessionDirectoryState(send) {
1028
1110
  sessionId: target,
1029
1111
  requestId: nextRequestId$1("session_change")
1030
1112
  });
1031
- }, [send, browserNamespace]);
1113
+ }, [
1114
+ send,
1115
+ browserNamespace,
1116
+ isObserving
1117
+ ]);
1032
1118
  return {
1033
1119
  sessionListing,
1034
1120
  sessionsError,
@@ -1323,6 +1409,9 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
1323
1409
  function useSessionClient(makeClient) {
1324
1410
  const [status, setStatus] = (0, react.useState)("disconnected");
1325
1411
  const statusRef = (0, react.useRef)("disconnected");
1412
+ const [role, setRole] = (0, react.useState)(null);
1413
+ const roleRef = (0, react.useRef)(null);
1414
+ const isObserving = (0, react.useCallback)(() => roleRef.current === "observe", []);
1326
1415
  const [messages, setMessages] = (0, react.useState)([]);
1327
1416
  const [activeTools, setActiveTools] = (0, react.useState)([]);
1328
1417
  const [streamingText, setStreamingText] = (0, react.useState)("");
@@ -1387,7 +1476,18 @@ function useSessionClient(makeClient) {
1387
1476
  const silentCommandRequestIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
1388
1477
  const sessionChangesInFlightRef = (0, react.useRef)(0);
1389
1478
  const sessionChangeRequestIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
1479
+ const [executionDetailInput, setExecutionDetailInput] = (0, react.useState)();
1480
+ const executionDetailInputRef = (0, react.useRef)(null);
1390
1481
  const send = (0, react.useCallback)((msg) => {
1482
+ const refused = isObserving() && !(0, _robota_sdk_agent_transport.isObserverMessageType)(msg.type);
1483
+ if (msg.type === "send-background-task") {
1484
+ if (refused || executionDetailInputRef.current !== null) return;
1485
+ executionDetailInputRef.current = msg.taskId;
1486
+ setExecutionDetailInput({
1487
+ taskId: msg.taskId,
1488
+ status: "pending"
1489
+ });
1490
+ }
1391
1491
  if (msg.type === "command") {
1392
1492
  if (msg.name === "help") {
1393
1493
  setHelpOpen(true);
@@ -1404,14 +1504,16 @@ function useSessionClient(makeClient) {
1404
1504
  });
1405
1505
  return;
1406
1506
  }
1507
+ if (refused) return;
1407
1508
  commandsInFlightRef.current += 1;
1408
1509
  }
1510
+ if (refused) return;
1409
1511
  if (msg.type === "switch-session" || msg.type === "new-session") {
1410
1512
  sessionChangesInFlightRef.current += 1;
1411
1513
  if (msg.requestId !== void 0) sessionChangeRequestIdsRef.current.add(msg.requestId);
1412
1514
  }
1413
1515
  clientRef.current?.send(msg);
1414
- }, [appendEntry]);
1516
+ }, [appendEntry, isObserving]);
1415
1517
  /** One session change this surface asked for has been answered. */
1416
1518
  const settleSessionChange = (0, react.useCallback)((requestId) => {
1417
1519
  sessionChangesInFlightRef.current = Math.max(0, sessionChangesInFlightRef.current - 1);
@@ -1444,11 +1546,11 @@ function useSessionClient(makeClient) {
1444
1546
  });
1445
1547
  }, [send]);
1446
1548
  const { handleUsageMessage, ...personalUsageState } = usePersonalUsageState(send);
1447
- const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1549
+ const { handleExecutionDetailMessage, handleExecutionWorkspace, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1448
1550
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1449
1551
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1450
- const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []));
1451
- const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
1552
+ const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []), isObserving);
1553
+ const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send, isObserving);
1452
1554
  const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
1453
1555
  const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
1454
1556
  const settingsOpenRef = (0, react.useRef)(false);
@@ -1470,6 +1572,10 @@ function useSessionClient(makeClient) {
1470
1572
  if (handleAgentDefinitionsMessage(msg)) return;
1471
1573
  if (handleSchedulesMessage(msg)) return;
1472
1574
  switch (msg.type) {
1575
+ case "surface_role":
1576
+ roleRef.current = msg.role;
1577
+ setRole(msg.role);
1578
+ break;
1473
1579
  case "messages": {
1474
1580
  const reconstructed = msg.display ? buildConversationEntriesFromDisplaySegments(msg.display) : msg.messages.flatMap((m) => {
1475
1581
  if (m.role !== "user" && m.role !== "assistant") return [];
@@ -1528,13 +1634,21 @@ function useSessionClient(makeClient) {
1528
1634
  break;
1529
1635
  }
1530
1636
  case "execution_workspace_event":
1637
+ handleExecutionWorkspace(msg.snapshot);
1531
1638
  setExecutionWorkspace(msg.snapshot);
1532
1639
  break;
1533
1640
  case "background_task_control_result":
1641
+ if (msg.action === "send" && executionDetailInputRef.current === msg.taskId) {
1642
+ executionDetailInputRef.current = null;
1643
+ setExecutionDetailInput({
1644
+ taskId: msg.taskId,
1645
+ status: msg.success ? "sent" : "failed"
1646
+ });
1647
+ }
1534
1648
  if (!msg.success) setSessionNotices((previous) => [...previous, {
1535
1649
  id: nextId(),
1536
1650
  kind: "background-task-control-failed",
1537
- message: msg.message ?? `Could not stop ${msg.taskId}.`
1651
+ message: msg.message ?? `Could not ${msg.action === "send" ? "send to" : "stop"} ${msg.taskId}.`
1538
1652
  }]);
1539
1653
  break;
1540
1654
  case "pending":
@@ -1679,6 +1793,13 @@ function useSessionClient(makeClient) {
1679
1793
  }]);
1680
1794
  break;
1681
1795
  case "protocol_error": {
1796
+ if (executionDetailInputRef.current !== null && msg.message === "Not permitted for an observer: send-background-task") {
1797
+ setExecutionDetailInput({
1798
+ taskId: executionDetailInputRef.current,
1799
+ status: "failed"
1800
+ });
1801
+ executionDetailInputRef.current = null;
1802
+ }
1682
1803
  if (sessionChangesInFlightRef.current > 0) {
1683
1804
  settleSessionChange();
1684
1805
  setSessionNotices((previous) => [...previous, {
@@ -1791,6 +1912,7 @@ function useSessionClient(makeClient) {
1791
1912
  handleAgentDefinitionsMessage,
1792
1913
  handleAppViewMessage,
1793
1914
  handleExecutionDetailMessage,
1915
+ handleExecutionWorkspace,
1794
1916
  handleModelListMessage,
1795
1917
  handleProjectMessage,
1796
1918
  handleSchedulesMessage,
@@ -1825,9 +1947,21 @@ function useSessionClient(makeClient) {
1825
1947
  const onStatusChange = (next) => {
1826
1948
  statusRef.current = next;
1827
1949
  setStatus(next);
1950
+ if (next !== "connected") {
1951
+ roleRef.current = null;
1952
+ setRole(null);
1953
+ }
1828
1954
  if (next !== "connected") {
1829
1955
  rejectPendingAppRequests();
1830
1956
  clearCompactionNotices();
1957
+ closeExecutionDetail();
1958
+ if (executionDetailInputRef.current !== null) {
1959
+ setExecutionDetailInput({
1960
+ taskId: executionDetailInputRef.current,
1961
+ status: "failed"
1962
+ });
1963
+ executionDetailInputRef.current = null;
1964
+ }
1831
1965
  }
1832
1966
  if (next === "connected") {
1833
1967
  commandsInFlightRef.current = 0;
@@ -1835,6 +1969,7 @@ function useSessionClient(makeClient) {
1835
1969
  sessionChangesInFlightRef.current = 0;
1836
1970
  sessionChangeRequestIdsRef.current.clear();
1837
1971
  pendingIntentRef.current = null;
1972
+ client.send({ type: "get-surface-role" });
1838
1973
  client.send({ type: "get-commands" });
1839
1974
  client.send({ type: "get-status" });
1840
1975
  client.send({ type: "get-pending" });
@@ -1843,10 +1978,18 @@ function useSessionClient(makeClient) {
1843
1978
  requestSchedules();
1844
1979
  }
1845
1980
  };
1846
- const client = makeClient({
1981
+ const inner = makeClient({
1847
1982
  onMessage: handleMessage,
1848
1983
  onStatusChange
1849
1984
  });
1985
+ const client = {
1986
+ connect: () => inner.connect(),
1987
+ disconnect: () => inner.disconnect(),
1988
+ send: (msg) => {
1989
+ if (roleRef.current === "observe" && !(0, _robota_sdk_agent_transport.isObserverMessageType)(msg.type)) return;
1990
+ inner.send(msg);
1991
+ }
1992
+ };
1850
1993
  clientRef.current = client;
1851
1994
  client.connect();
1852
1995
  return () => {
@@ -1858,6 +2001,7 @@ function useSessionClient(makeClient) {
1858
2001
  makeClient,
1859
2002
  handleMessage,
1860
2003
  clearCompactionNotices,
2004
+ closeExecutionDetail,
1861
2005
  requestSessions,
1862
2006
  requestSchedules,
1863
2007
  armRestore,
@@ -1874,6 +2018,7 @@ function useSessionClient(makeClient) {
1874
2018
  commandCatalog,
1875
2019
  sessionStatus,
1876
2020
  ownDriverId,
2021
+ role,
1877
2022
  send,
1878
2023
  sendCommandSilently,
1879
2024
  pendingPrompts,
@@ -1882,6 +2027,7 @@ function useSessionClient(makeClient) {
1882
2027
  answerAsk,
1883
2028
  ...personalUsageState,
1884
2029
  ...executionDetailState,
2030
+ executionDetailInput,
1885
2031
  closeExecutionDetail,
1886
2032
  ...modelListState,
1887
2033
  ...projectPanelState,
@@ -1916,6 +2062,63 @@ function useWsSession(url) {
1916
2062
  };
1917
2063
  }
1918
2064
  //#endregion
2065
+ //#region src/client/desktop-session-client.ts
2066
+ function createDesktopSessionClient(bridge, callbacks) {
2067
+ let connected = false;
2068
+ let subscribed = false;
2069
+ let release;
2070
+ let failed = false;
2071
+ let revoked = false;
2072
+ const close = () => {
2073
+ revoked = true;
2074
+ connected = false;
2075
+ if (!failed && subscribed) callbacks.onStatusChange("disconnected");
2076
+ };
2077
+ return {
2078
+ connect() {
2079
+ if (revoked || failed) return;
2080
+ if (subscribed) return;
2081
+ subscribed = true;
2082
+ callbacks.onStatusChange("connecting");
2083
+ try {
2084
+ release = bridge.subscribe((frame) => {
2085
+ if (!subscribed || failed || revoked) return;
2086
+ const decoded = (0, _robota_sdk_agent_transport_client.decodeFrame)(frame, _robota_sdk_agent_transport_client.decodeServerMessage);
2087
+ if (!decoded.ok) {
2088
+ failed = true;
2089
+ connected = false;
2090
+ bridge.close();
2091
+ callbacks.onStatusChange("error");
2092
+ return;
2093
+ }
2094
+ callbacks.onMessage(decoded.message);
2095
+ }, close, () => {
2096
+ if (!subscribed || failed || revoked) return;
2097
+ connected = true;
2098
+ callbacks.onStatusChange("connected");
2099
+ bridge.send(JSON.stringify({ type: "get-messages" }));
2100
+ });
2101
+ } catch {
2102
+ close();
2103
+ }
2104
+ },
2105
+ disconnect() {
2106
+ subscribed = false;
2107
+ connected = false;
2108
+ release?.();
2109
+ release = void 0;
2110
+ callbacks.onStatusChange("disconnected");
2111
+ },
2112
+ send(message) {
2113
+ if (!connected || failed) return;
2114
+ if (!bridge.send(JSON.stringify(message))) {
2115
+ bridge.close();
2116
+ close();
2117
+ }
2118
+ }
2119
+ };
2120
+ }
2121
+ //#endregion
1919
2122
  //#region src/client/runtime-host.ts
1920
2123
  /** Resolve host capabilities without importing Electron or a Node CLI into the browser graph. */
1921
2124
  function resolveClientRuntimeHost(environment) {
@@ -3429,7 +3632,7 @@ function AgentActivityPanel({ tasks, schedules = [], goal = null, onPauseSchedul
3429
3632
  task,
3430
3633
  onPause: onPauseSchedule,
3431
3634
  onResume: onResumeSchedule,
3432
- onRequestDelete: () => setPendingDeleteId(task.id)
3635
+ onRequestDelete: onDeleteSchedule ? () => setPendingDeleteId(task.id) : void 0
3433
3636
  }, task.id))
3434
3637
  })] }),
3435
3638
  goal && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -3530,33 +3733,33 @@ function ScheduleCard({ task, onPause, onResume, onRequestDelete }) {
3530
3733
  describeScheduleStatus(task)
3531
3734
  ]
3532
3735
  }),
3533
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3736
+ onResume || onPause || onRequestDelete ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3534
3737
  className: "mt-2 flex items-center gap-3",
3535
- children: [isPaused ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3738
+ children: [isPaused ? onResume && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3536
3739
  type: "button",
3537
- onClick: () => onResume?.(task.id),
3740
+ onClick: () => onResume(task.id),
3538
3741
  className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground",
3539
3742
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Play, {
3540
3743
  size: 11,
3541
3744
  fill: "currentColor",
3542
3745
  "aria-hidden": "true"
3543
3746
  }), "Resume"]
3544
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3747
+ }) : onPause && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3545
3748
  type: "button",
3546
- onClick: () => onPause?.(task.id),
3749
+ onClick: () => onPause(task.id),
3547
3750
  className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground",
3548
3751
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pause, {
3549
3752
  size: 11,
3550
3753
  fill: "currentColor",
3551
3754
  "aria-hidden": "true"
3552
3755
  }), "Pause"]
3553
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3756
+ }), onRequestDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3554
3757
  type: "button",
3555
3758
  onClick: onRequestDelete,
3556
3759
  className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-destructive hover:bg-destructive/10",
3557
3760
  children: "Delete…"
3558
3761
  })]
3559
- })
3762
+ }) : null
3560
3763
  ]
3561
3764
  });
3562
3765
  }
@@ -3580,7 +3783,7 @@ function GoalCard({ goal, onCancel }) {
3580
3783
  className: "mt-1.5 pl-[26px] text-[13px] text-muted-foreground",
3581
3784
  children: describeGoalStatus(goal)
3582
3785
  }),
3583
- goal.status === "active" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3786
+ goal.status === "active" && onCancel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3584
3787
  className: "mt-2 pl-[26px]",
3585
3788
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3586
3789
  type: "button",
@@ -5881,16 +6084,32 @@ function writeDraft(namespace, sessionId, value) {
5881
6084
  * context). The row's controls run the session's own commands (`/provider`, `/mode`, `/effort`), so a
5882
6085
  * setting changes through the one path every client shares.
5883
6086
  */
5884
- const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand, catalog, status, connected = true, running, onStop, queued, onCancelQueue, pickFiles, getPathForFile, modelList = null, onRequestModelList = () => {}, onSilentCommand = onCommand, onManageProviders = () => onCommand("provider") }, ref) {
6087
+ const ComposerInstance = (0, react.forwardRef)(function Composer({ draftMode = "persistent", onSubmit, onCommand, catalog, status, connected = true, running, onStop, queued, onCancelQueue, pickFiles, getPathForFile, modelList = null, onRequestModelList = () => {}, onSilentCommand = onCommand, onManageProviders = () => onCommand("provider") }, ref) {
5885
6088
  const { identity, storage: { browserNamespace } } = useProductIdentity();
5886
6089
  const textareaRef = (0, react.useRef)(null);
5887
6090
  (0, react.useImperativeHandle)(ref, () => ({ focus: () => textareaRef.current?.focus() }), []);
5888
6091
  const sessionId = status?.sessionId;
6092
+ const workspacePath = status?.workspace?.path;
6093
+ const pickerGeneration = (0, react.useRef)(0);
6094
+ const [fileInputEpoch, setFileInputEpoch] = (0, react.useState)(0);
6095
+ const browserPickerOpen = (0, react.useRef)(false);
6096
+ (0, react.useLayoutEffect)(() => {
6097
+ pickerGeneration.current += 1;
6098
+ browserPickerOpen.current = false;
6099
+ return () => {
6100
+ pickerGeneration.current += 1;
6101
+ };
6102
+ }, [sessionId, workspacePath]);
6103
+ const invalidatePickers = () => {
6104
+ pickerGeneration.current += 1;
6105
+ browserPickerOpen.current = false;
6106
+ setFileInputEpoch((epoch) => epoch + 1);
6107
+ };
5889
6108
  const sessionIdRef = (0, react.useRef)(sessionId);
5890
6109
  const hasKnownSessionRef = (0, react.useRef)(sessionId !== void 0);
5891
6110
  const stateRef = (0, react.useRef)(EMPTY_DRAFT);
5892
6111
  const [draft, setDraftState] = (0, react.useState)(() => {
5893
- const initial = readDraft(browserNamespace, sessionId);
6112
+ const initial = draftMode === "volatile" ? EMPTY_DRAFT : readDraft(browserNamespace, sessionId);
5894
6113
  stateRef.current = initial;
5895
6114
  return initial.text;
5896
6115
  });
@@ -5914,7 +6133,7 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
5914
6133
  text: value
5915
6134
  };
5916
6135
  setDraftState(value);
5917
- writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
6136
+ if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
5918
6137
  };
5919
6138
  /** #3282 §4d: attachment chips persist alongside the text, under the same per-session draft key. */
5920
6139
  const setAttachments = (updater) => {
@@ -5924,9 +6143,10 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
5924
6143
  attachments: next
5925
6144
  };
5926
6145
  setAttachmentsState(next);
5927
- writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
6146
+ if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
5928
6147
  };
5929
6148
  (0, react.useEffect)(() => {
6149
+ if (draftMode === "volatile") return;
5930
6150
  if (sessionId === void 0 && hasKnownSessionRef.current) return;
5931
6151
  if (sessionIdRef.current === sessionId) return;
5932
6152
  const firstArrival = !hasKnownSessionRef.current;
@@ -5945,23 +6165,26 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
5945
6165
  setDraftState(stored.text);
5946
6166
  setAttachmentsState(stored.attachments);
5947
6167
  setAttachmentNotice(null);
5948
- }, [browserNamespace, sessionId]);
6168
+ }, [
6169
+ browserNamespace,
6170
+ draftMode,
6171
+ sessionId
6172
+ ]);
5949
6173
  const submit = () => {
5950
6174
  if (!connected) return;
5951
6175
  const text = draft.trim();
5952
6176
  if (!text && attachments.length === 0) return;
6177
+ invalidatePickers();
5953
6178
  onSubmit(buildPromptWithAttachments(text, attachments));
5954
6179
  setDraft("");
5955
6180
  setAttachments([]);
5956
6181
  setAttachmentNotice(null);
5957
6182
  };
5958
- const workspacePath = status?.workspace?.path;
5959
- const totalAttachedBytes = attachments.reduce((sum, a) => sum + a.size, 0);
5960
6183
  /** Evaluate every dropped/picked file in order, so a mixed batch attaches what it can. */
5961
6184
  const addCandidates = (candidates) => {
5962
6185
  if (candidates.length === 0) return;
5963
- let total = totalAttachedBytes;
5964
- let count = attachments.length;
6186
+ let total = stateRef.current.attachments.reduce((sum, attachment) => sum + attachment.size, 0);
6187
+ let count = stateRef.current.attachments.length;
5965
6188
  const added = [];
5966
6189
  let notice = null;
5967
6190
  for (const candidate of candidates) {
@@ -5985,19 +6208,44 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
5985
6208
  absolutePath: getPathForFile ? getPathForFile(file) || void 0 : void 0
5986
6209
  });
5987
6210
  const fileInputRef = (0, react.useRef)(null);
6211
+ (0, react.useEffect)(() => {
6212
+ const input = fileInputRef.current;
6213
+ if (!input) return;
6214
+ const cancel = () => {
6215
+ browserPickerOpen.current = false;
6216
+ };
6217
+ input.addEventListener("cancel", cancel);
6218
+ return () => {
6219
+ input.removeEventListener("cancel", cancel);
6220
+ };
6221
+ }, [
6222
+ sessionId,
6223
+ workspacePath,
6224
+ fileInputEpoch
6225
+ ]);
5988
6226
  const handleAttachClick = () => {
5989
6227
  if (!connected) return;
5990
6228
  if (pickFiles) {
5991
- pickFiles().then((picked) => addCandidates(picked.map((f) => ({
5992
- name: f.name,
5993
- size: f.size,
5994
- absolutePath: f.path
5995
- })))).catch(() => setAttachmentNotice("Could not open the file picker. Try again."));
6229
+ const generation = pickerGeneration.current;
6230
+ pickFiles().then((picked) => {
6231
+ if (generation !== pickerGeneration.current) return;
6232
+ addCandidates(picked.map((f) => ({
6233
+ name: f.name,
6234
+ size: f.size,
6235
+ absolutePath: f.path
6236
+ })));
6237
+ }).catch(() => {
6238
+ if (generation !== pickerGeneration.current) return;
6239
+ setAttachmentNotice("Could not open the file picker. Try again.");
6240
+ });
5996
6241
  return;
5997
6242
  }
6243
+ if (browserPickerOpen.current) return;
6244
+ browserPickerOpen.current = true;
5998
6245
  fileInputRef.current?.click();
5999
6246
  };
6000
6247
  const handleFileInputChange = (event) => {
6248
+ browserPickerOpen.current = false;
6001
6249
  const files = Array.from(event.target.files ?? []);
6002
6250
  event.target.value = "";
6003
6251
  addCandidates(files.map(toCandidateFromFile));
@@ -6029,6 +6277,7 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
6029
6277
  /** Choosing a command from the menu, by mouse or by keyboard (`acceptMenu` below). */
6030
6278
  const chooseMenuItem = (item) => {
6031
6279
  if (isImmediateScreenCommand(item)) {
6280
+ invalidatePickers();
6032
6281
  setDraft("");
6033
6282
  onCommand(item.name);
6034
6283
  return;
@@ -6191,7 +6440,11 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
6191
6440
  className: "sr-only",
6192
6441
  tabIndex: -1,
6193
6442
  "aria-hidden": "true"
6194
- }),
6443
+ }, JSON.stringify([
6444
+ sessionId ?? null,
6445
+ workspacePath ?? null,
6446
+ fileInputEpoch
6447
+ ])),
6195
6448
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6196
6449
  ref: textareaRef,
6197
6450
  "aria-label": "message",
@@ -6283,6 +6536,15 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
6283
6536
  ]
6284
6537
  });
6285
6538
  });
6539
+ /** Volatile state belongs to one session and never crosses a mode or session boundary. */
6540
+ const Composer = (0, react.forwardRef)(function Composer(props, ref) {
6541
+ const key = props.draftMode === "volatile" ? JSON.stringify(["volatile", props.status?.sessionId ?? null]) : "persistent";
6542
+ return /* @__PURE__ */ (0, react.createElement)(ComposerInstance, {
6543
+ ...props,
6544
+ key,
6545
+ ref
6546
+ });
6547
+ });
6286
6548
  /**
6287
6549
  * The goal being pursued (`/goal`), above the composer while it is active — the objective, how far
6288
6550
  * the turn budget has gone, and a way to stop it (`/goal cancel`).
@@ -6317,7 +6579,7 @@ function GoalBar({ status, onStop }) {
6317
6579
  goal.maxIterations
6318
6580
  ]
6319
6581
  }),
6320
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6582
+ onStop && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6321
6583
  type: "button",
6322
6584
  "aria-label": "Stop goal",
6323
6585
  onClick: onStop,
@@ -6343,6 +6605,7 @@ const STATUS_TAG_TONE = {
6343
6605
  destructive: "bg-destructive/12 text-destructive",
6344
6606
  muted: "bg-subtle/15 text-subtle"
6345
6607
  };
6608
+ const FOLLOW_BOTTOM_MARGIN_PX = 32;
6346
6609
  const RECORD_KIND_LABEL = {
6347
6610
  message: "",
6348
6611
  tool_activity: "Tool",
@@ -6357,10 +6620,40 @@ const RECORD_KIND_LABEL = {
6357
6620
  * headline), its transcript so far (paginated via `onLoadMore`) and, once it has one, its result.
6358
6621
  * `entry === null` renders nothing; the caller owns showing/hiding this over the panel.
6359
6622
  */
6360
- function ExecutionDetailSheet({ entry, status, records, error, complete, onClose, onLoadMore, onStop }) {
6623
+ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose, onLoadMore, onStop, onSend, inputState, onContinue, mode = "live", unreadable, onShowEarlier, onShowLive }) {
6624
+ const [prompt, setPrompt] = react.default.useState("");
6625
+ const submitted = react.default.useRef(null);
6626
+ const outputRef = react.default.useRef(null);
6627
+ const followingRef = react.default.useRef(true);
6628
+ const isFork = Boolean(entry?.resumeSessionId && entry.controls.includes("attach"));
6629
+ react.default.useLayoutEffect(() => {
6630
+ followingRef.current = true;
6631
+ }, [entry?.id, mode]);
6632
+ react.default.useLayoutEffect(() => {
6633
+ const output = outputRef.current;
6634
+ if (output && isFork && mode === "live" && followingRef.current) output.scrollTop = output.scrollHeight;
6635
+ }, [
6636
+ entry?.id,
6637
+ isFork,
6638
+ mode,
6639
+ records
6640
+ ]);
6641
+ react.default.useEffect(() => {
6642
+ setPrompt("");
6643
+ submitted.current = null;
6644
+ }, [entry?.id]);
6645
+ react.default.useEffect(() => {
6646
+ if (!entry || inputState?.taskId !== entry.sourceId || inputState.status === "pending") return;
6647
+ if (inputState.status === "sent" && submitted.current !== null) {
6648
+ const sent = submitted.current;
6649
+ setPrompt((current) => current.trim() === sent ? "" : current);
6650
+ }
6651
+ submitted.current = null;
6652
+ }, [entry, inputState]);
6361
6653
  if (!entry) return null;
6362
6654
  const described = describeExecutionEntryStatus(entry);
6363
6655
  const canStop = onStop !== void 0 && entry.controls.includes("cancel");
6656
+ const ended = entry.status === "completed" || entry.status === "failed" || entry.status === "cancelled";
6364
6657
  const askedLine = entry.headline?.text ?? entry.preview;
6365
6658
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6366
6659
  role: "dialog",
@@ -6415,10 +6708,37 @@ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose
6415
6708
  entry.deniedToolCalls,
6416
6709
  " tool ",
6417
6710
  entry.deniedToolCalls === 1 ? "call was" : "calls were",
6418
- " refused."
6711
+ " ",
6712
+ "refused."
6419
6713
  ]
6420
6714
  }),
6715
+ isFork && onShowEarlier && onShowLive && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6716
+ className: "flex gap-3 border-b border-subtle/15 px-4 py-2 text-[12px]",
6717
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6718
+ type: "button",
6719
+ onClick: onShowLive,
6720
+ disabled: mode === "live",
6721
+ children: "Live output"
6722
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6723
+ type: "button",
6724
+ onClick: onShowEarlier,
6725
+ disabled: mode === "transcript",
6726
+ children: "Earlier output"
6727
+ })]
6728
+ }),
6421
6729
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6730
+ ref: outputRef,
6731
+ role: isFork ? mode === "live" ? "log" : "region" : void 0,
6732
+ "aria-label": isFork ? mode === "live" ? "Live fork output" : "Earlier fork output" : void 0,
6733
+ "aria-live": isFork && mode === "live" ? "polite" : void 0,
6734
+ "aria-relevant": isFork && mode === "live" ? "additions text" : void 0,
6735
+ "aria-atomic": isFork && mode === "live" ? false : void 0,
6736
+ tabIndex: isFork ? 0 : void 0,
6737
+ onScroll: (event) => {
6738
+ if (!isFork || mode !== "live") return;
6739
+ const output = event.currentTarget;
6740
+ followingRef.current = output.scrollHeight - output.scrollTop - output.clientHeight <= FOLLOW_BOTTOM_MARGIN_PX;
6741
+ },
6422
6742
  className: "flex-1 space-y-2 overflow-y-auto px-4 py-3",
6423
6743
  children: [
6424
6744
  records.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -6439,17 +6759,54 @@ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose
6439
6759
  className: "text-[13px] text-destructive",
6440
6760
  children: error
6441
6761
  }),
6442
- !complete && status !== "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6762
+ unreadable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
6763
+ className: "text-[12px] text-warning",
6764
+ children: [
6765
+ "Some records could not be read: ",
6766
+ unreadable.locked,
6767
+ " locked, ",
6768
+ unreadable.noKeys,
6769
+ " without keys, ",
6770
+ unreadable.doesNotOpen,
6771
+ " unavailable."
6772
+ ]
6773
+ }),
6774
+ (!isFork || mode === "transcript") && !complete && status !== "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6443
6775
  type: "button",
6444
6776
  onClick: onLoadMore,
6445
6777
  className: "text-[12px] text-subtle underline hover:text-foreground",
6446
6778
  children: "Load more"
6447
6779
  }),
6448
- !complete && status === "loading" && records.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6780
+ (!isFork || mode === "transcript") && !complete && status === "loading" && records.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6449
6781
  className: "text-[12px] text-subtle",
6450
6782
  children: "Loading more…"
6451
6783
  })
6452
6784
  ]
6785
+ }),
6786
+ isFork && !ended && entry.controls.includes("send") && onSend && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
6787
+ className: "border-t border-subtle/15 p-3",
6788
+ onSubmit: (event) => {
6789
+ event.preventDefault();
6790
+ if (!prompt.trim() || submitted.current !== null || inputState?.status === "pending") return;
6791
+ submitted.current = prompt.trim();
6792
+ onSend(submitted.current);
6793
+ },
6794
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6795
+ "aria-label": "Message the fork",
6796
+ value: prompt,
6797
+ onChange: (event) => setPrompt(event.target.value),
6798
+ className: "w-full rounded border border-subtle/20 bg-background p-2 text-foreground"
6799
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6800
+ type: "submit",
6801
+ disabled: !prompt.trim() || inputState?.status === "pending",
6802
+ children: "Send to fork"
6803
+ })]
6804
+ }),
6805
+ isFork && ended && onContinue && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6806
+ type: "button",
6807
+ onClick: onContinue,
6808
+ className: "border-t border-subtle/15 p-3 text-foreground",
6809
+ children: "Continue as session"
6453
6810
  })
6454
6811
  ]
6455
6812
  });
@@ -8127,7 +8484,8 @@ function SetupPanel({ onSetUp, disabled }) {
8127
8484
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
8128
8485
  * mark (e.g. "app").
8129
8486
  */
8130
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
8487
+ function SessionSurface({ state, surface, personalUsageEnabled: personalUsageRequested = false, draftMode = "persistent", onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
8488
+ const readOnly = state.role === "observe";
8131
8489
  const [view, setView] = (0, react.useState)("chat");
8132
8490
  const { identity } = useProductIdentity();
8133
8491
  const appViewHost = (0, react.useMemo)(() => ({
@@ -8146,6 +8504,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8146
8504
  identity.displayName,
8147
8505
  surface
8148
8506
  ]);
8507
+ const personalUsageEnabled = personalUsageRequested && !readOnly;
8149
8508
  const composerRef = (0, react.useRef)(null);
8150
8509
  const tasks = state.executionWorkspace?.entries ?? [];
8151
8510
  const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
@@ -8165,7 +8524,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8165
8524
  const hasWorkInProgress = hasTasks || state.scheduledTasks.length > 0 || currentGoal !== null;
8166
8525
  const isEmpty = state.messages.length === 0 && !state.streamingText && !state.isThinking && state.activeTools.length === 0;
8167
8526
  const setupRequired = state.sessionStatus?.setupRequired === true;
8168
- const hasSessionList = (state.sessionListing ?? null) !== null || state.sessionsError?.code === "list_failed";
8527
+ const hasSessionList = !readOnly && ((state.sessionListing ?? null) !== null || state.sessionsError?.code === "list_failed");
8169
8528
  const sidebarOpen = hasSessionList && state.sessionSidebarOpen;
8170
8529
  const usage = personalUsageEnabled && view === "usage";
8171
8530
  const project = view === "project";
@@ -8235,7 +8594,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8235
8594
  connectionLost: state.connectionLost ?? false
8236
8595
  }), usage ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8237
8596
  className: "min-h-0 flex-1",
8238
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonalUsageDashboard, { state }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
8597
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonalUsageDashboard, { state }), readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
8239
8598
  layout: "modal",
8240
8599
  prompts: state.pendingPrompts,
8241
8600
  onAnswerPermission: state.answerPermission,
@@ -8247,7 +8606,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8247
8606
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectPanel, {
8248
8607
  state,
8249
8608
  onOpenMemoryInEditor
8250
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
8609
+ }), readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
8251
8610
  layout: "modal",
8252
8611
  prompts: state.pendingPrompts,
8253
8612
  onAnswerPermission: state.answerPermission,
@@ -8267,7 +8626,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8267
8626
  }),
8268
8627
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8269
8628
  className: "min-h-0 flex-1 overflow-hidden",
8270
- children: setupRequired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupPanel, {
8629
+ children: setupRequired && !readOnly ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupPanel, {
8271
8630
  onSetUp: () => state.send({
8272
8631
  type: "command",
8273
8632
  name: "provider",
@@ -8275,7 +8634,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8275
8634
  }),
8276
8635
  disabled: state.status !== "connected"
8277
8636
  }) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppViewHostContext.Provider, {
8278
- value: appViewHost,
8637
+ value: readOnly ? null : appViewHost,
8279
8638
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationView, {
8280
8639
  messages: state.messages,
8281
8640
  activeTools: state.activeTools,
@@ -8290,13 +8649,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8290
8649
  children: [
8291
8650
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GoalBar, {
8292
8651
  status: state.sessionStatus ?? null,
8293
- onStop: () => state.send({
8652
+ onStop: readOnly ? void 0 : () => state.send({
8294
8653
  type: "command",
8295
8654
  name: "goal",
8296
8655
  args: "cancel"
8297
8656
  })
8298
8657
  }),
8299
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
8658
+ readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
8300
8659
  layout: "dock",
8301
8660
  prompts: state.pendingPrompts,
8302
8661
  onAnswerPermission: state.answerPermission,
@@ -8304,8 +8663,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8304
8663
  ownDriverId: state.ownDriverId,
8305
8664
  onFocusReturn: () => composerRef.current?.focus()
8306
8665
  }),
8307
- setupRequired ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Composer, {
8666
+ readOnly ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8667
+ role: "status",
8668
+ className: "rounded-2xl bg-card px-4 py-3 text-center text-[14px] text-muted-foreground",
8669
+ children: "Read-only: you are watching this session."
8670
+ }) : setupRequired ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Composer, {
8308
8671
  ref: composerRef,
8672
+ draftMode,
8309
8673
  catalog: state.commandCatalog ?? null,
8310
8674
  status: state.sessionStatus ?? null,
8311
8675
  connected: state.status === "connected",
@@ -8358,13 +8722,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8358
8722
  selectedEntryId: state.openEntryId ?? void 0,
8359
8723
  onSelect: (entry) => state.openExecutionDetail(entry.id),
8360
8724
  onReturnToConversation: () => state.closeExecutionDetail(),
8361
- onStop: stopExecutionEntry,
8725
+ onStop: readOnly ? void 0 : stopExecutionEntry,
8362
8726
  schedules: state.scheduledTasks,
8363
8727
  goal: currentGoal,
8364
- onPauseSchedule: state.pauseSchedule,
8365
- onResumeSchedule: state.resumeSchedule,
8366
- onDeleteSchedule: state.deleteSchedule,
8367
- onCancelGoal: () => state.send({
8728
+ onPauseSchedule: readOnly ? void 0 : state.pauseSchedule,
8729
+ onResumeSchedule: readOnly ? void 0 : state.resumeSchedule,
8730
+ onDeleteSchedule: readOnly ? void 0 : state.deleteSchedule,
8731
+ onCancelGoal: readOnly ? void 0 : () => state.send({
8368
8732
  type: "command",
8369
8733
  name: "goal",
8370
8734
  args: "cancel"
@@ -8379,14 +8743,27 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8379
8743
  complete: state.executionDetailComplete,
8380
8744
  onClose: () => state.closeExecutionDetail(),
8381
8745
  onLoadMore: () => state.loadMoreExecutionDetail(),
8382
- onStop: openEntry ? () => stopExecutionEntry(openEntry) : void 0
8746
+ onStop: openEntry && !readOnly ? () => stopExecutionEntry(openEntry) : void 0,
8747
+ inputState: state.executionDetailInput,
8748
+ mode: state.executionDetailMode,
8749
+ unreadable: state.executionDetailUnreadable,
8750
+ onShowEarlier: state.showEarlierExecutionDetail,
8751
+ onShowLive: state.showLiveExecutionDetail,
8752
+ onSend: openEntry && !readOnly ? (prompt) => state.send({
8753
+ type: "send-background-task",
8754
+ taskId: openEntry.sourceId,
8755
+ input: { prompt }
8756
+ }) : void 0,
8757
+ onContinue: openEntry?.resumeSessionId && !readOnly ? () => {
8758
+ if (openEntry.resumeSessionId) state.switchSession(openEntry.resumeSessionId);
8759
+ } : void 0
8383
8760
  })
8384
8761
  ]
8385
8762
  })]
8386
8763
  }),
8387
8764
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionNotices, { state }),
8388
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsScreen, { state }),
8389
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentSwitcherSheet, { state }),
8765
+ readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsScreen, { state }),
8766
+ readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentSwitcherSheet, { state }),
8390
8767
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HelpSheet, {
8391
8768
  open: state.helpOpen,
8392
8769
  onClose: () => state.closeHelp(),
@@ -8582,6 +8959,7 @@ exports.SessionSurface = SessionSurface;
8582
8959
  exports.SettingsScreen = SettingsScreen;
8583
8960
  exports.applyPromptEvent = applyPromptEvent;
8584
8961
  exports.askResponse = askResponse;
8962
+ exports.createDesktopSessionClient = createDesktopSessionClient;
8585
8963
  exports.createWsSessionClient = createWsSessionClient;
8586
8964
  exports.describeUiIntentForGui = describeUiIntentForGui;
8587
8965
  exports.guiScreenForUiIntent = guiScreenForUiIntent;