@robota-sdk/agent-ui-web 3.1.2 → 3.2.1

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
@@ -540,13 +540,6 @@ function useAppViewRequests(send, isConnected) {
540
540
  }
541
541
  //#endregion
542
542
  //#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
543
  let requestCounter$5 = 0;
551
544
  function nextRequestId$5() {
552
545
  requestCounter$5 += 1;
@@ -558,14 +551,27 @@ function useExecutionDetailState(send) {
558
551
  const [records, setRecords] = (0, react.useState)([]);
559
552
  const [error, setError] = (0, react.useState)(null);
560
553
  const [complete, setComplete] = (0, react.useState)(false);
554
+ const [mode, setMode] = (0, react.useState)("live");
555
+ const [unreadable, setUnreadable] = (0, react.useState)();
561
556
  const requestIdRef = (0, react.useRef)(null);
562
557
  const entryIdRef = (0, react.useRef)(null);
558
+ const modeRef = (0, react.useRef)("live");
563
559
  const nextCursorRef = (0, react.useRef)(void 0);
564
- const readPage = (0, react.useCallback)((entryId, cursor) => {
560
+ const queuedRef = (0, react.useRef)(null);
561
+ const timerRef = (0, react.useRef)(null);
562
+ const lastReadAtRef = (0, react.useRef)(0);
563
+ const readPage = (0, react.useCallback)((entryId, cursor, refresh = false) => {
564
+ if (requestIdRef.current !== null) {
565
+ if (!refresh || queuedRef.current === null || queuedRef.current.refresh) queuedRef.current = {
566
+ cursor,
567
+ refresh
568
+ };
569
+ return;
570
+ }
565
571
  const requestId = nextRequestId$5();
566
572
  requestIdRef.current = requestId;
567
- entryIdRef.current = entryId;
568
- setStatus("loading");
573
+ lastReadAtRef.current = Date.now();
574
+ if (!refresh) setStatus("loading");
569
575
  setError(null);
570
576
  send({
571
577
  type: "read-execution-detail",
@@ -574,51 +580,118 @@ function useExecutionDetailState(send) {
574
580
  cursor
575
581
  });
576
582
  }, [send]);
583
+ const clearPending = (0, react.useCallback)(() => {
584
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
585
+ timerRef.current = null;
586
+ queuedRef.current = null;
587
+ }, []);
588
+ (0, react.useEffect)(() => clearPending, [clearPending]);
589
+ const openExecutionDetail = (0, react.useCallback)((entryId) => {
590
+ clearPending();
591
+ requestIdRef.current = null;
592
+ entryIdRef.current = entryId;
593
+ modeRef.current = "live";
594
+ setMode("live");
595
+ setOpenEntryId(entryId);
596
+ setRecords([]);
597
+ setUnreadable(void 0);
598
+ setComplete(false);
599
+ nextCursorRef.current = void 0;
600
+ readPage(entryId);
601
+ }, [clearPending, readPage]);
602
+ const loadMoreExecutionDetail = (0, react.useCallback)(() => {
603
+ if (!openEntryId || status === "loading" || complete) return;
604
+ readPage(openEntryId, nextCursorRef.current);
605
+ }, [
606
+ openEntryId,
607
+ status,
608
+ complete,
609
+ readPage
610
+ ]);
611
+ const changeMode = (0, react.useCallback)((next) => {
612
+ const entryId = entryIdRef.current;
613
+ if (!entryId) return;
614
+ clearPending();
615
+ modeRef.current = next;
616
+ setMode(next);
617
+ setRecords([]);
618
+ setUnreadable(void 0);
619
+ setComplete(false);
620
+ setStatus("loading");
621
+ nextCursorRef.current = void 0;
622
+ readPage(entryId, next === "transcript" ? { offset: 0 } : void 0);
623
+ }, [clearPending, readPage]);
624
+ const showEarlierExecutionDetail = (0, react.useCallback)(() => changeMode("transcript"), [changeMode]);
625
+ const showLiveExecutionDetail = (0, react.useCallback)(() => changeMode("live"), [changeMode]);
626
+ const closeExecutionDetail = (0, react.useCallback)(() => {
627
+ clearPending();
628
+ setOpenEntryId(null);
629
+ requestIdRef.current = null;
630
+ entryIdRef.current = null;
631
+ setRecords([]);
632
+ setUnreadable(void 0);
633
+ setStatus("idle");
634
+ setError(null);
635
+ setComplete(false);
636
+ }, [clearPending]);
637
+ const scheduleRefresh = (0, react.useCallback)((entryId) => {
638
+ if (timerRef.current !== null) return;
639
+ const delay = Math.max(0, 250 - (Date.now() - lastReadAtRef.current));
640
+ timerRef.current = setTimeout(() => {
641
+ timerRef.current = null;
642
+ if (entryIdRef.current === entryId && modeRef.current === "live") readPage(entryId, void 0, true);
643
+ }, delay);
644
+ }, [readPage]);
645
+ const handleExecutionWorkspace = (0, react.useCallback)((snapshot) => {
646
+ const entryId = entryIdRef.current;
647
+ if (!entryId || modeRef.current !== "live") return;
648
+ const entry = snapshot.entries.find((item) => item.id === entryId);
649
+ if (entry?.resumeSessionId && entry.controls.includes("attach")) scheduleRefresh(entryId);
650
+ }, [scheduleRefresh]);
577
651
  return {
578
652
  openEntryId,
579
653
  executionDetailStatus: status,
580
654
  executionDetailRecords: records,
581
655
  executionDetailError: error,
582
656
  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
- }, []),
657
+ executionDetailMode: mode,
658
+ executionDetailUnreadable: unreadable,
659
+ openExecutionDetail,
660
+ loadMoreExecutionDetail,
661
+ showEarlierExecutionDetail,
662
+ showLiveExecutionDetail,
663
+ closeExecutionDetail,
608
664
  handleExecutionDetailMessage: (0, react.useCallback)((msg) => {
609
665
  if (msg.type !== "execution_detail" && msg.type !== "execution_detail_error") return false;
610
666
  if (requestIdRef.current !== msg.requestId) return true;
611
- if (msg.type === "execution_detail_error") {
667
+ requestIdRef.current = null;
668
+ const queued = queuedRef.current;
669
+ queuedRef.current = null;
670
+ if (!queued || queued.refresh) if (msg.type === "execution_detail_error") {
612
671
  setStatus("error");
613
672
  setError(msg.message);
614
- return true;
673
+ } else {
674
+ setRecords((previous) => msg.page.cursor === void 0 || msg.page.cursor.offset === 0 ? msg.page.records : [...previous, ...msg.page.records]);
675
+ setUnreadable((previous) => {
676
+ const current = msg.page.unreadable;
677
+ if (msg.page.cursor === void 0 || msg.page.cursor.offset === 0) return current;
678
+ if (!previous) return current;
679
+ if (!current) return previous;
680
+ return {
681
+ locked: previous.locked + current.locked,
682
+ noKeys: previous.noKeys + current.noKeys,
683
+ doesNotOpen: previous.doesNotOpen + current.doesNotOpen
684
+ };
685
+ });
686
+ nextCursorRef.current = msg.page.nextCursor;
687
+ setComplete(msg.page.nextCursor === void 0);
688
+ setStatus("ready");
615
689
  }
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");
690
+ if (queued && entryIdRef.current) if (queued.refresh) scheduleRefresh(entryIdRef.current);
691
+ else readPage(entryIdRef.current, queued.cursor);
620
692
  return true;
621
- }, [])
693
+ }, [readPage, scheduleRefresh]),
694
+ handleExecutionWorkspace
622
695
  };
623
696
  }
624
697
  //#endregion
@@ -1387,7 +1460,17 @@ function useSessionClient(makeClient) {
1387
1460
  const silentCommandRequestIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
1388
1461
  const sessionChangesInFlightRef = (0, react.useRef)(0);
1389
1462
  const sessionChangeRequestIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
1463
+ const [executionDetailInput, setExecutionDetailInput] = (0, react.useState)();
1464
+ const executionDetailInputRef = (0, react.useRef)(null);
1390
1465
  const send = (0, react.useCallback)((msg) => {
1466
+ if (msg.type === "send-background-task") {
1467
+ if (executionDetailInputRef.current !== null) return;
1468
+ executionDetailInputRef.current = msg.taskId;
1469
+ setExecutionDetailInput({
1470
+ taskId: msg.taskId,
1471
+ status: "pending"
1472
+ });
1473
+ }
1391
1474
  if (msg.type === "command") {
1392
1475
  if (msg.name === "help") {
1393
1476
  setHelpOpen(true);
@@ -1444,7 +1527,7 @@ function useSessionClient(makeClient) {
1444
1527
  });
1445
1528
  }, [send]);
1446
1529
  const { handleUsageMessage, ...personalUsageState } = usePersonalUsageState(send);
1447
- const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1530
+ const { handleExecutionDetailMessage, handleExecutionWorkspace, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1448
1531
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1449
1532
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1450
1533
  const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []));
@@ -1528,13 +1611,21 @@ function useSessionClient(makeClient) {
1528
1611
  break;
1529
1612
  }
1530
1613
  case "execution_workspace_event":
1614
+ handleExecutionWorkspace(msg.snapshot);
1531
1615
  setExecutionWorkspace(msg.snapshot);
1532
1616
  break;
1533
1617
  case "background_task_control_result":
1618
+ if (msg.action === "send" && executionDetailInputRef.current === msg.taskId) {
1619
+ executionDetailInputRef.current = null;
1620
+ setExecutionDetailInput({
1621
+ taskId: msg.taskId,
1622
+ status: msg.success ? "sent" : "failed"
1623
+ });
1624
+ }
1534
1625
  if (!msg.success) setSessionNotices((previous) => [...previous, {
1535
1626
  id: nextId(),
1536
1627
  kind: "background-task-control-failed",
1537
- message: msg.message ?? `Could not stop ${msg.taskId}.`
1628
+ message: msg.message ?? `Could not ${msg.action === "send" ? "send to" : "stop"} ${msg.taskId}.`
1538
1629
  }]);
1539
1630
  break;
1540
1631
  case "pending":
@@ -1679,6 +1770,13 @@ function useSessionClient(makeClient) {
1679
1770
  }]);
