@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.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import React, { createContext, createElement, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { decodeFrame, decodeServerMessage } from "@robota-sdk/agent-transport/client";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import ReactMarkdown from "react-markdown";
@@ -514,13 +514,6 @@ function useAppViewRequests(send, isConnected) {
514
514
  }
515
515
  //#endregion
516
516
  //#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
517
  let requestCounter$5 = 0;
525
518
  function nextRequestId$5() {
526
519
  requestCounter$5 += 1;
@@ -532,14 +525,27 @@ function useExecutionDetailState(send) {
532
525
  const [records, setRecords] = useState([]);
533
526
  const [error, setError] = useState(null);
534
527
  const [complete, setComplete] = useState(false);
528
+ const [mode, setMode] = useState("live");
529
+ const [unreadable, setUnreadable] = useState();
535
530
  const requestIdRef = useRef(null);
536
531
  const entryIdRef = useRef(null);
532
+ const modeRef = useRef("live");
537
533
  const nextCursorRef = useRef(void 0);
538
- const readPage = useCallback((entryId, cursor) => {
534
+ const queuedRef = useRef(null);
535
+ const timerRef = useRef(null);
536
+ const lastReadAtRef = useRef(0);
537
+ const readPage = useCallback((entryId, cursor, refresh = false) => {
538
+ if (requestIdRef.current !== null) {
539
+ if (!refresh || queuedRef.current === null || queuedRef.current.refresh) queuedRef.current = {
540
+ cursor,
541
+ refresh
542
+ };
543
+ return;
544
+ }
539
545
  const requestId = nextRequestId$5();
540
546
  requestIdRef.current = requestId;
541
- entryIdRef.current = entryId;
542
- setStatus("loading");
547
+ lastReadAtRef.current = Date.now();
548
+ if (!refresh) setStatus("loading");
543
549
  setError(null);
544
550
  send({
545
551
  type: "read-execution-detail",
@@ -548,51 +554,118 @@ function useExecutionDetailState(send) {
548
554
  cursor
549
555
  });
550
556
  }, [send]);
557
+ const clearPending = useCallback(() => {
558
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
559
+ timerRef.current = null;
560
+ queuedRef.current = null;
561
+ }, []);
562
+ useEffect(() => clearPending, [clearPending]);
563
+ const openExecutionDetail = useCallback((entryId) => {
564
+ clearPending();
565
+ requestIdRef.current = null;
566
+ entryIdRef.current = entryId;
567
+ modeRef.current = "live";
568
+ setMode("live");
569
+ setOpenEntryId(entryId);
570
+ setRecords([]);
571
+ setUnreadable(void 0);
572
+ setComplete(false);
573
+ nextCursorRef.current = void 0;
574
+ readPage(entryId);
575
+ }, [clearPending, readPage]);
576
+ const loadMoreExecutionDetail = useCallback(() => {
577
+ if (!openEntryId || status === "loading" || complete) return;
578
+ readPage(openEntryId, nextCursorRef.current);
579
+ }, [
580
+ openEntryId,
581
+ status,
582
+ complete,
583
+ readPage
584
+ ]);
585
+ const changeMode = useCallback((next) => {
586
+ const entryId = entryIdRef.current;
587
+ if (!entryId) return;
588
+ clearPending();
589
+ modeRef.current = next;
590
+ setMode(next);
591
+ setRecords([]);
592
+ setUnreadable(void 0);
593
+ setComplete(false);
594
+ setStatus("loading");
595
+ nextCursorRef.current = void 0;
596
+ readPage(entryId, next === "transcript" ? { offset: 0 } : void 0);
597
+ }, [clearPending, readPage]);
598
+ const showEarlierExecutionDetail = useCallback(() => changeMode("transcript"), [changeMode]);
599
+ const showLiveExecutionDetail = useCallback(() => changeMode("live"), [changeMode]);
600
+ const closeExecutionDetail = useCallback(() => {
601
+ clearPending();
602
+ setOpenEntryId(null);
603
+ requestIdRef.current = null;
604
+ entryIdRef.current = null;
605
+ setRecords([]);
606
+ setUnreadable(void 0);
607
+ setStatus("idle");
608
+ setError(null);
609
+ setComplete(false);
610
+ }, [clearPending]);
611
+ const scheduleRefresh = useCallback((entryId) => {
612
+ if (timerRef.current !== null) return;
613
+ const delay = Math.max(0, 250 - (Date.now() - lastReadAtRef.current));
614
+ timerRef.current = setTimeout(() => {
615
+ timerRef.current = null;
616
+ if (entryIdRef.current === entryId && modeRef.current === "live") readPage(entryId, void 0, true);
617
+ }, delay);
618
+ }, [readPage]);
619
+ const handleExecutionWorkspace = useCallback((snapshot) => {
620
+ const entryId = entryIdRef.current;
621
+ if (!entryId || modeRef.current !== "live") return;
622
+ const entry = snapshot.entries.find((item) => item.id === entryId);
623
+ if (entry?.resumeSessionId && entry.controls.includes("attach")) scheduleRefresh(entryId);
624
+ }, [scheduleRefresh]);
551
625
  return {
552
626
  openEntryId,
553
627
  executionDetailStatus: status,
554
628
  executionDetailRecords: records,
555
629
  executionDetailError: error,
556
630
  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
- }, []),
631
+ executionDetailMode: mode,
632
+ executionDetailUnreadable: unreadable,
633
+ openExecutionDetail,
634
+ loadMoreExecutionDetail,
635
+ showEarlierExecutionDetail,
636
+ showLiveExecutionDetail,
637
+ closeExecutionDetail,
582
638
  handleExecutionDetailMessage: useCallback((msg) => {
583
639
  if (msg.type !== "execution_detail" && msg.type !== "execution_detail_error") return false;
584
640
  if (requestIdRef.current !== msg.requestId) return true;
585
- if (msg.type === "execution_detail_error") {
641
+ requestIdRef.current = null;
642
+ const queued = queuedRef.current;
643
+ queuedRef.current = null;
644
+ if (!queued || queued.refresh) if (msg.type === "execution_detail_error") {
586
645
  setStatus("error");
587
646
  setError(msg.message);
588
- return true;
647
+ } else {
648
+ setRecords((previous) => msg.page.cursor === void 0 || msg.page.cursor.offset === 0 ? msg.page.records : [...previous, ...msg.page.records]);
649
+ setUnreadable((previous) => {
650
+ const current = msg.page.unreadable;
651
+ if (msg.page.cursor === void 0 || msg.page.cursor.offset === 0) return current;
652
+ if (!previous) return current;
653
+ if (!current) return previous;
654
+ return {
655
+ locked: previous.locked + current.locked,
656
+ noKeys: previous.noKeys + current.noKeys,
657
+ doesNotOpen: previous.doesNotOpen + current.doesNotOpen
658
+ };
659
+ });
660
+ nextCursorRef.current = msg.page.nextCursor;
661
+ setComplete(msg.page.nextCursor === void 0);
662
+ setStatus("ready");
589
663
  }
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");
664
+ if (queued && entryIdRef.current) if (queued.refresh) scheduleRefresh(entryIdRef.current);
665
+ else readPage(entryIdRef.current, queued.cursor);
594
666
  return true;
595
- }, [])
667
+ }, [readPage, scheduleRefresh]),
668
+ handleExecutionWorkspace
596
669
  };
597
670
  }
598
671
  //#endregion
@@ -1361,7 +1434,17 @@ function useSessionClient(makeClient) {
1361
1434
  const silentCommandRequestIdsRef = useRef(/* @__PURE__ */ new Set());
1362
1435
  const sessionChangesInFlightRef = useRef(0);
1363
1436
  const sessionChangeRequestIdsRef = useRef(/* @__PURE__ */ new Set());
1437
+ const [executionDetailInput, setExecutionDetailInput] = useState();
1438
+ const executionDetailInputRef = useRef(null);
1364
1439
  const send = useCallback((msg) => {
1440
+ if (msg.type === "send-background-task") {
1441
+ if (executionDetailInputRef.current !== null) return;
1442
+ executionDetailInputRef.current = msg.taskId;
1443
+ setExecutionDetailInput({
1444
+ taskId: msg.taskId,
1445
+ status: "pending"
1446
+ });
1447
+ }
1365
1448
  if (msg.type === "command") {
1366
1449
  if (msg.name === "help") {
1367
1450
  setHelpOpen(true);
@@ -1418,7 +1501,7 @@ function useSessionClient(makeClient) {
1418
1501
  });
1419
1502
  }, [send]);
1420
1503
  const { handleUsageMessage, ...personalUsageState } = usePersonalUsageState(send);
1421
- const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1504
+ const { handleExecutionDetailMessage, handleExecutionWorkspace, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1422
1505
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1423
1506
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1424
1507
  const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, useCallback(() => statusRef.current === "connected", []));
@@ -1502,13 +1585,21 @@ function useSessionClient(makeClient) {
1502
1585
  break;
1503
1586
  }
1504
1587
  case "execution_workspace_event":
1588
+ handleExecutionWorkspace(msg.snapshot);
1505
1589
  setExecutionWorkspace(msg.snapshot);
1506
1590
  break;
1507
1591
  case "background_task_control_result":
1592
+ if (msg.action === "send" && executionDetailInputRef.current === msg.taskId) {
1593
+ executionDetailInputRef.current = null;
1594
+ setExecutionDetailInput({
1595
+ taskId: msg.taskId,
1596
+ status: msg.success ? "sent" : "failed"
1597
+ });
1598
+ }
1508
1599
  if (!msg.success) setSessionNotices((previous) => [...previous, {
1509
1600
  id: nextId(),
1510
1601
  kind: "background-task-control-failed",
1511
- message: msg.message ?? `Could not stop ${msg.taskId}.`
1602
+ message: msg.message ?? `Could not ${msg.action === "send" ? "send to" : "stop"} ${msg.taskId}.`
1512
1603
  }]);
1513
1604
  break;
1514
1605
  case "pending":
@@ -1653,6 +1744,13 @@ function useSessionClient(makeClient) {
1653
1744
  }]);
1654
1745
  break;
1655
1746
  case "protocol_error": {
1747
+ if (executionDetailInputRef.current !== null && msg.message === "Not permitted for an observer: send-background-task") {
1748
+ setExecutionDetailInput({
1749
+ taskId: executionDetailInputRef.current,
1750
+ status: "failed"
1751
+ });
1752
+ executionDetailInputRef.current = null;
1753
+ }
1656
1754
  if (sessionChangesInFlightRef.current > 0) {
1657
1755
  settleSessionChange();
1658
1756
  setSessionNotices((previous) => [...previous, {
@@ -1765,6 +1863,7 @@ function useSessionClient(makeClient) {
1765
1863
  handleAgentDefinitionsMessage,
1766
1864
  handleAppViewMessage,
1767
1865
  handleExecutionDetailMessage,
1866
+ handleExecutionWorkspace,
1768
1867
  handleModelListMessage,
1769
1868
  handleProjectMessage,
1770
1869
  handleSchedulesMessage,
@@ -1802,6 +1901,14 @@ function useSessionClient(makeClient) {
1802
1901
  if (next !== "connected") {
1803
1902
  rejectPendingAppRequests();
1804
1903
  clearCompactionNotices();
1904
+ closeExecutionDetail();
1905
+ if (executionDetailInputRef.current !== null) {
1906
+ setExecutionDetailInput({
1907
+ taskId: executionDetailInputRef.current,
1908
+ status: "failed"
1909
+ });
1910
+ executionDetailInputRef.current = null;
1911
+ }
1805
1912
  }
1806
1913
  if (next === "connected") {
1807
1914
  commandsInFlightRef.current = 0;
@@ -1832,6 +1939,7 @@ function useSessionClient(makeClient) {
1832
1939
  makeClient,
1833
1940
  handleMessage,
1834
1941
  clearCompactionNotices,
1942
+ closeExecutionDetail,
1835
1943
  requestSessions,
1836
1944
  requestSchedules,
1837
1945
  armRestore,
@@ -1856,6 +1964,7 @@ function useSessionClient(makeClient) {
1856
1964
  answerAsk,
1857
1965
  ...personalUsageState,
1858
1966
  ...executionDetailState,
1967
+ executionDetailInput,
1859
1968
  closeExecutionDetail,
1860
1969
  ...modelListState,
1861
1970
  ...projectPanelState,
@@ -1890,6 +1999,63 @@ function useWsSession(url) {
1890
1999
  };
1891
2000
  }
1892
2001
  //#endregion
2002
+ //#region src/client/desktop-session-client.ts
2003
+ function createDesktopSessionClient(bridge, callbacks) {
2004
+ let connected = false;
2005
+ let subscribed = false;
2006
+ let release;
2007
+ let failed = false;
2008
+ let revoked = false;
2009
+ const close = () => {
2010
+ revoked = true;
2011
+ connected = false;
2012
+ if (!failed && subscribed) callbacks.onStatusChange("disconnected");
2013
+ };
2014
+ return {
2015
+ connect() {
2016
+ if (revoked || failed) return;
2017
+ if (subscribed) return;
2018
+ subscribed = true;
2019
+ callbacks.onStatusChange("connecting");
2020
+ try {
2021
+ release = bridge.subscribe((frame) => {
2022
+ if (!subscribed || failed || revoked) return;
2023
+ const decoded = decodeFrame(frame, decodeServerMessage);
2024
+ if (!decoded.ok) {
2025
+ failed = true;
2026
+ connected = false;
2027
+ bridge.close();
2028
+ callbacks.onStatusChange("error");
2029
+ return;
2030
+ }
2031
+ callbacks.onMessage(decoded.message);
2032
+ }, close, () => {
2033
+ if (!subscribed || failed || revoked) return;
2034
+ connected = true;
2035
+ callbacks.onStatusChange("connected");
2036
+ bridge.send(JSON.stringify({ type: "get-messages" }));
2037
+ });
2038
+ } catch {
2039
+ close();
2040
+ }
2041
+ },
2042
+ disconnect() {
2043
+ subscribed = false;
2044
+ connected = false;
2045
+ release?.();
2046
+ release = void 0;
2047
+ callbacks.onStatusChange("disconnected");
2048
+ },
2049
+ send(message) {
2050
+ if (!connected || failed) return;
2051
+ if (!bridge.send(JSON.stringify(message))) {
2052
+ bridge.close();
2053
+ close();
2054
+ }
2055
+ }
2056
+ };
2057
+ }
2058
+ //#endregion
1893
2059
  //#region src/client/runtime-host.ts
1894
2060
  /** Resolve host capabilities without importing Electron or a Node CLI into the browser graph. */
1895
2061
  function resolveClientRuntimeHost(environment) {
@@ -5855,16 +6021,32 @@ function writeDraft(namespace, sessionId, value) {
5855
6021
  * context). The row's controls run the session's own commands (`/provider`, `/mode`, `/effort`), so a
5856
6022
  * setting changes through the one path every client shares.
5857
6023
  */
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) {
6024
+ 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
6025
  const { identity, storage: { browserNamespace } } = useProductIdentity();
5860
6026
  const textareaRef = useRef(null);
5861
6027
  useImperativeHandle(ref, () => ({ focus: () => textareaRef.current?.focus() }), []);
5862
6028
  const sessionId = status?.sessionId;
6029
+ const workspacePath = status?.workspace?.path;
6030
+ const pickerGeneration = useRef(0);
6031
+ const [fileInputEpoch, setFileInputEpoch] = useState(0);
6032
+ const browserPickerOpen = useRef(false);
6033
+ useLayoutEffect(() => {
6034
+ pickerGeneration.current += 1;
6035
+ browserPickerOpen.current = false;
6036
+ return () => {
6037
+ pickerGeneration.current += 1;
6038
+ };
6039
+ }, [sessionId, workspacePath]);
6040
+ const invalidatePickers = () => {
6041
+ pickerGeneration.current += 1;
6042
+ browserPickerOpen.current = false;
6043
+ setFileInputEpoch((epoch) => epoch + 1);
6044
+ };
5863
6045
  const sessionIdRef = useRef(sessionId);
5864
6046
  const hasKnownSessionRef = useRef(sessionId !== void 0);
5865
6047
  const stateRef = useRef(EMPTY_DRAFT);
5866
6048
  const [draft, setDraftState] = useState(() => {
5867
- const initial = readDraft(browserNamespace, sessionId);
6049
+ const initial = draftMode === "volatile" ? EMPTY_DRAFT : readDraft(browserNamespace, sessionId);
5868
6050
  stateRef.current = initial;
5869
6051
  return initial.text;
5870
6052
  });
@@ -5888,7 +6070,7 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
5888
6070
  text: value
5889
6071
  };
5890
6072
  setDraftState(value);
5891
- writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
6073
+ if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
5892
6074
  };
5893
6075
  /** #3282 §4d: attachment chips persist alongside the text, under the same per-session draft key. */
5894
6076
  const setAttachments = (updater) => {
@@ -5898,9 +6080,10 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
5898
6080
  attachments: next
5899
6081
  };
5900
6082
  setAttachmentsState(next);
5901
- writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
6083
+ if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
5902
6084
  };
5903
6085
  useEffect(() => {
6086
+ if (draftMode === "volatile") return;
5904
6087
  if (sessionId === void 0 && hasKnownSessionRef.current) return;
5905
6088
  if (sessionIdRef.current === sessionId) return;
5906
6089
  const firstArrival = !hasKnownSessionRef.current;
@@ -5919,23 +6102,26 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
5919
6102
  setDraftState(stored.text);
5920
6103
  setAttachmentsState(stored.attachments);
5921
6104
  setAttachmentNotice(null);
5922
- }, [browserNamespace, sessionId]);
6105
+ }, [
6106
+ browserNamespace,
6107
+ draftMode,
6108
+ sessionId
6109
+ ]);
5923
6110
  const submit = () => {
5924
6111
  if (!connected) return;
5925
6112
  const text = draft.trim();
5926
6113
  if (!text && attachments.length === 0) return;
6114
+ invalidatePickers();
5927
6115
  onSubmit(buildPromptWithAttachments(text, attachments));
5928
6116
  setDraft("");
5929
6117
  setAttachments([]);
5930
6118
  setAttachmentNotice(null);
5931
6119
  };
5932
- const workspacePath = status?.workspace?.path;
5933
- const totalAttachedBytes = attachments.reduce((sum, a) => sum + a.size, 0);
5934
6120
  /** Evaluate every dropped/picked file in order, so a mixed batch attaches what it can. */
5935
6121
  const addCandidates = (candidates) => {
5936
6122
  if (candidates.length === 0) return;
5937
- let total = totalAttachedBytes;
5938
- let count = attachments.length;
6123
+ let total = stateRef.current.attachments.reduce((sum, attachment) => sum + attachment.size, 0);
6124
+ let count = stateRef.current.attachments.length;
5939
6125
  const added = [];
5940
6126
  let notice = null;
5941
6127
  for (const candidate of candidates) {
@@ -5959,19 +6145,44 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
5959
6145
  absolutePath: getPathForFile ? getPathForFile(file) || void 0 : void 0
5960
6146
  });
5961
6147
  const fileInputRef = useRef(null);
6148
+ useEffect(() => {
6149
+ const input = fileInputRef.current;
6150
+ if (!input) return;
6151
+ const cancel = () => {
6152
+ browserPickerOpen.current = false;
6153
+ };
6154
+ input.addEventListener("cancel", cancel);
6155
+ return () => {
6156
+ input.removeEventListener("cancel", cancel);
6157
+ };
6158
+ }, [
6159
+ sessionId,
6160
+ workspacePath,
6161
+ fileInputEpoch
6162
+ ]);
5962
6163
  const handleAttachClick = () => {
5963
6164
  if (!connected) return;
5964
6165
  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."));
6166
+ const generation = pickerGeneration.current;
6167
+ pickFiles().then((picked) => {
6168
+ if (generation !== pickerGeneration.current) return;
6169
+ addCandidates(picked.map((f) => ({
6170
+ name: f.name,
6171
+ size: f.size,
6172
+ absolutePath: f.path
6173
+ })));
6174
+ }).catch(() => {
6175
+ if (generation !== pickerGeneration.current) return;
6176
+ setAttachmentNotice("Could not open the file picker. Try again.");
6177
+ });
5970
6178
  return;
5971
6179
  }
6180
+ if (browserPickerOpen.current) return;
6181
+ browserPickerOpen.current = true;
5972
6182
  fileInputRef.current?.click();
5973
6183
  };
5974
6184
  const handleFileInputChange = (event) => {
6185
+ browserPickerOpen.current = false;
5975
6186
  const files = Array.from(event.target.files ?? []);
5976
6187
  event.target.value = "";
5977
6188
  addCandidates(files.map(toCandidateFromFile));
@@ -6003,6 +6214,7 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
6003
6214
  /** Choosing a command from the menu, by mouse or by keyboard (`acceptMenu` below). */
6004
6215
  const chooseMenuItem = (item) => {
6005
6216
  if (isImmediateScreenCommand(item)) {
6217
+ invalidatePickers();
6006
6218
  setDraft("");
6007
6219
  onCommand(item.name);
6008
6220
  return;
@@ -6165,7 +6377,11 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
6165
6377
  className: "sr-only",
6166
6378
  tabIndex: -1,
6167
6379
  "aria-hidden": "true"
6168
- }),
6380
+ }, JSON.stringify([
6381
+ sessionId ?? null,
6382
+ workspacePath ?? null,
6383
+ fileInputEpoch
6384
+ ])),
6169
6385
  /* @__PURE__ */ jsx("textarea", {
6170
6386
  ref: textareaRef,
6171
6387
  "aria-label": "message",
@@ -6257,6 +6473,15 @@ const Composer = forwardRef(function Composer({ onSubmit, onCommand, catalog, st
6257
6473
  ]
6258
6474
  });
6259
6475
  });
6476
+ /** Volatile state belongs to one session and never crosses a mode or session boundary. */
6477
+ const Composer = forwardRef(function Composer(props, ref) {
6478
+ const key = props.draftMode === "volatile" ? JSON.stringify(["volatile", props.status?.sessionId ?? null]) : "persistent";
6479
+ return /* @__PURE__ */ createElement(ComposerInstance, {
6480
+ ...props,
6481
+ key,
6482
+ ref
6483
+ });
6484
+ });
6260
6485
  /**
6261
6486
  * The goal being pursued (`/goal`), above the composer while it is active — the objective, how far
6262
6487
  * the turn budget has gone, and a way to stop it (`/goal cancel`).
@@ -6317,6 +6542,7 @@ const STATUS_TAG_TONE = {
6317
6542
  destructive: "bg-destructive/12 text-destructive",
6318
6543
  muted: "bg-subtle/15 text-subtle"
6319
6544
  };
6545
+ const FOLLOW_BOTTOM_MARGIN_PX = 32;
6320
6546
  const RECORD_KIND_LABEL = {
6321
6547
  message: "",
6322
6548
  tool_activity: "Tool",
@@ -6331,10 +6557,40 @@ const RECORD_KIND_LABEL = {
6331
6557
  * headline), its transcript so far (paginated via `onLoadMore`) and, once it has one, its result.
6332
6558
  * `entry === null` renders nothing; the caller owns showing/hiding this over the panel.
6333
6559
  */
6334
- function ExecutionDetailSheet({ entry, status, records, error, complete, onClose, onLoadMore, onStop }) {
6560
+ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose, onLoadMore, onStop, onSend, inputState, onContinue, mode = "live", unreadable, onShowEarlier, onShowLive }) {
6561
+ const [prompt, setPrompt] = React.useState("");
6562
+ const submitted = React.useRef(null);
6563
+ const outputRef = React.useRef(null);
6564
+ const followingRef = React.useRef(true);
6565
+ const isFork = Boolean(entry?.resumeSessionId && entry.controls.includes("attach"));
6566
+ React.useLayoutEffect(() => {
6567
+ followingRef.current = true;
6568
+ }, [entry?.id, mode]);
6569
+ React.useLayoutEffect(() => {
6570
+ const output = outputRef.current;
6571
+ if (output && isFork && mode === "live" && followingRef.current) output.scrollTop = output.scrollHeight;
6572
+ }, [
6573
+ entry?.id,
6574
+ isFork,
6575
+ mode,
6576
+ records
6577
+ ]);
6578
+ React.useEffect(() => {
6579
+ setPrompt("");
6580
+ submitted.current = null;
6581
+ }, [entry?.id]);
6582
+ React.useEffect(() => {
6583
+ if (!entry || inputState?.taskId !== entry.sourceId || inputState.status === "pending") return;
6584
+ if (inputState.status === "sent" && submitted.current !== null) {
6585
+ const sent = submitted.current;
6586
+ setPrompt((current) => current.trim() === sent ? "" : current);
6587
+ }
6588
+ submitted.current = null;
6589
+ }, [entry, inputState]);
6335
6590
  if (!entry) return null;
6336
6591
  const described = describeExecutionEntryStatus(entry);
6337
6592
  const canStop = onStop !== void 0 && entry.controls.includes("cancel");
6593
+ const ended = entry.status === "completed" || entry.status === "failed" || entry.status === "cancelled";
6338
6594
  const askedLine = entry.headline?.text ?? entry.preview;
6339
6595
  return /* @__PURE__ */ jsxs("div", {
6340
6596
  role: "dialog",
@@ -6389,10 +6645,37 @@ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose
6389
6645
  entry.deniedToolCalls,
6390
6646
  " tool ",
6391
6647
  entry.deniedToolCalls === 1 ? "call was" : "calls were",
6392
- " refused."
6648
+ " ",
6649
+ "refused."
6393
6650
  ]
6394
6651
  }),
6652
+ isFork && onShowEarlier && onShowLive && /* @__PURE__ */ jsxs("div", {
6653
+ className: "flex gap-3 border-b border-subtle/15 px-4 py-2 text-[12px]",
6654
+ children: [/* @__PURE__ */ jsx("button", {
6655
+ type: "button",
6656
+ onClick: onShowLive,
6657
+ disabled: mode === "live",
6658
+ children: "Live output"
6659
+ }), /* @__PURE__ */ jsx("button", {
6660
+ type: "button",
6661
+ onClick: onShowEarlier,
6662
+ disabled: mode === "transcript",
6663
+ children: "Earlier output"
6664
+ })]
6665
+ }),
6395
6666
  /* @__PURE__ */ jsxs("div", {
6667
+ ref: outputRef,
6668
+ role: isFork ? mode === "live" ? "log" : "region" : void 0,
6669
+ "aria-label": isFork ? mode === "live" ? "Live fork output" : "Earlier fork output" : void 0,
6670
+ "aria-live": isFork && mode === "live" ? "polite" : void 0,
6671
+ "aria-relevant": isFork && mode === "live" ? "additions text" : void 0,
6672
+ "aria-atomic": isFork && mode === "live" ? false : void 0,
6673
+ tabIndex: isFork ? 0 : void 0,
6674
+ onScroll: (event) => {
6675
+ if (!isFork || mode !== "live") return;
6676
+ const output = event.currentTarget;
6677
+ followingRef.current = output.scrollHeight - output.scrollTop - output.clientHeight <= FOLLOW_BOTTOM_MARGIN_PX;
6678
+ },
6396
6679
  className: "flex-1 space-y-2 overflow-y-auto px-4 py-3",
6397
6680
  children: [
6398
6681
  records.map((record) => /* @__PURE__ */ jsxs("div", {
@@ -6413,17 +6696,54 @@ function ExecutionDetailSheet({ entry, status, records, error, complete, onClose
6413
6696
  className: "text-[13px] text-destructive",
6414
6697
  children: error
6415
6698
  }),
6416
- !complete && status !== "loading" && /* @__PURE__ */ jsx("button", {
6699
+ unreadable && /* @__PURE__ */ jsxs("p", {
6700
+ className: "text-[12px] text-warning",
6701
+ children: [
6702
+ "Some records could not be read: ",
6703
+ unreadable.locked,
6704
+ " locked, ",
6705
+ unreadable.noKeys,
6706
+ " without keys, ",
6707
+ unreadable.doesNotOpen,
6708
+ " unavailable."
6709
+ ]
6710
+ }),
6711
+ (!isFork || mode === "transcript") && !complete && status !== "loading" && /* @__PURE__ */ jsx("button", {
6417
6712
  type: "button",
6418
6713
  onClick: onLoadMore,
6419
6714
  className: "text-[12px] text-subtle underline hover:text-foreground",
6420
6715
  children: "Load more"
6421
6716
  }),
6422
- !complete && status === "loading" && records.length > 0 && /* @__PURE__ */ jsx("p", {
6717
+ (!isFork || mode === "transcript") && !complete && status === "loading" && records.length > 0 && /* @__PURE__ */ jsx("p", {
6423
6718
  className: "text-[12px] text-subtle",
6424
6719
  children: "Loading more…"
6425
6720
  })
6426
6721
  ]
6722
+ }),
6723
+ isFork && !ended && entry.controls.includes("send") && onSend && /* @__PURE__ */ jsxs("form", {
6724
+ className: "border-t border-subtle/15 p-3",
6725
+ onSubmit: (event) => {
6726
+ event.preventDefault();
6727
+ if (!prompt.trim() || submitted.current !== null || inputState?.status === "pending") return;
6728
+ submitted.current = prompt.trim();
6729
+ onSend(submitted.current);
6730
+ },
6731
+ children: [/* @__PURE__ */ jsx("textarea", {
6732
+ "aria-label": "Message the fork",
6733
+ value: prompt,
6734
+ onChange: (event) => setPrompt(event.target.value),
6735
+ className: "w-full rounded border border-subtle/20 bg-background p-2 text-foreground"
6736
+ }), /* @__PURE__ */ jsx("button", {
6737
+ type: "submit",
6738
+ disabled: !prompt.trim() || inputState?.status === "pending",
6739
+ children: "Send to fork"
6740
+ })]
6741
+ }),
6742
+ isFork && ended && onContinue && /* @__PURE__ */ jsx("button", {
6743
+ type: "button",
6744
+ onClick: onContinue,
6745
+ className: "border-t border-subtle/15 p-3 text-foreground",
6746
+ children: "Continue as session"
6427
6747
  })
6428
6748
  ]
6429
6749
  });
@@ -8101,7 +8421,7 @@ function SetupPanel({ onSetUp, disabled }) {
8101
8421
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
8102
8422
  * mark (e.g. "app").
8103
8423
  */
8104
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
8424
+ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMode = "persistent", onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
8105
8425
  const [view, setView] = useState("chat");
8106
8426
  const { identity } = useProductIdentity();
8107
8427
  const appViewHost = useMemo(() => ({
@@ -8280,6 +8600,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8280
8600
  }),
8281
8601
  setupRequired ? null : /* @__PURE__ */ jsx(Composer, {
8282
8602
  ref: composerRef,
8603
+ draftMode,
8283
8604
  catalog: state.commandCatalog ?? null,
8284
8605
  status: state.sessionStatus ?? null,
8285
8606
  connected: state.status === "connected",
@@ -8353,7 +8674,20 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
8353
8674
  complete: state.executionDetailComplete,
8354
8675
  onClose: () => state.closeExecutionDetail(),
8355
8676
  onLoadMore: () => state.loadMoreExecutionDetail(),
8356
- onStop: openEntry ? () => stopExecutionEntry(openEntry) : void 0
8677
+ onStop: openEntry ? () => stopExecutionEntry(openEntry) : void 0,
8678
+ inputState: state.executionDetailInput,
8679
+ mode: state.executionDetailMode,
8680
+ unreadable: state.executionDetailUnreadable,
8681
+ onShowEarlier: state.showEarlierExecutionDetail,
8682
+ onShowLive: state.showLiveExecutionDetail,
8683
+ onSend: openEntry ? (prompt) => state.send({
8684
+ type: "send-background-task",
8685
+ taskId: openEntry.sourceId,
8686
+ input: { prompt }
8687
+ }) : void 0,
8688
+ onContinue: openEntry?.resumeSessionId ? () => {
8689
+ if (openEntry.resumeSessionId) state.switchSession(openEntry.resumeSessionId);
8690
+ } : void 0
8357
8691
  })
8358
8692
  ]
8359
8693
  })]
@@ -8538,6 +8872,6 @@ function SessionInput({ enabled, onSubmit }) {
8538
8872
  });
8539
8873
  }
8540
8874
  //#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 };
8875
+ 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
8876
 
8543
8877
  //# sourceMappingURL=index.js.map