@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.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
|
|
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
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
591
|
-
|
|
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
|
|
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
|
-
}, [
|
|
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 =
|
|
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
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|