1680
1771
  break;
1681
1772
  case "protocol_error": {
1773
+ if (executionDetailInputRef.current !== null && msg.message === "Not permitted for an observer: send-background-task") {
1774
+ setExecutionDetailInput({
1775
+ taskId: executionDetailInputRef.current,
1776
+ status: "failed"
1777
+ });
1778
+ executionDetailInputRef.current = null;
1779
+ }
1682
1780
  if (sessionChangesInFlightRef.current > 0) {
1683
1781
  settleSessionChange();
1684
1782
  setSessionNotices((previous) => [...previous, {
@@ -1791,6 +1889,7 @@ function useSessionClient(makeClient) {
1791
1889
  handleAgentDefinitionsMessage,
1792
1890
  handleAppViewMessage,
1793
1891
  handleExecutionDetailMessage,
1892
+ handleExecutionWorkspace,
1794
1893
  handleModelListMessage,
1795
1894
  handleProjectMessage,
1796
1895
  handleSchedulesMessage,
@@ -1828,6 +1927,14 @@ function useSessionClient(makeClient) {
1828
1927
  if (next !== "connected") {
1829
1928
  rejectPendingAppRequests();
1830
1929
  clearCompactionNotices();
1930
+ closeExecutionDetail();
1931
+ if (executionDetailInputRef.current !== null) {
1932
+ setExecutionDetailInput({
1933
+ taskId: executionDetailInputRef.current,
1934
+ status: "failed"
1935
+ });
1936
+ executionDetailInputRef.current = null;
1937
+ }
1831
1938
  }
1832
1939
  if (next === "connected") {
1833
1940
  commandsInFlightRef.current = 0;
@@ -1858,6 +1965,7 @@ function useSessionClient(makeClient) {
1858
1965
  makeClient,
1859
1966
  handleMessage,
1860
1967
  clearCompactionNotices,
1968
+ closeExecutionDetail,
1861
1969
  requestSessions,
1862
1970
  requestSchedules,
1863
1971
  armRestore,
@@ -1882,6 +1990,7 @@ function useSessionClient(makeClient) {
1882
1990
  answerAsk,
1883
1991
  ...personalUsageState,
1884
1992
  ...executionDetailState,
1993
+ executionDetailInput,
1885
1994
  closeExecutionDetail,
1886
1995
  ...modelListState,
1887
1996
  ...projectPanelState,
@@ -1916,6 +2025,63 @@ function useWsSession(url) {
1916
2025
  };
1917
2026
  }
1918
2027
  //#endregion
2028
+ //#region src/client/desktop-session-client.ts
2029
+ function createDesktopSessionClient(bridge, callbacks) {
2030
+ let connected = false;
2031
+ let subscribed = false;
2032
+ let release;
2033
+ let failed = false;
2034
+ let revoked = false;
2035
+ const close = () => {
2036
+ revoked = true;
2037
+ connected = false;
2038
+ if (!failed && subscribed) callbacks.onStatusChange("disconnected");
2039
+ };
2040
+ return {
2041
+ connect() {
2042
+ if (revoked || failed) return;
2043
+ if (subscribed) return;
2044
+ subscribed = true;
2045
+ callbacks.onStatusChange("connecting");
2046
+ try {
2047
+ release = bridge.subscribe((frame) => {
2048
+ if (!subscribed || failed || revoked) return;
2049
+ const decoded = (0, _robota_sdk_agent_transport_client.decodeFrame)(frame, _robota_sdk_agent_transport_client.decodeServerMessage);
2050
+ if (!decoded.ok) {
2051
+ failed = true;
2052
+ connected = false;
2053
+ bridge.close();
2054
+ callbacks.onStatusChange("error");
2055
+ return;
2056
+ }
2057
+ callbacks.onMessage(decoded.message);
2058
+ }, close, () => {
2059
+ if (!subscribed || failed || revoked) return;
2060
+ connected = true;
2061
+ callbacks.onStatusChange("connected");
2062
+ bridge.send(JSON.stringify({ type: "get-messages" }));
2063
+ });
2064
+ } catch {
2065
+ close();
2066
+ }
2067
+ },
2068
+ disconnect() {
2069
+ subscribed = false;
2070
+ connected = false;
2071
+ release?.();
2072
+ release = void 0;
2073
+ callbacks.onStatusChange("disconnected");
2074
+ },
2075
+ send(message) {
2076
+ if (!connected || failed) return;
2077
+ if (!bridge.send(JSON.stringify(message))) {
2078
+ bridge.close();
2079
+ close();
2080
+ }
2081
+ }
2082
+ };
2083
+ }
2084
+ //#endregion
1919
2085
  //#region src/client/runtime-host.ts
1920
2086
  /** Resolve host capabilities without importing Electron or a Node CLI into the browser graph. */
1921
2087
  function resolveClientRuntimeHost(environment) {
@@ -5881,16 +6047,32 @@ function writeDraft(namespace, sessionId, value) {
5881
6047
  * context). The row's controls run the session's own commands (`/provider`, `/mode`, `/effort`), so a
5882
6048
  * setting changes through the one path every client shares.
5883
6049
  */
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) {
6050
+ 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
6051
  const { identity, storage: { browserNamespace } } = useProductIdentity();
5886
6052
  const textareaRef = (0, react.useRef)(null);
5887
6053
  (0, react.useImperativeHandle)(ref, () => ({ focus: () => textareaRef.current?.focus() }), []);
5888
6054
  const sessionId = status?.sessionId;
6055
+ const workspacePath = status?.workspace?.path;
6056
+ const pickerGeneration = (0, react.useRef)(0);
6057
+ const [fileInputEpoch, setFileInputEpoch] = (0, react.useState)(0);
6058
+ const browserPickerOpen = (0, react.useRef)(false);
6059
+ (0, react.useLayoutEffect)(() => {
6060
+ pickerGeneration.current += 1;
6061
+ browserPickerOpen.current = false;
6062
+ return () => {
6063
+ pickerGeneration.current += 1;
6064
+ };
6065
+ }, [sessionId, workspacePath]);
6066
+ const invalidatePickers = () => {
6067
+ pickerGeneration.current += 1;
6068
+ browserPickerOpen.current = false;
6069
+ setFileInputEpoch((epoch) => epoch + 1);
6070
+ };
5889
6071
  const sessionIdRef = (0, react.useRef)(sessionId);
5890
6072
  const hasKnownSessionRef = (0, react.useRef)(sessionId !== void 0);
5891
6073
  const stateRef = (0, react.useRef)(EMPTY_DRAFT);
5892
6074
  const [draft, setDraftState] = (0, react.useState)(() => {
5893
- const initial = readDraft(browserNamespace, sessionId);
6075
+ const initial = draftMode === "volatile" ? EMPTY_DRAFT : readDraft(browserNamespace, sessionId);
5894
6076
  stateRef.current = initial;
5895
6077
  return initial.text;
5896
6078
  });
@@ -5914,7 +6096,7 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
5914
6096
  text: value
5915
6097
  };
5916
6098
  setDraftState(value);
5917
- writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
6099
+ if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
5918
6100
  };
5919
6101
  /** #3282 §4d: attachment chips persist alongside the text, under the same per-session draft key. */
5920
6102
  const setAttachments = (updater) => {
@@ -5924,9 +6106,10 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
5924
6106
  attachments: next
5925
6107
  };
5926
6108
  setAttachmentsState(next);
5927
- writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
6109
+ if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
5928
6110
  };
5929
6111
  (0, react.useEffect)(() => {
6112
+ if (draftMode === "volatile") return;
5930
6113
  if (sessionId === void 0 && hasKnownSessionRef.current) return;
5931
6114
  if (sessionIdRef.current === sessionId) return;
5932
6115
  const firstArrival = !hasKnownSessionRef.current;
@@ -5945,23 +6128,26 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
5945
6128
  setDraftState(stored.text);
5946
6129
  setAttachmentsState(stored.attachments);
5947
6130
  setAttachmentNotice(null);
5948
- }, [browserNamespace, sessionId]);
6131
+ }, [
6132
+ browserNamespace,
6133
+ draftMode,
6134
+ sessionId
6135
+ ]);
5949
6136
  const submit = () => {
5950
6137
  if (!connected) return;
5951
6138
  const text = draft.trim();
5952
6139
  if (!text && attachments.length === 0) return;
6140
+ invalidatePickers();
5953
6141
  onSubmit(buildPromptWithAttachments(text, attachments));
5954
6142
  setDraft("");
5955
6143
  setAttachments([]);
5956
6144
  setAttachmentNotice(null);
5957
6145
  };
5958
- const workspacePath = status?.workspace?.path;
5959
- const totalAttachedBytes = attachments.reduce((sum, a) => sum + a.size, 0);
5960
6146
  /** Evaluate every dropped/picked file in order, so a mixed batch attaches what it can. */
5961
6147
  const addCandidates = (candidates) => {
5962
6148
  if (candidates.length === 0) return;
5963
- let total = totalAttachedBytes;
5964
- let count = attachments.length;
6149
+ let total = stateRef.current.attachments.reduce((sum, attachment) => sum + attachment.size, 0);
6150
+ let count = stateRef.current.attachments.length;
5965
6151
  const added = [];
5966
6152
  let notice = null;
5967
6153
  for (const candidate of candidates) {
@@ -5985,19 +6171,44 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
5985
6171
  absolutePath: getPathForFile ? getPathForFile(file) || void 0 : void 0
5986
6172
  });
5987
6173
  const fileInputRef = (0, react.useRef)(null);
6174
+ (0, react.useEffect)(() => {
6175
+ const input = fileInputRef.current;
6176
+ if (!input) return;
6177
+ const cancel = () => {
6178
+ browserPickerOpen.current = false;
6179
+ };
6180
+ input.addEventListener("cancel", cancel);
6181
+ return () => {
6182
+ input.removeEventListener("cancel", cancel);
6183
+ };
6184
+ }, [
6185
+ sessionId,
6186
+ workspacePath,
6187
+ fileInputEpoch
6188
+ ]);
5988
6189
  const handleAttachClick = () => {
5989
6190
  if (!connected) return;
5990
6191
  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."));
6192
+ const generation = pickerGeneration.current;
6193
+ pickFiles().then((picked) => {
6194
+ if (generation !== pickerGeneration.current) return;
6195
+ addCandidates(picked.map((f) => ({
6196
+ name: f.name,
6197
+ size: f.size,
6198
+ absolutePath: f.path
6199
+ })));
6200
+ }).catch(() => {
6201
+ if (generation !== pickerGeneration.current) return;
6202
+ setAttachmentNotice("Could not open the file picker. Try again.");
6203
+ });
5996
6204
  return;
5997
6205
  }
6206
+ if (browserPickerOpen.current) return;
6207
+ browserPickerOpen.current = true;
5998
6208
  fileInputRef.current?.click();
5999
6209
  };
6000
6210
  const handleFileInputChange = (event) => {
6211
+ browserPickerOpen.current = false;
6001
6212
  const files = Array.from(event.target.files ?? []);
6002
6213
  event.target.value = "";
6003
6214
  addCandidates(files.map(toCandidateFromFile));
@@ -6029,6 +6240,7 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
6029
6240
  /** Choosing a command from the menu, by mouse or by keyboard (`acceptMenu` below). */
6030
6241
  const chooseMenuItem = (item) => {
6031
6242
  if (isImmediateScreenCommand(item)) {
6243
+ invalidatePickers();
6032
6244
  setDraft("");
6033
6245
  onCommand(item.name);
6034
6246
  return;
@@ -6191,7 +6403,11 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
6191
6403
  className: "sr-only",
6192
6404
  tabIndex: -1,
6193
6405
  "aria-hidden": "true"
6194
- }),
6406
+ }, JSON.stringify([
6407
+ sessionId ?? null,
6408
+ workspacePath ?? null,
6409
+ fileInputEpoch
6410
+ ])),
6195
6411
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6196
6412
  ref: textareaRef,
6197
6413
  "aria-label": "message",
@@ -6283,6 +6499,15 @@ const Composer = (0, react.forwardRef)(function Composer({ onSubmit, onCommand,
6283
6499
  ]
6284
6500
  });
6285
6501
  });
6502
+ /** Volatile state belongs to one session and never crosses a mode or session boundary. */
6503
+ const Composer = (0, react.forwardRef)(function Composer(props, ref) {
6504
+ const key = props.draftMode === "volatile" ? JSON.stringify(["volatile", props.status?.sessionId ?? null]) : "persistent";
6505
+ return /* @__PURE__ */ (0, react.createElement)(ComposerInstance, {
6506
+ ...props,
6507
+ key,
6508
+ ref
6509
+ });
6510
+ });
6286
6511
  /**
6287
6512
  * The goal being pursued (`/goal`), above the composer while it is active — the objective, how far
6288
6513
  * the turn budget has gone, and a way to stop it (`/goal cancel`).
@@ -6343,6 +6568,7 @@ const STATUS_TAG_TONE = {
6343
6568
  destructive: "bg-destructive/12 text-destructive",
6344
6569
  muted: "bg-subtle/15 text-subtle"
6345
6570
  };
6571
+ const FOLLOW_BOTTOM_MARGIN_PX = 32;
6346
6572
  const RECORD_KIND_LABEL = {
6347
6573
  message: "",
6348
6574
  tool_activity: "Tool",
@@ -6357,10 +6583,40 @@ const RECORD_KIND_LABEL = {
6357
6583
  * headline), its transcript so far (paginated via `onLoadMore`) and, once it has one, its result.
6358
6584
  * `entry === null` renders nothing; the caller owns showing/hiding this over the panel.
6359
6585
  */
6360
- function ExecutionDetailSheet({ entry, status, records, error, complete, onClose, onLoadMore, onStop }) {
6586
+ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose, onLoadMore, onStop, onSend, inputState, onContinue, mode = "live", unreadable, onShowEarlier, onShowLive }) {
6587
+ const [prompt, setPrompt] = react.default.useState("");
6588
+ const submitted = react.default.useRef(null);
6589
+ const outputRef = react.default.useRef(null);
6590
+ const followingRef = react.default.useRef(true);
6591
+ const isFork = Boolean(entry?.resumeSessionId && entry.controls.includes("attach"));
6592
+ react.default.useLayoutEffect(() => {
6593
+ followingRef.current = true;
6594
+ }, [entry?.id, mode]);
6595
+ react.default.useLayoutEffect(() => {
6596
+ const output = outputRef.current;
6597
+ if (output && isFork && mode === "live" && followingRef.current) output.scrollTop = output.scrollHeight;
6598
+ }, [
6599
+ entry?.id,
6600
+ isFork,
6601
+ mode,
6602
+ records
6603
+ ]);
6604
+ react.default.useEffect(() => {
6605
+ setPrompt("");
6606
+ submitted.current = null;
6607
+ }, [entry?.id]);
6608
+ react.default.useEffect(() => {
6609
+ if (!entry || inputState?.taskId !== entry.sourceId || inputState.status === "pending") return;
6610
+ if (inputState.status === "sent" && submitted.current !== null) {
6611
+ const sent = submitted.current;
6612
+ setPrompt((current) => current.trim() === sent ? "" : current);
6613
+ }
6614
+ submitted.current = null;
6615
+ }, [entry, inputState]);
6361
6616
  if (!entry) return null;
6362
6617
  const described = describeExecutionEntryStatus(entry);
6363
6618
  const canStop = onStop !== void 0 && entry.controls.includes("cancel");
6619
+ const ended = entry.status === "completed" || entry.status === "failed" || entry.status === "cancelled";
6364
6620
  const askedLine = entry.headline?.text ?? entry.preview;
6365
6621
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6366
6622
  role: "dialog",
@@ -6415,10 +6671,37 @@ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose
6415
6671
  entry.deniedToolCalls,
6416
6672
  " tool ",
6417
6673
  entry.deniedToolCalls === 1 ? "call was" : "calls were",
6418
- " refused."
6674
+ " ",
6675
+ "refused."
6419
6676
  ]
