@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.js CHANGED
@@ -1,4 +1,5 @@
1
- import React, { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import { isObserverMessageType } from "@robota-sdk/agent-transport";
2
+ import React, { createContext, createElement, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
3
  import { decodeFrame, decodeServerMessage } from "@robota-sdk/agent-transport/client";
3
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
5
  import ReactMarkdown from "react-markdown";
@@ -306,6 +307,7 @@ const SERVER_MESSAGE_HANDLING = {
306
307
  background_task_control_result: "visible-notice",
307
308
  protocol_error: "visible-notice",
308
309
  resume_gap: "transport-control",
310
+ surface_role: "reducer-state",
309
311
  settings: "reducer-state",
310
312
  settings_error: "reducer-state"
311
313
  };
@@ -400,6 +402,7 @@ function nextRequestId$6(prefix) {
400
402
  return `${prefix}_${requestCounter$6}_${Date.now()}`;
401
403
  }
402
404
  const CONNECTION_CLOSED = "The connection to the session closed.";
405
+ const READ_ONLY = "This session is read-only.";
403
406
  /**
404
407
  * The key a finished call's app view is read and called by, taken from the tool row's `appView`
405
408
  * reference; `undefined` when the row gives none. Everything outside this module treats it as an
@@ -431,7 +434,7 @@ function callAppToolMessage(requestId, viewKey, name, args) {
431
434
  * keeps its own entry. A reply that lost its connection never arrives: the owner of the connection
432
435
  * rejects every outstanding request when it drops, is replaced or goes away (`rejectPendingAppRequests`).
433
436
  */
434
- function useAppViewRequests(send, isConnected) {
437
+ function useAppViewRequests(send, isConnected, isObserving) {
435
438
  const pendingRef = useRef(/* @__PURE__ */ new Map());
436
439
  const [requestOwner] = useState(() => nextRequestId$6("app_request"));
437
440
  const rejectPendingAppRequests = useCallback((reason = CONNECTION_CLOSED) => {
@@ -447,6 +450,10 @@ function useAppViewRequests(send, isConnected) {
447
450
  reject(/* @__PURE__ */ new Error(signal?.aborted ? "The app view closed." : CONNECTION_CLOSED));
448
451
  return;
449
452
  }
453
+ if (isObserving()) {
454
+ reject(/* @__PURE__ */ new Error(READ_ONLY));
455
+ return;
456
+ }
450
457
  const requestId = nextRequestId$6(`${requestOwner}_${prefix}`);
451
458
  const dispose = () => signal?.removeEventListener("abort", abort);
452
459
  const abort = () => {
@@ -465,6 +472,7 @@ function useAppViewRequests(send, isConnected) {
465
472
  }
466
473
  }), [
467
474
  isConnected,
475
+ isObserving,
468
476
  requestOwner,
469
477
  send
470
478
  ]);
@@ -514,13 +522,6 @@ function useAppViewRequests(send, isConnected) {
514
522
  }
515
523
  //#endregion
516
524
  //#region src/hooks/use-execution-detail.ts
517
- /**
518
- * #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
519
- * accumulated transcript records, and whether more can be loaded. Owned independently from chat
520
- * state, the same way `use-personal-usage.ts` owns the usage-report protocol: one `requestId` per
521
- * outstanding read, so a stale reply for an entry the operator already closed (or a page they moved
522
- * past) is dropped rather than clobbering what is now on screen.
523
- */
524
525
  let requestCounter$5 = 0;
525
526
  function nextRequestId$5() {
526
527
  requestCounter$5 += 1;
@@ -532,14 +533,27 @@ function useExecutionDetailState(send) {
532
533
  const [records, setRecords] = useState([]);
533
534
  const [error, setError] = useState(null);
534
535
  const [complete, setComplete] = useState(false);
536
+ const [mode, setMode] = useState("live");
537
+ const [unreadable, setUnreadable] = useState();
535
538
  const requestIdRef = useRef(null);
536
539
  const entryIdRef = useRef(null);
540
+ const modeRef = useRef("live");
537
541
  const nextCursorRef = useRef(void 0);
538
- const readPage = useCallback((entryId, cursor) => {
542
+ const queuedRef = useRef(null);
543
+ const timerRef = useRef(null);
544
+ const lastReadAtRef = useRef(0);
545
+ const readPage = useCallback((entryId, cursor, refresh = false) => {
546
+ if (requestIdRef.current !== null) {
547
+ if (!refresh || queuedRef.current === null || queuedRef.current.refresh) queuedRef.current = {
548
+ cursor,
549
+ refresh
550
+ };
551
+ return;
552
+ }
539
553
  const requestId = nextRequestId$5();
540
554
  requestIdRef.current = requestId;
541
- entryIdRef.current = entryId;
542
- setStatus("loading");
555
+ lastReadAtRef.current = Date.now();
556
+ if (!refresh) setStatus("loading");
543
557
  setError(null);
544
558
  send({
545
559
  type: "read-execution-detail",
@@ -548,51 +562,118 @@ function useExecutionDetailState(send) {
548
562
  cursor
549
563
  });
550
564
  }, [send]);
565
+ const clearPending = useCallback(() => {
566
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
567
+ timerRef.current = null;
568
+ queuedRef.current = null;
569
+ }, []);
570
+ useEffect(() => clearPending, [clearPending]);
571
+ const openExecutionDetail = useCallback((entryId) => {
572
+ clearPending();
573
+ requestIdRef.current = null;
574
+ entryIdRef.current = entryId;
575
+ modeRef.current = "live";
576
+ setMode("live");
577
+ setOpenEntryId(entryId);
578
+ setRecords([]);
579
+ setUnreadable(void 0);
580
+ setComplete(false);
581
+ nextCursorRef.current = void 0;
582
+ readPage(entryId);
583
+ }, [clearPending, readPage]);
584
+ const loadMoreExecutionDetail = useCallback(() => {
585
+ if (!openEntryId || status === "loading" || complete) return;
586
+ readPage(openEntryId, nextCursorRef.current);
587
+ }, [
588
+ openEntryId,
589
+ status,
590
+ complete,
591
+ readPage
592
+ ]);
593
+ const changeMode = useCallback((next) => {
594
+ const entryId = entryIdRef.current;
595
+ if (!entryId) return;
596
+ clearPending();
597
+ modeRef.current = next;
598
+ setMode(next);
599
+ setRecords([]);
600
+ setUnreadable(void 0);
601
+ setComplete(false);
602
+ setStatus("loading");
603
+ nextCursorRef.current = void 0;
604
+ readPage(entryId, next === "transcript" ? { offset: 0 } : void 0);
605
+ }, [clearPending, readPage]);
606
+ const showEarlierExecutionDetail = useCallback(() => changeMode("transcript"), [changeMode]);
607
+ const showLiveExecutionDetail = useCallback(() => changeMode("live"), [changeMode]);
608
+ const closeExecutionDetail = useCallback(() => {
609
+ clearPending();
610
+ setOpenEntryId(null);
611
+ requestIdRef.current = null;
612
+ entryIdRef.current = null;
613
+ setRecords([]);
614
+ setUnreadable(void 0);
615
+ setStatus("idle");
616
+ setError(null);
617
+ setComplete(false);
618
+ }, [clearPending]);
619
+ const scheduleRefresh = useCallback((entryId) => {
620
+ if (timerRef.current !== null) return;
621
+ const delay = Math.max(0, 250 - (Date.now() - lastReadAtRef.current));
622
+ timerRef.current = setTimeout(() => {
623
+ timerRef.current = null;
624
+ if (entryIdRef.current === entryId && modeRef.current === "live") readPage(entryId, void 0, true);
625
+ }, delay);
626
+ }, [readPage]);
627
+ const handleExecutionWorkspace = useCallback((snapshot) => {
628
+ const entryId = entryIdRef.current;
629
+ if (!entryId || modeRef.current !== "live") return;
630
+ const entry = snapshot.entries.find((item) => item.id === entryId);
631
+ if (entry?.resumeSessionId && entry.controls.includes("attach")) scheduleRefresh(entryId);
632
+ }, [scheduleRefresh]);
551
633
  return {
552
634
  openEntryId,
553
635
  executionDetailStatus: status,
554
636
  executionDetailRecords: records,
555
637
  executionDetailError: error,
556
638
  executionDetailComplete: complete,
557
- openExecutionDetail: useCallback((entryId) => {
558
- setOpenEntryId(entryId);
559
- setRecords([]);
560
- setComplete(false);
561
- nextCursorRef.current = void 0;
562
- readPage(entryId);
563
- }, [readPage]),
564
- loadMoreExecutionDetail: useCallback(() => {
565
- if (!openEntryId || status === "loading" || complete) return;
566
- readPage(openEntryId, nextCursorRef.current);
567
- }, [
568
- openEntryId,
569
- status,
570
- complete,
571
- readPage
572
- ]),
573
- closeExecutionDetail: useCallback(() => {
574
- setOpenEntryId(null);
575
- requestIdRef.current = null;
576
- entryIdRef.current = null;
577
- setRecords([]);
578
- setStatus("idle");
579
- setError(null);
580
- setComplete(false);
581
- }, []),
639
+ executionDetailMode: mode,
640
+ executionDetailUnreadable: unreadable,
641
+ openExecutionDetail,
642
+ loadMoreExecutionDetail,
643
+ showEarlierExecutionDetail,
644
+ showLiveExecutionDetail,
645
+ closeExecutionDetail,
582
646
  handleExecutionDetailMessage: useCallback((msg) => {
583
647
  if (msg.type !== "execution_detail" && msg.type !== "execution_detail_error") return false;
584
648
  if (requestIdRef.current !== msg.requestId) return true;
585
- if (msg.type === "execution_detail_error") {
649
+ requestIdRef.current = null;
650
+ const queued = queuedRef.current;
651
+ queuedRef.current = null;
652
+ if (!queued || queued.refresh) if (msg.type === "execution_detail_error") {
586
653
  setStatus("error");
587
654
  setError(msg.message);
588
- return true;
655
+ } else {
656
+ setRecords((previous) => msg.page.cursor === void 0 || msg.page.cursor.offset === 0 ? msg.page.records : [...previous, ...msg.page.records]);
657
+ setUnreadable((previous) => {
658
+ const current = msg.page.unreadable;
659
+ if (msg.page.cursor === void 0 || msg.page.cursor.offset === 0) return current;
660
+ if (!previous) return current;
661
+ if (!current) return previous;
662
+ return {
663
+ locked: previous.locked + current.locked,
664
+ noKeys: previous.noKeys + current.noKeys,
665
+ doesNotOpen: previous.doesNotOpen + current.doesNotOpen
666
+ };
667
+ });
668
+ nextCursorRef.current = msg.page.nextCursor;
669
+ setComplete(msg.page.nextCursor === void 0);
670
+ setStatus("ready");
589
671
  }
590
- setRecords((previous) => msg.page.cursor === void 0 ? msg.page.records : [...previous, ...msg.page.records]);
591
- nextCursorRef.current = msg.page.nextCursor;
592
- setComplete(msg.page.nextCursor === void 0);
593
- setStatus("ready");
672
+ if (queued && entryIdRef.current) if (queued.refresh) scheduleRefresh(entryIdRef.current);
673
+ else readPage(entryIdRef.current, queued.cursor);
594
674
  return true;
595
- }, [])
675
+ }, [readPage, scheduleRefresh]),
676
+ handleExecutionWorkspace
596
677
  };
597
678
  }
598
679
  //#endregion
@@ -944,7 +1025,7 @@ function initialSidebarOpen() {
944
1025
  * on its primary one. After a reconnect to such a host this surface goes back to the session it was
945
1026
  * on; an older host has one session for everyone, so there is nothing to go back to.
946
1027
  */
947
- function useSessionDirectoryState(send) {
1028
+ function useSessionDirectoryState(send, isObserving) {
948
1029
  const { storage: { browserNamespace } } = useProductIdentity();
949
1030
  const [sessionListing, setSessionListing] = useState(null);
950
1031
  const [sessionsError, setSessionsError] = useState(null);
@@ -995,6 +1076,7 @@ function useSessionDirectoryState(send) {
995
1076
  if (target === null) return;
996
1077
  clearStoredRestoreId(browserNamespace);
997
1078
  if (target === listing.currentSessionId) return;
1079
+ if (isObserving()) return;
998
1080
  if (!keepsSessionsLive(listing)) return;
999
1081
  if (!listing.sessions.some((row) => row.id === target)) return;
1000
1082
  send({
@@ -1002,7 +1084,11 @@ function useSessionDirectoryState(send) {
1002
1084
  sessionId: target,
1003
1085
  requestId: nextRequestId$1("session_change")
1004
1086
  });
1005
- }, [send, browserNamespace]);
1087
+ }, [
1088
+ send,
1089
+ browserNamespace,
1090
+ isObserving
1091
+ ]);
1006
1092
  return {
1007
1093
  sessionListing,
1008
1094
  sessionsError,
@@ -1297,6 +1383,9 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
1297
1383
  function useSessionClient(makeClient) {
1298
1384
  const [status, setStatus] = useState("disconnected");
1299
1385
  const statusRef = useRef("disconnected");
1386
+ const [role, setRole] = useState(null);
1387
+ const roleRef = useRef(null);
1388
+ const isObserving = useCallback(() => roleRef.current === "observe", []);
1300
1389
  const [messages, setMessages] = useState([]);
1301
1390
  const [activeTools, setActiveTools] = useState([]);
1302
1391
  const [streamingText, setStreamingText] = useState("");
@@ -1361,7 +1450,18 @@ function useSessionClient(makeClient) {
1361
1450
  const silentCommandRequestIdsRef = useRef(/* @__PURE__ */ new Set());
1362
1451
  const sessionChangesInFlightRef = useRef(0);
1363
1452
  const sessionChangeRequestIdsRef = useRef(/* @__PURE__ */ new Set());
1453
+ const [executionDetailInput, setExecutionDetailInput] = useState();
1454
+ const executionDetailInputRef = useRef(null);
1364
1455
  const send = useCallback((msg) => {
1456
+ const refused = isObserving() && !isObserverMessageType(msg.type);
1457
+ if (msg.type === "send-background-task") {
1458
+ if (refused || executionDetailInputRef.current !== null) return;
1459
+ executionDetailInputRef.current = msg.taskId;
1460
+ setExecutionDetailInput({
1461
+ taskId: msg.taskId,
1462
+ status: "pending"
1463
+ });
1464
+ }
1365
1465
  if (msg.type === "command") {
1366
1466
  if (msg.name === "help") {
1367
1467
  setHelpOpen(true);
@@ -1378,14 +1478,16 @@ function useSessionClient(makeClient) {
1378
1478
  });
1379
1479
  return;
1380
1480
  }
1481
+ if (refused) return;
1381
1482
  commandsInFlightRef.current += 1;
1382
1483
  }
1484
+ if (refused) return;
1383
1485
  if (msg.type === "switch-session" || msg.type === "new-session") {
1384
1486
  sessionChangesInFlightRef.current += 1;
1385
1487
  if (msg.requestId !== void 0) sessionChangeRequestIdsRef.current.add(msg.requestId);
1386
1488
  }
1387
1489
  clientRef.current?.send(msg);
1388
- }, [appendEntry]);
1490
+ }, [appendEntry, isObserving]);
1389
1491
  /** One session change this surface asked for has been answered. */
1390
1492
  const settleSessionChange = useCallback((requestId) => {
1391
1493
  sessionChangesInFlightRef.current = Math.max(0, sessionChangesInFlightRef.current - 1);
@@ -1418,11 +1520,11 @@ function useSessionClient(makeClient) {
1418
1520
  });
1419
1521
  }, [send]);
1420
1522
  const { handleUsageMessage, ...personalUsageState } = usePersonalUsageState(send);
1421
- const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1523
+ const { handleExecutionDetailMessage, handleExecutionWorkspace, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1422
1524
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1423
1525
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1424
- const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, useCallback(() => statusRef.current === "connected", []));
1425
- const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
1526
+ const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, useCallback(() => statusRef.current === "connected", []), isObserving);
1527
+ const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send, isObserving);
1426
1528
  const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
1427
1529
  const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
1428
1530
  const settingsOpenRef = useRef(false);
@@ -1444,6 +1546,10 @@ function useSessionClient(makeClient) {
1444
1546
  if (handleAgentDefinitionsMessage(msg)) return;
1445
1547
  if (handleSchedulesMessage(msg)) return;
1446
1548
  switch (msg.type) {
1549
+ case "surface_role":
1550
+ roleRef.current = msg.role;
1551
+ setRole(msg.role);
1552
+ break;
1447
1553
  case "messages": {
1448
1554
  const reconstructed = msg.display ? buildConversationEntriesFromDisplaySegments(msg.display) : msg.messages.flatMap((m) => {
1449
1555
  if (m.role !== "user" && m.role !== "assistant") return [];
@@ -1502,13 +1608,21 @@ function useSessionClient(makeClient) {
1502
1608
  break;
1503
1609
  }
1504
1610
  case "execution_workspace_event":
1611
+ handleExecutionWorkspace(msg.snapshot);
1505
1612
  setExecutionWorkspace(msg.snapshot);
1506
1613
  break;
1507
1614
  case "background_task_control_result":
1615
+ if (msg.action === "send" && executionDetailInputRef.current === msg.taskId) {
1616
+ executionDetailInputRef.current = null;
1617
+ setExecutionDetailInput({
1618
+ taskId: msg.taskId,
1619
+ status: msg.success ? "sent" : "failed"
1620
+ });
1621
+ }
1508
1622
  if (!msg.success) setSessionNotices((previous) => [...previous, {
1509
1623
  id: nextId(),
1510
1624
  kind: "background-task-control-failed",
1511
- message: msg.message ?? `Could not stop ${msg.taskId}.`
1625
+ message: msg.message ?? `Could not ${msg.action === "send" ? "send to" : "stop"} ${msg.taskId}.`
1512
1626
  }]);
1513
1627
  break;
1514
1628
  case "pending":
@@ -1653,6 +1767,13 @@ function useSessionClient(makeClient) {
1653
1767
  }]);
1654
1768
  break;
1655
1769
  case "protocol_error": {
1770
+ if (executionDetailInputRef.current !== null && msg.message === "Not permitted for an observer: send-background-task") {
1771
+ setExecutionDetailInput({
1772
+ taskId: executionDetailInputRef.current,
1773
+ status: "failed"
1774
+ });
1775
+ executionDetailInputRef.current = null;
1776
+ }
1656
1777
  if (sessionChangesInFlightRef.current > 0) {
1657
1778
  settleSessionChange();
1658
1779
  setSessionNotices((previous) => [...previous, {
@@ -1765,6 +1886,7 @@ function useSessionClient(makeClient) {
1765
1886
  handleAgentDefinitionsMessage,
1766
1887
  handleAppViewMessage,
1767
1888
  handleExecutionDetailMessage,
1889
+ handleExecutionWorkspace,
1768
1890
  handleModelListMessage,
1769
1891
  handleProjectMessage,
1770
1892
  handleSchedulesMessage,
@@ -1799,9 +1921,21 @@ function useSessionClient(makeClient) {
1799
1921
  const onStatusChange = (next) => {
1800
1922
  statusRef.current = next;
1801
1923
  setStatus(next);
1924
+ if (next !== "connected") {
1925
+ roleRef.current = null;
1926
+ setRole(null);
1927
+ }
1802
1928
  if (next !== "connected") {
1803
1929
  rejectPendingAppRequests();
1804
1930
  clearCompactionNotices();
1931
+ closeExecutionDetail();
1932
+ if (executionDetailInputRef.current !== null) {
1933
+ setExecutionDetailInput({
1934
+ taskId: executionDetailInputRef.current,
1935
+ status: "failed"
1936
+ });
1937
+ executionDetailInputRef.current = null;
1938
+ }
1805
1939
  }
1806
1940
  if (next === "connected") {
1807
1941
  commandsInFlightRef.current = 0;
@@ -1809,6 +1943,7 @@ function useSessionClient(makeClient) {
1809
1943
  sessionChangesInFlightRef.current = 0;
1810
1944
  sessionChangeRequestIdsRef.current.clear();
1811
1945
  pendingIntentRef.current = null;
1946
+ client.send({ type: "get-surface-role" });
1812
1947
  client.send({ type: "get-commands" });
1813
1948
  client.send({ type: "get-status" });
1814
1949
  client.send({ type: "get-pending" });
@@ -1817,10 +1952,18 @@ function useSessionClient(makeClient) {
1817
1952
  requestSchedules();
1818
1953
  }
1819
1954
  };
1820
- const client = makeClient({
1955
+ const inner = makeClient({
1821
1956
  onMessage: handleMessage,
1822
1957
  onStatusChange
1823
1958
  });
1959
+ const client = {
1960
+ connect: () => inner.connect(),
1961
+ disconnect: () => inner.disconnect(),
1962
+ send: (msg) => {
1963
+ if (roleRef.current === "observe" && !isObserverMessageType(msg.type)) return;
1964
+ inner.send(msg);
1965
+ }
1966
+ };
1824
1967
  clientRef.current = client;
1825
1968
  client.connect();
1826
1969
  return () => {
@@ -1832,6 +1975,7 @@ function useSessionClient(makeClient) {
1832
1975
  makeClient,
1833
1976
  handleMessage,
1834
1977
  clearCompactionNotices,
1978
+ closeExecutionDetail,
1835
1979
  requestSessions,
1836
1980
  requestSchedules,
1837
1981
  armRestore,
@@ -1848,6 +1992,7 @@ function useSessionClient(makeClient) {
1848
1992
  commandCatalog,
1849
1993
  sessionStatus,
1850
1994
  ownDriverId,
1995
+ role,
1851
1996
  send,
1852
1997
  sendCommandSilently,
1853
1998
  pendingPrompts,
@@ -1856,6 +2001,7 @@ function useSessionClient(makeClient) {
1856
2001
  answerAsk,
1857
2002
  ...personalUsageState,
1858
2003
  ...executionDetailState,
2004
+ executionDetailInput,
1859
2005
  closeExecutionDetail,
1860
2006
  ...modelListState,
1861
2007
  ...projectPanelState,
@@ -1890,6 +2036,63 @@ function useWsSession(url) {
1890
2036
  };
1891
2037
  }
1892
2038
  //#endregion
2039
+ //#region src/client/desktop-session-client.ts
2040
+ function createDesktopSessionClient(bridge, callbacks) {
2041
+ let connected = false;
2042
+ let subscribed = false;
2043
+ let release;
2044
+ let failed = false;
2045
+ let revoked = false;
2046
+ const close = () => {
2047
+ revoked = true;
2048
+ connected = false;
2049
+ if (!failed && subscribed) callbacks.onStatusChange("disconnected");
2050
+ };
2051
+ return {
2052
+ connect() {
2053
+ if (revoked || failed) return;
2054
+ if (subscribed) return;
2055
+ subscribed = true;
2056
+ callbacks.onStatusChange("connecting");
2057
+ try {
2058
+ release = bridge.subscribe((frame) => {
2059
+ if (!subscribed || failed || revoked) return;
2060
+ const decoded = decodeFrame(frame, decodeServerMessage);
2061
+ if (!decoded.ok) {
2062
+ failed = true;
2063
+ connected = false;
2064
+ bridge.close();
2065
+ callbacks.onStatusChange("error");
2066
+ return;
2067
+ }
2068
+ callbacks.onMessage(decoded.message);
2069
+ }, close, () => {
2070
+ if (!subscribed || failed || revoked) return;
2071
+ connected = true;
2072
+ callbacks.onStatusChange("connected");
2073
+ bridge.send(JSON.stringify({ type: "get-messages" }));
2074
+ });
2075
+ } catch {
2076
+ close();
2077
+ }
2078
+ },
2079
+ disconnect() {
2080
+ subscribed = false;
2081
+ connected = false;
2082
+ release?.();
2083
+ release = void 0;
2084
+ callbacks.onStatusChange("disconnected");
2085
+ },
2086
+ send(message) {
2087
+ if (!connected || failed) return;
2088
+ if (!bridge.send(JSON.stringify(message))) {
2089
+ bridge.close();
2090
+ close();
2091
+ }
2092
+ }
2093
+ };
2094
+ }
2095
+ //#endregion
1893
2096
  //#region src/client/runtime-host.ts
1894
2097
  /** Resolve host capabilities without importing Electron or a Node CLI into the browser graph. */
1895
2098
  function resolveClientRuntimeHost(environment) {
@@ -3403,7 +3606,7 @@ function AgentActivityPanel({ tasks, schedules = [], goal = null, onPauseSchedul
3403
3606
  task,
3404
3607
  onPause: onPauseSchedule,
3405
3608
  onResume: onResumeSchedule,
3406
- onRequestDelete: () => setPendingDeleteId(task.id)
3609
+ onRequestDelete: onDeleteSchedule ? () => setPendingDeleteId(task.id) : void 0
3407
3610
  }, task.id))
3408
3611
  })] }),
3409
3612
  goal && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
@@ -3504,33 +3707,33 @@ function ScheduleCard({ task, onPause, onResume, onRequestDelete }) {
3504
3707
  describeScheduleStatus(task)
3505
3708
  ]
3506
3709
  }),
3507
- /* @__PURE__ */ jsxs("div", {
3710
+ onResume || onPause || onRequestDelete ? /* @__PURE__ */ jsxs("div", {
3508
3711
  className: "mt-2 flex items-center gap-3",
3509
- children: [isPaused ? /* @__PURE__ */ jsxs("button", {
3712
+ children: [isPaused ? onResume && /* @__PURE__ */ jsxs("button", {
3510
3713
  type: "button",
3511
- onClick: () => onResume?.(task.id),
3714
+ onClick: () => onResume(task.id),
3512
3715
  className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground",
3513
3716
  children: [/* @__PURE__ */ jsx(Play, {
3514
3717
  size: 11,
3515
3718
  fill: "currentColor",
3516
3719
  "aria-hidden": "true"
3517
3720
  }), "Resume"]
3518
- }) : /* @__PURE__ */ jsxs("button", {
3721
+ }) : onPause && /* @__PURE__ */ jsxs("button", {
3519
3722
  type: "button",
3520
- onClick: () => onPause?.(task.id),
3723
+ onClick: () => onPause(task.id),
3521
3724
  className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground",
3522
3725
  children: [/* @__PURE__ */ jsx(Pause, {
3523
3726
  size: 11,
3524
3727
  fill: "currentColor",
3525
3728
  "aria-hidden": "true"
3526
3729
  }), "Pause"]
3527
- }), /* @__PURE__ */ jsx("button", {
3730
+ }), onRequestDelete && /* @__PURE__ */ jsx("button", {
3528
3731
  type: "button",
3529
3732
  onClick: onRequestDelete,
3530
3733
  className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-destructive hover:bg-destructive/10",
3531
3734
  children: "Delete…"
3532
3735
  })]
3533
- })
3736
+ }) : null
3534
3737
  ]
3535
3738
  });
3536
3739
  }
@@ -3554,7 +3757,7 @@ function GoalCard({ goal, onCancel }) {
3554
3757
  className: "mt-1.5 pl-[26px] text-[13px] text-muted-foreground",
3555
3758
  children: describeGoalStatus(goal)
3556
3759
  }),
3557
- goal.status === "active" && /* @__PURE__ */ jsx("div", {
3760
+ goal.status === "active" && onCancel && /* @__PURE__ */ jsx("div", {
3558
3761
  className: "mt-2 pl-[26px]",
3559
3762
  children: /* @__PURE__ */ jsxs("button", {
3560
3763
  type: "button",
@@ -5855,16 +6058,32 @@ function writeDraft(namespace, sessionId, value) {
5855
6058
  * context). The row's controls run the session's own commands (`/provider`, `/mode`, `/effort`), so a
5856
6059
  * setting changes through the one path every client shares.
5857
6060
  */
5858
- const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, status, connected = true, running, onStop, queued, onCancelQueue, pickFiles, getPathForFile, modelList = null, onRequestModelList = () => {}, onSilentCommand = onCommand, onManageProviders = () => onCommand("provider") }, ref) {
6061
+ const ComposerInstance = 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) {
5859
6062
  const { identity, storage: { browserNamespace } } = useProductIdentity();
5860
6063
  const textareaRef = useRef(null);
5861
6064
  useImperativeHandle(ref, () => ({ focus: () => textareaRef.current?.focus() }), []);
5862
6065
  const sessionId = status?.sessionId;
6066
+ const workspacePath = status?.workspace?.path;
6067
+ const pickerGeneration = useRef(0);
6068
+ const [fileInputEpoch, setFileInputEpoch] = useState(0);
6069
+ const browserPickerOpen = useRef(false);
6070
+ useLayoutEffect(() => {
6071
+ pickerGeneration.current += 1;
6072
+ browserPickerOpen.current = false;
6073
+ return () => {
6074
+ pickerGeneration.current += 1;
6075
+ };
6076
+ }, [sessionId, workspacePath]);
6077
+ const invalidatePickers = () => {
6078
+ pickerGeneration.current += 1;
6079
+ browserPickerOpen.current = false;
6080
+ setFileInputEpoch((epoch) => epoch + 1);
6081
+ };
5863
6082
  const sessionIdRef = useRef(sessionId);
5864
6083
  const hasKnownSessionRef = useRef(sessionId !== void 0);
5865
6084
  const stateRef = useRef(EMPTY_DRAFT);
5866
6085
  const [draft, setDraftState] = useState(() => {
5867
- const initial = readDraft(browserNamespace, sessionId);
6086
+ const initial = draftMode === "volatile" ? EMPTY_DRAFT : readDraft(browserNamespace, sessionId);
5868
6087
  stateRef.current = initial;
5869
6088
  return initial.text;
5870
6089
  });
@@ -5888,7 +6107,7 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
5888
6107
  text: value
5889
6108
  };
5890
6109
  setDraftState(value);
5891
- writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
6110
+ if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
5892
6111
  };
5893
6112
  /** #3282 §4d: attachment chips persist alongside the text, under the same per-session draft key. */
5894
6113
  const setAttachments = (updater) => {
@@ -5898,9 +6117,10 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
5898
6117
  attachments: next
5899
6118
  };
5900
6119
  setAttachmentsState(next);
5901
- writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
6120
+ if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
5902
6121
  };
5903
6122
  useEffect(() => {
6123
+ if (draftMode === "volatile") return;
5904
6124
  if (sessionId === void 0 && hasKnownSessionRef.current) return;
5905
6125
  if (sessionIdRef.current === sessionId) return;
5906
6126
  const firstArrival = !hasKnownSessionRef.current;
@@ -5919,23 +6139,26 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
5919
6139
  setDraftState(stored.text);
5920
6140
  setAttachmentsState(stored.attachments);
5921
6141
  setAttachmentNotice(null);
5922
- }, [browserNamespace, sessionId]);
6142
+ }, [
6143
+ browserNamespace,
6144
+ draftMode,
6145
+ sessionId
6146
+ ]);
5923
6147
  const submit = () => {
5924
6148
  if (!connected) return;
5925
6149
  const text = draft.trim();
5926
6150
  if (!text && attachments.length === 0) return;
6151
+ invalidatePickers();
5927
6152
  onSubmit(buildPromptWithAttachments(text, attachments));
5928
6153
  setDraft("");
5929
6154
  setAttachments([]);
5930
6155
  setAttachmentNotice(null);
5931
6156
  };
5932
- const workspacePath = status?.workspace?.path;
5933
- const totalAttachedBytes = attachments.reduce((sum, a) => sum + a.size, 0);
5934
6157
  /** Evaluate every dropped/picked file in order, so a mixed batch attaches what it can. */
5935
6158
  const addCandidates = (candidates) => {
5936
6159
  if (candidates.length === 0) return;
5937
- let total = totalAttachedBytes;
5938
- let count = attachments.length;
6160
+ let total = stateRef.current.attachments.reduce((sum, attachment) => sum + attachment.size, 0);
6161
+ let count = stateRef.current.attachments.length;
5939
6162
  const added = [];
5940
6163
  let notice = null;
5941
6164
  for (const candidate of candidates) {
@@ -5959,19 +6182,44 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
5959
6182
  absolutePath: getPathForFile ? getPathForFile(file) || void 0 : void 0
5960
6183
  });
5961
6184
  const fileInputRef = useRef(null);
6185
+ useEffect(() => {
6186
+ const input = fileInputRef.current;
6187
+ if (!input) return;
6188
+ const cancel = () => {
6189
+ browserPickerOpen.current = false;
6190
+ };
6191
+ input.addEventListener("cancel", cancel);
6192
+ return () => {
6193
+ input.removeEventListener("cancel", cancel);
6194
+ };
6195
+ }, [
6196
+ sessionId,
6197
+ workspacePath,
6198
+ fileInputEpoch
6199
+ ]);
5962
6200
  const handleAttachClick = () => {
5963
6201
  if (!connected) return;
5964
6202
  if (pickFiles) {
5965
- pickFiles().then((picked) => addCandidates(picked.map((f) => ({
5966
- name: f.name,
5967
- size: f.size,
5968
- absolutePath: f.path
5969
- })))).catch(() => setAttachmentNotice("Could not open the file picker. Try again."));
6203
+ const generation = pickerGeneration.current;
6204
+ pickFiles().then((picked) => {
6205
+ if (generation !== pickerGeneration.current) return;
6206
+ addCandidates(picked.map((f) => ({
6207
+ name: f.name,
6208
+ size: f.size,
6209
+ absolutePath: f.path
6210
+ })));
6211
+ }).catch(() => {
6212
+ if (generation !== pickerGeneration.current) return;
6213
+ setAttachmentNotice("Could not open the file picker. Try again.");
6214
+ });
5970
6215
  return;
5971
6216
  }
6217
+ if (browserPickerOpen.current) return;
6218
+ browserPickerOpen.current = true;
5972
6219
  fileInputRef.current?.click();
5973
6220
  };
5974
6221
  const handleFileInputChange = (event) => {
6222
+ browserPickerOpen.current = false;
5975
6223
  const files = Array.from(event.target.files ?? []);
5976
6224
  event.target.value = "";
5977
6225
  addCandidates(files.map(toCandidateFromFile));
@@ -6003,6 +6251,7 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
6003
6251
  /** Choosing a command from the menu, by mouse or by keyboard (`acceptMenu` below). */
6004
6252
  const chooseMenuItem = (item) => {
6005
6253
  if (isImmediateScreenCommand(item)) {
6254
+ invalidatePickers();
6006
6255
  setDraft("");
6007
6256
  onCommand(item.name);
6008
6257
  return;
@@ -6165,7 +6414,11 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
6165
6414
  className: "sr-only",
6166
6415
  tabIndex: -1,
6167
6416
  "aria-hidden": "true"
6168
- }),
6417
+ }, JSON.stringify([
6418
+ sessionId ?? null,
6419
+ workspacePath ?? null,
6420
+ fileInputEpoch
6421
+ ])),
6169
6422
  /* @__PURE__ */ jsx("textarea", {
6170
6423
  ref: textareaRef,
6171
6424
  "aria-label": "message",
@@ -6257,6 +6510,15 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
6257
6510
  ]
6258
6511
  });
6259
6512
  });
6513
+ /** Volatile state belongs to one session and never crosses a mode or session boundary. */
6514
+ const Composer = forwardRef(function Composer(props, ref) {
6515
+ const key = props.draftMode === "volatile" ? JSON.stringify(["volatile", props.status?.sessionId ?? null]) : "persistent";
6516
+ return /* @__PURE__ */ createElement(ComposerInstance, {
6517
+ ...props,
6518
+ key,
6519
+ ref
6520
+ });
6521
+ });
6260
6522
  /**
6261
6523
  * The goal being pursued (`/goal`), above the composer while it is active — the objective, how far
6262
6524
  * the turn budget has gone, and a way to stop it (`/goal cancel`).
@@ -6291,7 +6553,7 @@ function GoalBar({ status, onStop }) {
6291
6553
  goal.maxIterations
6292
6554
  ]
6293
6555
  }),
6294
- /* @__PURE__ */ jsxs("button", {
6556
+ onStop && /* @__PURE__ */ jsxs("button", {
6295
6557
  type: "button",
6296
6558
  "aria-label": "Stop goal",
6297
6559
  onClick: onStop,
@@ -6317,6 +6579,7 @@ const STATUS_TAG_TONE = {
6317
6579
  destructive: "bg-destructive/12 text-destructive",
6318
6580
  muted: "bg-subtle/15 text-subtle"
6319
6581
  };
6582
+ const FOLLOW_BOTTOM_MARGIN_PX = 32;
6320
6583
  const RECORD_KIND_LABEL = {
6321
6584
  message: "",
6322
6585
  tool_activity: "Tool",
@@ -6331,10 +6594,40 @@ const RECORD_KIND_LABEL = {
6331
6594
  * headline), its transcript so far (paginated via `onLoadMore`) and, once it has one, its result.
6332
6595
  * `entry === null` renders nothing; the caller owns showing/hiding this over the panel.
6333
6596
  */
6334
- function ExecutionDetailSheet({ entry, status, records, error, complete, onClose, onLoadMore, onStop }) {
6597
+ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose, onLoadMore, onStop, onSend, inputState, onContinue, mode = "live", unreadable, onShowEarlier, onShowLive }) {
6598
+ const [prompt, setPrompt] = React.useState("");
6599
+ const submitted = React.useRef(null);
6600
+ const outputRef = React.useRef(null);
6601
+ const followingRef = React.useRef(true);
6602
+ const isFork = Boolean(entry?.resumeSessionId && entry.controls.includes("attach"));
6603
+ React.useLayoutEffect(() => {
6604
+ followingRef.current = true;
6605
+ }, [entry?.id, mode]);
6606
+ React.useLayoutEffect(() => {
6607
+ const output = outputRef.current;
6608
+ if (output && isFork && mode === "live" && followingRef.current) output.scrollTop = output.scrollHeight;
6609
+ }, [
6610
+ entry?.id,
6611
+ isFork,
6612
+ mode,
6613
+ records
6614
+ ]);
6615
+ React.useEffect(() => {
6616
+ setPrompt("");
6617
+ submitted.current = null;
6618
+ }, [entry?.id]);
6619
+ React.useEffect(() => {
6620
+ if (!entry || inputState?.taskId !== entry.sourceId || inputState.status === "pending") return;
6621
+ if (inputState.status === "sent" && submitted.current !== null) {
6622
+ const sent = submitted.current;
6623
+ setPrompt((current) => current.trim() === sent ? "" : current);
6624
+ }
6625
+ submitted.current = null;
6626
+ }, [entry, inputState]);
6335
6627
  if (!entry) return null;
6336
6628
  const described = describeExecutionEntryStatus(entry);
6337
6629
  const canStop = onStop !== void 0 && entry.controls.includes("cancel");
6630
+ const ended = entry.status === "completed" || entry.status === "failed" || entry.status === "cancelled";
6338
6631
  const askedLine = entry.headline?.text ?? entry.preview;
6339
6632
  return /* @__PURE__ */ jsxs("div", {
6340
6633
  role: "dialog",
@@ -6389,10 +6682,37 @@ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose
6389
6682
  entry.deniedToolCalls,
6390
6683
  " tool ",
6391
6684
  entry.deniedToolCalls === 1 ? "call was" : "calls were",
6392
- " refused."
6685
+ " ",
6686
+ "refused."
6393
6687
  ]
6394
6688
  }),
6689
+ isFork && onShowEarlier && onShowLive && /* @__PURE__ */ jsxs("div", {
6690
+ className: "flex gap-3 border-b border-subtle/15 px-4 py-2 text-[12px]",
6691
+ children: [/* @__PURE__ */ jsx("button", {
6692
+ type: "button",
6693
+ onClick: onShowLive,
6694
+ disabled: mode === "live",
6695
+ children: "Live output"
6696
+ }), /* @__PURE__ */ jsx("button", {
6697
+ type: "button",
6698
+ onClick: onShowEarlier,
6699
+ disabled: mode === "transcript",
6700
+ children: "Earlier output"
6701
+ })]
6702
+ }),
6395
6703
  /* @__PURE__ */ jsxs("div", {
6704
+ ref: outputRef,
6705
+ role: isFork ? mode === "live" ? "log" : "region" : void 0,
6706
+ "aria-label": isFork ? mode === "live" ? "Live fork output" : "Earlier fork output" : void 0,
6707
+ "aria-live": isFork && mode === "live" ? "polite" : void 0,
6708
+ "aria-relevant": isFork && mode === "live" ? "additions text" : void 0,
6709
+ "aria-atomic": isFork && mode === "live" ? false : void 0,
6710
+ tabIndex: isFork ? 0 : void 0,
6711
+ onScroll: (event) => {
6712
+ if (!isFork || mode !== "live") return;
6713
+ const output = event.currentTarget;
6714
+ followingRef.current = output.scrollHeight - output.scrollTop - output.clientHeight <= FOLLOW_BOTTOM_MARGIN_PX;
6715
+ },
6396
6716
  className: "flex-1 space-y-2 overflow-y-auto px-4 py-3",
6397
6717
  children: [
6398
6718
  records.map((record) => /* @__PURE__ */ jsxs("div", {
@@ -6413,17 +6733,54 @@ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose
6413
6733
  className: "text-[13px] text-destructive",
6414
6734
  children: error
6415
6735
  }),
6416
- !complete && status !== "loading" && /* @__PURE__ */ jsx("button", {
6736
+ unreadable && /* @__PURE__ */ jsxs("p", {
6737
+ className: "text-[12px] text-warning",
6738
+ children: [
6739
+ "Some records could not be read: ",
6740
+ unreadable.locked,
6741
+ " locked, ",
6742
+ unreadable.noKeys,
6743
+ " without keys, ",
6744
+ unreadable.doesNotOpen,
6745
+ " unavailable."
6746
+ ]
6747
+ }),
6748
+ (!isFork || mode === "transcript") && !complete && status !== "loading" && /* @__PURE__ */ jsx("button", {
6417
6749
  type: "button",
6418
6750
  onClick: onLoadMore,
6419
6751
  className: "text-[12px] text-subtle underline hover:text-foreground",
6420
6752
  children: "Load more"
6421
6753
  }),
6422
- !complete && status === "loading" && records.length > 0 && /* @__PURE__ */ jsx("p", {
6754
+ (!isFork || mode === "transcript") && !complete && status === "loading" && records.length > 0 && /* @__PURE__ */ jsx("p", {
6423
6755
  className: "text-[12px] text-subtle",
6424
6756
  children: "Loading more…"
6425
6757
  })
6426
6758
  ]
6759
+ }),
6760
+ isFork && !ended && entry.controls.includes("send") && onSend && /* @__PURE__ */ jsxs("form", {
6761
+ className: "border-t border-subtle/15 p-3",
6762
+ onSubmit: (event) => {
6763
+ event.preventDefault();
6764
+ if (!prompt.trim() || submitted.current !== null || inputState?.status === "pending") return;
6765
+ submitted.current = prompt.trim();
6766
+ onSend(submitted.current);
6767
+ },
6768
+ children: [/* @__PURE__ */ jsx("textarea", {
6769
+ "aria-label": "Message the fork",
6770
+ value: prompt,
6771
+ onChange: (event) => setPrompt(event.target.value),
6772
+ className: "w-full rounded border border-subtle/20 bg-background p-2 text-foreground"
6773
+ }), /* @__PURE__ */ jsx("button", {
6774
+ type: "submit",
6775
+ disabled: !prompt.trim() || inputState?.status === "pending",
6776
+ children: "Send to fork"
6777
+ })]
6778
+ }),
6779
+ isFork && ended && onContinue && /* @__PURE__ */ jsx("button", {
6780
+ type: "button",
6781
+ onClick: onContinue,
6782
+ className: "border-t border-subtle/15 p-3 text-foreground",
6783
+ children: "Continue as session"
6427
6784
  })
6428
6785
  ]
6429
6786
  });
@@ -8101,7 +8458,8 @@ function SetupPanel({ onSetUp, disabled }) {
8101
8458
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
8102
8459
  * mark (e.g. "app").
8103
8460
  */
8104
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
8461
+ function SessionSurface({ state, surface, personalUsageEnabled: personalUsageRequested = false, draftMode = "persistent", onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
8462
+ const readOnly = state.role === "observe";
8105
8463
  const [view, setView] = useState("chat");
8106
8464
  const { identity } = useProductIdentity();
8107
8465
  const appViewHost = useMemo(() => ({
@@ -8120,6 +8478,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8120
8478
  identity.displayName,
8121
8479
  surface
8122
8480
  ]);
8481
+ const personalUsageEnabled = personalUsageRequested && !readOnly;
8123
8482
  const composerRef = useRef(null);
8124
8483
  const tasks = state.executionWorkspace?.entries ?? [];
8125
8484
  const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
@@ -8139,7 +8498,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8139
8498
  const hasWorkInProgress = hasTasks || state.scheduledTasks.length > 0 || currentGoal !== null;
8140
8499
  const isEmpty = state.messages.length === 0 && !state.streamingText && !state.isThinking && state.activeTools.length === 0;
8141
8500
  const setupRequired = state.sessionStatus?.setupRequired === true;
8142
- const hasSessionList = (state.sessionListing ?? null) !== null || state.sessionsError?.code === "list_failed";
8501
+ const hasSessionList = !readOnly && ((state.sessionListing ?? null) !== null || state.sessionsError?.code === "list_failed");
8143
8502
  const sidebarOpen = hasSessionList && state.sessionSidebarOpen;
8144
8503
  const usage = personalUsageEnabled && view === "usage";
8145
8504
  const project = view === "project";
@@ -8209,7 +8568,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8209
8568
  connectionLost: state.connectionLost ?? false
8210
8569
  }), usage ? /* @__PURE__ */ jsxs("div", {
8211
8570
  className: "min-h-0 flex-1",
8212
- children: [/* @__PURE__ */ jsx(PersonalUsageDashboard, { state }), /* @__PURE__ */ jsx(PermissionPrompt, {
8571
+ children: [/* @__PURE__ */ jsx(PersonalUsageDashboard, { state }), readOnly ? null : /* @__PURE__ */ jsx(PermissionPrompt, {
8213
8572
  layout: "modal",
8214
8573
  prompts: state.pendingPrompts,
8215
8574
  onAnswerPermission: state.answerPermission,
@@ -8221,7 +8580,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8221
8580
  children: [/* @__PURE__ */ jsx(ProjectPanel, {
8222
8581
  state,
8223
8582
  onOpenMemoryInEditor
8224
- }), /* @__PURE__ */ jsx(PermissionPrompt, {
8583
+ }), readOnly ? null : /* @__PURE__ */ jsx(PermissionPrompt, {
8225
8584
  layout: "modal",
8226
8585
  prompts: state.pendingPrompts,
8227
8586
  onAnswerPermission: state.answerPermission,
@@ -8241,7 +8600,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8241
8600
  }),
8242
8601
  /* @__PURE__ */ jsx("div", {
8243
8602
  className: "min-h-0 flex-1 overflow-hidden",
8244
- children: setupRequired ? /* @__PURE__ */ jsx(SetupPanel, {
8603
+ children: setupRequired && !readOnly ? /* @__PURE__ */ jsx(SetupPanel, {
8245
8604
  onSetUp: () => state.send({
8246
8605
  type: "command",
8247
8606
  name: "provider",
@@ -8249,7 +8608,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8249
8608
  }),
8250
8609
  disabled: state.status !== "connected"
8251
8610
  }) : isEmpty ? /* @__PURE__ */ jsx(EmptyState, {}) : /* @__PURE__ */ jsx(AppViewHostContext.Provider, {
8252
- value: appViewHost,
8611
+ value: readOnly ? null : appViewHost,
8253
8612
  children: /* @__PURE__ */ jsx(ConversationView, {
8254
8613
  messages: state.messages,
8255
8614
  activeTools: state.activeTools,
@@ -8264,13 +8623,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8264
8623
  children: [
8265
8624
  /* @__PURE__ */ jsx(GoalBar, {
8266
8625
  status: state.sessionStatus ?? null,
8267
- onStop: () => state.send({
8626
+ onStop: readOnly ? void 0 : () => state.send({
8268
8627
  type: "command",
8269
8628
  name: "goal",
8270
8629
  args: "cancel"
8271
8630
  })
8272
8631
  }),
8273
- /* @__PURE__ */ jsx(PermissionPrompt, {
8632
+ readOnly ? null : /* @__PURE__ */ jsx(PermissionPrompt, {
8274
8633
  layout: "dock",
8275
8634
  prompts: state.pendingPrompts,
8276
8635
  onAnswerPermission: state.answerPermission,
@@ -8278,8 +8637,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8278
8637
  ownDriverId: state.ownDriverId,
8279
8638
  onFocusReturn: () => composerRef.current?.focus()
8280
8639
  }),
8281
- setupRequired ? null : /* @__PURE__ */ jsx(Composer, {
8640
+ readOnly ? /* @__PURE__ */ jsx("p", {
8641
+ role: "status",
8642
+ className: "rounded-2xl bg-card px-4 py-3 text-center text-[14px] text-muted-foreground",
8643
+ children: "Read-only: you are watching this session."
8644
+ }) : setupRequired ? null : /* @__PURE__ */ jsx(Composer, {
8282
8645
  ref: composerRef,
8646
+ draftMode,
8283
8647
  catalog: state.commandCatalog ?? null,
8284
8648
  status: state.sessionStatus ?? null,
8285
8649
  connected: state.status === "connected",
@@ -8332,13 +8696,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8332
8696
  selectedEntryId: state.openEntryId ?? void 0,
8333
8697
  onSelect: (entry) => state.openExecutionDetail(entry.id),
8334
8698
  onReturnToConversation: () => state.closeExecutionDetail(),
8335
- onStop: stopExecutionEntry,
8699
+ onStop: readOnly ? void 0 : stopExecutionEntry,
8336
8700
  schedules: state.scheduledTasks,
8337
8701
  goal: currentGoal,
8338
- onPauseSchedule: state.pauseSchedule,
8339
- onResumeSchedule: state.resumeSchedule,
8340
- onDeleteSchedule: state.deleteSchedule,
8341
- onCancelGoal: () => state.send({
8702
+ onPauseSchedule: readOnly ? void 0 : state.pauseSchedule,
8703
+ onResumeSchedule: readOnly ? void 0 : state.resumeSchedule,
8704
+ onDeleteSchedule: readOnly ? void 0 : state.deleteSchedule,
8705
+ onCancelGoal: readOnly ? void 0 : () => state.send({
8342
8706
  type: "command",
8343
8707
  name: "goal",
8344
8708
  args: "cancel"
@@ -8353,14 +8717,27 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8353
8717
  complete: state.executionDetailComplete,
8354
8718
  onClose: () => state.closeExecutionDetail(),
8355
8719
  onLoadMore: () => state.loadMoreExecutionDetail(),
8356
- onStop: openEntry ? () => stopExecutionEntry(openEntry) : void 0
8720
+ onStop: openEntry && !readOnly ? () => stopExecutionEntry(openEntry) : void 0,
8721
+ inputState: state.executionDetailInput,
8722
+ mode: state.executionDetailMode,
8723
+ unreadable: state.executionDetailUnreadable,
8724
+ onShowEarlier: state.showEarlierExecutionDetail,
8725
+ onShowLive: state.showLiveExecutionDetail,
8726
+ onSend: openEntry && !readOnly ? (prompt) => state.send({
8727
+ type: "send-background-task",
8728
+ taskId: openEntry.sourceId,
8729
+ input: { prompt }
8730
+ }) : void 0,
8731
+ onContinue: openEntry?.resumeSessionId && !readOnly ? () => {
8732
+ if (openEntry.resumeSessionId) state.switchSession(openEntry.resumeSessionId);
8733
+ } : void 0
8357
8734
  })
8358
8735
  ]
8359
8736
  })]
8360
8737
  }),
8361
8738
  /* @__PURE__ */ jsx(SessionNotices, { state }),
8362
- /* @__PURE__ */ jsx(SettingsScreen, { state }),
8363
- /* @__PURE__ */ jsx(AgentSwitcherSheet, { state }),
8739
+ readOnly ? null : /* @__PURE__ */ jsx(SettingsScreen, { state }),
8740
+ readOnly ? null : /* @__PURE__ */ jsx(AgentSwitcherSheet, { state }),
8364
8741
  /* @__PURE__ */ jsx(HelpSheet, {
8365
8742
  open: state.helpOpen,
8366
8743
  onClose: () => state.closeHelp(),
@@ -8538,6 +8915,6 @@ function SessionInput({ enabled, onSubmit }) {
8538
8915
  });
8539
8916
  }
8540
8917
  //#endregion
8541
- export { AgentActivityPanel, AgentSwitcherSheet, CenteredChrome, ConfirmDialog, ConversationView, Dialog, HelpSheet, PermissionPrompt, PersonalUsageDashboard, ProductIdentityProvider, ProductMark, ProductWordmark, SessionMonitor, SessionSidebar, SessionSurface, SettingsScreen, applyPromptEvent, askResponse, createWsSessionClient, describeUiIntentForGui, guiScreenForUiIntent, permissionResponse, rememberSessionForRestore, resolveClientRuntimeHost, useProductIdentity, useSessionClient, useWsSession };
8918
+ export { AgentActivityPanel, AgentSwitcherSheet, CenteredChrome, ConfirmDialog, ConversationView, Dialog, HelpSheet, PermissionPrompt, PersonalUsageDashboard, ProductIdentityProvider, ProductMark, ProductWordmark, SessionMonitor, SessionSidebar, SessionSurface, SettingsScreen, applyPromptEvent, askResponse, createDesktopSessionClient, createWsSessionClient, describeUiIntentForGui, guiScreenForUiIntent, permissionResponse, rememberSessionForRestore, resolveClientRuntimeHost, useProductIdentity, useSessionClient, useWsSession };
8542
8919
 
8543
8920
  //# sourceMappingURL=index.js.map