@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/CHANGELOG.md +25 -0
- package/dist/index.cjs +400 -65
- package/dist/index.d.cts +28 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +401 -67
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/client/desktop-session-client.ts +78 -0
- package/src/components/Composer.tsx +58 -14
- package/src/components/ExecutionDetailSheet.tsx +116 -6
- package/src/components/SessionSurface.tsx +26 -0
- package/src/hooks/session-client-types.ts +5 -0
- package/src/hooks/use-execution-detail.ts +133 -33
- package/src/hooks/useSessionClient.ts +45 -4
- package/src/index.ts +2 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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
|
|
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
|
-
}, [
|
|
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 =
|
|
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
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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;
|