6420
6677
  }),
6678
+ isFork && onShowEarlier && onShowLive && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6679
+ className: "flex gap-3 border-b border-subtle/15 px-4 py-2 text-[12px]",
6680
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6681
+ type: "button",
6682
+ onClick: onShowLive,
6683
+ disabled: mode === "live",
6684
+ children: "Live output"
6685
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6686
+ type: "button",
6687
+ onClick: onShowEarlier,
6688
+ disabled: mode === "transcript",
6689
+ children: "Earlier output"
6690
+ })]
6691
+ }),
6421
6692
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6693
+ ref: outputRef,
6694
+ role: isFork ? mode === "live" ? "log" : "region" : void 0,
6695
+ "aria-label": isFork ? mode === "live" ? "Live fork output" : "Earlier fork output" : void 0,
6696
+ "aria-live": isFork && mode === "live" ? "polite" : void 0,
6697
+ "aria-relevant": isFork && mode === "live" ? "additions text" : void 0,
6698
+ "aria-atomic": isFork && mode === "live" ? false : void 0,
6699
+ tabIndex: isFork ? 0 : void 0,
6700
+ onScroll: (event) => {
6701
+ if (!isFork || mode !== "live") return;
6702
+ const output = event.currentTarget;
6703
+ followingRef.current = output.scrollHeight - output.scrollTop - output.clientHeight <= FOLLOW_BOTTOM_MARGIN_PX;
6704
+ },
6422
6705
  className: "flex-1 space-y-2 overflow-y-auto px-4 py-3",
