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