6423
6706
  children: [
6424
6707
  records.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -6439,17 +6722,54 @@ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose
6439
6722
  className: "text-[13px] text-destructive",
6440
6723
  children: error
6441
6724
  }),
6442
- !complete && status !== "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6725
+ unreadable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
6726
+ className: "text-[12px] text-warning",
6727
+ children: [
6728
+ "Some records could not be read: ",
6729
+ unreadable.locked,
6730
+ " locked, ",
6731
+ unreadable.noKeys,
6732
+ " without keys, ",
6733
+ unreadable.doesNotOpen,
6734
+ " unavailable."
6735
+ ]
6736
+ }),
6737
+ (!isFork || mode === "transcript") && !complete && status !== "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6443
6738
  type: "button",
6444
6739
  onClick: onLoadMore,
6445
6740
  className: "text-[12px] text-subtle underline hover:text-foreground",
6446
6741
  children: "Load more"
6447
6742
  }),
6448
- !complete && status === "loading" && records.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6743
+ (!isFork || mode === "transcript") && !complete && status === "loading" && records.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6449
6744
  className: "text-[12px] text-subtle",
6450
6745
  children: "Loading more…"
6451
6746
  })
6452
6747
  ]
6748
+ }),
6749
+ isFork && !ended && entry.controls.includes("send") && onSend && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
6750
+ className: "border-t border-subtle/15 p-3",
6751
+ onSubmit: (event) => {
6752
+ event.preventDefault();
6753
+ if (!prompt.trim() || submitted.current !== null || inputState?.status === "pending") return;
6754
+ submitted.current = prompt.trim();
6755
+ onSend(submitted.current);
6756
+ },
6757
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6758
+ "aria-label": "Message the fork",
6759
+ value: prompt,
6760
+ onChange: (event) => setPrompt(event.target.value),
6761
+ className: "w-full rounded border border-subtle/20 bg-background p-2 text-foreground"
6762
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6763
+ type: "submit",
6764
+ disabled: !prompt.trim() || inputState?.status === "pending",
6765
+ children: "Send to fork"
6766
+ })]
6767
+ }),
6768
+ isFork && ended && onContinue && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6769
+ type: "button",
6770
+ onClick: onContinue,
6771
+ className: "border-t border-subtle/15 p-3 text-foreground",
6772
+ children: "Continue as session"
6453
6773
  })
6454
6774
  ]
6455
6775
  });
@@ -8127,7 +8447,7 @@ function SetupPanel({ onSetUp, disabled }) {
8127
8447
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
8128
8448
  * mark (e.g. "app").
8129
8449
  */
8130
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
8450
+ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMode = "persistent", onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
8131
8451
  const [view, setView] = (0, react.useState)("chat");
8132
8452
  const { identity } = useProductIdentity();
8133
8453
  const appViewHost = (0, react.useMemo)(() => ({
@@ -8306,6 +8626,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8306
8626
  }),
8307
8627
  setupRequired ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Composer, {
8308
8628
  ref: composerRef,
8629
+ draftMode,
8309
8630
  catalog: state.commandCatalog ?? null,
8310
8631
  status: state.sessionStatus ?? null,
8311
8632
  connected: state.status === "connected",
@@ -8379,7 +8700,20 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8379
8700
  complete: state.executionDetailComplete,
8380
8701
  onClose: () => state.closeExecutionDetail(),
8381
8702
  onLoadMore: () => state.loadMoreExecutionDetail(),
8382
- onStop: openEntry ? () => stopExecutionEntry(openEntry) : void 0
8703
+ onStop: openEntry ? () => stopExecutionEntry(openEntry) : void 0,
8704
+ inputState: state.executionDetailInput,
8705
+ mode: state.executionDetailMode,
8706
+ unreadable: state.executionDetailUnreadable,
8707
+ onShowEarlier: state.showEarlierExecutionDetail,
8708
+ onShowLive: state.showLiveExecutionDetail,
8709
+ onSend: openEntry ? (prompt) => state.send({
8710
+ type: "send-background-task",
8711
+ taskId: openEntry.sourceId,
8712
+ input: { prompt }
8713
+ }) : void 0,
8714
+ onContinue: openEntry?.resumeSessionId ? () => {
8715
+ if (openEntry.resumeSessionId) state.switchSession(openEntry.resumeSessionId);
8716
+ } : void 0
8383
8717
  })
8384
8718
  ]
8385
8719
  })]
@@ -8582,6 +8916,7 @@ exports.SessionSurface = SessionSurface;
8582
8916
  exports.SettingsScreen = SettingsScreen;
8583
8917
  exports.applyPromptEvent = applyPromptEvent;
8584
8918
  exports.askResponse = askResponse;
8919
+ exports.createDesktopSessionClient = createDesktopSessionClient;
8585
8920
  exports.createWsSessionClient = createWsSessionClient;
8586
8921
  exports.describeUiIntentForGui = describeUiIntentForGui;
8587
8922
  exports.guiScreenForUiIntent = guiScreenForUiIntent;