@robota-sdk/agent-ui-web 3.2.1 → 3.3.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 +29 -0
- package/dist/index.cjs +161 -118
- package/dist/index.d.cts +65 -63
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +65 -63
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +161 -118
- package/dist/index.js.map +1 -1
- package/docs/SPEC.md +6 -1
- package/package.json +6 -6
- package/src/client/ws-session-client.ts +1 -1
- package/src/clipboard.ts +2 -2
- package/src/components/AgentActivityPanel.tsx +46 -39
- package/src/components/AgentSwitcherSheet.tsx +2 -2
- package/src/components/Composer.tsx +40 -37
- package/src/components/ConversationView.tsx +14 -14
- package/src/components/Dialog.tsx +6 -6
- package/src/components/DiffLines.tsx +2 -2
- package/src/components/ExecutionDetailSheet.tsx +1 -1
- package/src/components/HelpSheet.tsx +2 -2
- package/src/components/PermissionPrompt.tsx +22 -22
- package/src/components/PopupMenu.tsx +1 -1
- package/src/components/ProjectPanel.tsx +2 -2
- package/src/components/SessionMonitor.tsx +1 -1
- package/src/components/SessionSidebar.tsx +8 -8
- package/src/components/SessionSurface.tsx +83 -56
- package/src/components/SessionSurfaceChrome.tsx +8 -8
- package/src/components/SettingsGeneralSection.tsx +1 -1
- package/src/components/SettingsMcpSection.tsx +1 -1
- package/src/components/SettingsPermissionsSection.tsx +1 -1
- package/src/components/SettingsPluginsSection.tsx +1 -1
- package/src/components/SettingsProvidersSection.tsx +3 -3
- package/src/components/SettingsScreen.tsx +3 -3
- package/src/components/StatusControls.tsx +12 -12
- package/src/components/composer-attachments.ts +5 -5
- package/src/driver-labels.ts +3 -3
- package/src/execution-entry-status.ts +1 -1
- package/src/goal-view.ts +1 -1
- package/src/hooks/command-menu.ts +2 -2
- package/src/hooks/excluded-commands.ts +2 -2
- package/src/hooks/prompt-state.ts +4 -4
- package/src/hooks/server-message-handling.ts +13 -11
- package/src/hooks/session-client-types.ts +36 -30
- package/src/hooks/ui-intent-state.ts +6 -6
- package/src/hooks/use-agent-switcher-state.ts +2 -2
- package/src/hooks/use-app-view-requests.ts +8 -1
- package/src/hooks/use-media-query.ts +1 -1
- package/src/hooks/use-model-list.ts +1 -1
- package/src/hooks/use-project-panel.ts +1 -1
- package/src/hooks/use-schedules-state.ts +4 -4
- package/src/hooks/use-session-directory.ts +11 -6
- package/src/hooks/use-settings-state.ts +4 -4
- package/src/hooks/useSessionClient.ts +81 -50
- package/src/index.ts +5 -5
- package/src/schedule-view.ts +1 -1
- package/src/styles/surface.css +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isObserverMessageType } from "@robota-sdk/agent-transport";
|
|
1
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";
|
|
@@ -100,12 +101,12 @@ function askResponse(id, response) {
|
|
|
100
101
|
//#endregion
|
|
101
102
|
//#region src/hooks/ui-intent-state.ts
|
|
102
103
|
/**
|
|
103
|
-
*
|
|
104
|
+
* How the GUI surface answers a `ui_intent`.
|
|
104
105
|
*
|
|
105
106
|
* The server requester-routes `ui_intent`, so everything that arrives here was asked for by THIS
|
|
106
107
|
* surface and must be answered visibly. An intent with a GUI screen opens it (the session picker is
|
|
107
|
-
* the session sidebar
|
|
108
|
-
*
|
|
108
|
+
* the session sidebar; settings and the plugin manager both open the Settings modal; the agent
|
|
109
|
+
* switcher is a sheet). An intent whose
|
|
109
110
|
* command is on the GUI's exclusion list never reaches here at all (`excluded-commands.ts` catches it
|
|
110
111
|
* before the session is asked) — this module only answers a screen this HOST cannot back even though
|
|
111
112
|
* the GUI knows how to render it (the session picker, when the host cannot list sessions), or a kind
|
|
@@ -135,7 +136,7 @@ function guiScreenForUiIntent(intent) {
|
|
|
135
136
|
* Describe one intent for this surface: a plain, honest sentence for a screen this HOST cannot back
|
|
136
137
|
* right now (a session picker with no session list), the `/theme` exclusion this intent defensively
|
|
137
138
|
* reuses, or an unrecognized future kind. Never called for `show-settings`, `show-plugin-manager` or
|
|
138
|
-
* `show-agent-switcher` — all three always have a screen now
|
|
139
|
+
* `show-agent-switcher` — all three always have a screen now —
|
|
139
140
|
* but their cases are left out of this switch rather than kept as dead branches that would claim
|
|
140
141
|
* otherwise; an unknown wire-level kind (a newer host) still yields an explicit notice naming the raw
|
|
141
142
|
* kind, so the "never a silent no-op" floor holds for future intents too.
|
|
@@ -239,7 +240,7 @@ function createWsSessionClient(url, callbacks) {
|
|
|
239
240
|
//#endregion
|
|
240
241
|
//#region src/hooks/server-message-handling.ts
|
|
241
242
|
/**
|
|
242
|
-
*
|
|
243
|
+
* Exhaustive ownership for every decoded server variant on the shared GUI surface.
|
|
243
244
|
* Adding a wire variant without deciding its GUI disposition is a compile error instead of a silent
|
|
244
245
|
* reducer omission. `intentionally-not-rendered` is an explicit disposition for server-side query
|
|
245
246
|
* snapshots and domain events that this session/usage surface does not currently visualize.
|
|
@@ -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
|
};
|
|
@@ -317,10 +319,10 @@ function nextRequestId$7(prefix) {
|
|
|
317
319
|
return `${prefix}_${requestCounter$7}_${Date.now()}`;
|
|
318
320
|
}
|
|
319
321
|
/**
|
|
320
|
-
*
|
|
322
|
+
* The agent switcher sheet's own correlated request/response state, the same
|
|
321
323
|
* shape `useSettingsState` already uses for `get-settings`/`update-settings` — independent from the
|
|
322
324
|
* chat conversation, because choosing an agent is answered by the sheet's own confirmation line
|
|
323
|
-
* ("Default agent: Explore"), never a conversation card
|
|
325
|
+
* ("Default agent: Explore"), never a conversation card.
|
|
324
326
|
*
|
|
325
327
|
* `openAgentSwitcher` re-fetches every time (`/agent` bare, or reopening after a close), so a change
|
|
326
328
|
* made elsewhere reaches an open-then-reopened sheet, matching `useSettingsState`'s own reasoning.
|
|
@@ -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
|
]);
|
|
@@ -676,7 +684,7 @@ function nextRequestId$4() {
|
|
|
676
684
|
return `model_list_${requestCounter$4}_${Date.now()}`;
|
|
677
685
|
}
|
|
678
686
|
/**
|
|
679
|
-
*
|
|
687
|
+
* The model list the model control's pop-up menu is built from, requested on
|
|
680
688
|
* demand (`list-models`) and correlated by `requestId` (mirrors `usePersonalUsageState`'s pattern) so
|
|
681
689
|
* a reply to an older request — one already superseded by a newer one, e.g. across a reconnect — is
|
|
682
690
|
* never applied over the list a still-open menu is currently showing.
|
|
@@ -801,7 +809,7 @@ function nextRequestId$2(prefix) {
|
|
|
801
809
|
return `${prefix}_${requestCounter$2}_${Date.now()}`;
|
|
802
810
|
}
|
|
803
811
|
/**
|
|
804
|
-
*
|
|
812
|
+
* The Project panel's correlated request/response state (status, one file's diff, project
|
|
805
813
|
* memory), independent from chat state. Mirrors `usePersonalUsageState`'s shape: a status per read, a
|
|
806
814
|
* `requestId` ref that drops a stale reply (a diff request superseded by clicking another file before
|
|
807
815
|
* the first answered), and a request function per read.
|
|
@@ -887,15 +895,15 @@ function useProjectPanelState(send) {
|
|
|
887
895
|
//#endregion
|
|
888
896
|
//#region src/hooks/use-schedules-state.ts
|
|
889
897
|
/**
|
|
890
|
-
*
|
|
898
|
+
* The Agents panel's "Scheduled" group. No `requestId` correlates
|
|
891
899
|
* `get-background-tasks`/`background_tasks` (the message carries none — see `wire-messages.ts`), so
|
|
892
900
|
* this owns that whole message type for the GUI: every reply is a full refresh of the roster,
|
|
893
901
|
* filtered server-side to `kind: 'scheduled'` (the same tasks `/schedule list` shows).
|
|
894
902
|
*
|
|
895
903
|
* Pause/resume send the SAME `command` text `/schedule pause <id>` / `/schedule resume <id>` runs.
|
|
896
904
|
* Delete sends the existing `cancel-background-task` write — the same one Stop already uses for any
|
|
897
|
-
* other background-task kind
|
|
898
|
-
* one is permanent (distinct from the reversible pause)
|
|
905
|
+
* other background-task kind — since a schedule IS a background task, and cancelling
|
|
906
|
+
* one is permanent (distinct from the reversible pause).
|
|
899
907
|
*/
|
|
900
908
|
function useSchedulesState(send) {
|
|
901
909
|
const [scheduledTasks, setScheduledTasks] = useState([]);
|
|
@@ -973,7 +981,7 @@ function keepsSessionsLive(listing) {
|
|
|
973
981
|
return listing.sessions.some((row) => typeof row.live === "boolean");
|
|
974
982
|
}
|
|
975
983
|
/**
|
|
976
|
-
*
|
|
984
|
+
* A desktop Reconnect reloads the page, which loses every bit of JS state — so the
|
|
977
985
|
* session to go back to cannot live in a ref like `lastSwitchedIdRef` below. `sessionStorage` survives
|
|
978
986
|
* the reload (and only that tab), so it carries the id across it. Namespaced so a page hosting other
|
|
979
987
|
* state under the same origin does not collide.
|
|
@@ -1010,14 +1018,14 @@ function initialSidebarOpen() {
|
|
|
1010
1018
|
return window.matchMedia("(min-width: 768px)").matches;
|
|
1011
1019
|
}
|
|
1012
1020
|
/**
|
|
1013
|
-
*
|
|
1021
|
+
* The host's session list beside the conversation: requests correlated by id (the latest
|
|
1014
1022
|
* wins), the start/switch commands, and whether the sidebar is shown.
|
|
1015
1023
|
*
|
|
1016
1024
|
* A host that keeps sessions live binds each connection to its own session and puts a new connection
|
|
1017
1025
|
* on its primary one. After a reconnect to such a host this surface goes back to the session it was
|
|
1018
1026
|
* on; an older host has one session for everyone, so there is nothing to go back to.
|
|
1019
1027
|
*/
|
|
1020
|
-
function useSessionDirectoryState(send) {
|
|
1028
|
+
function useSessionDirectoryState(send, isObserving) {
|
|
1021
1029
|
const { storage: { browserNamespace } } = useProductIdentity();
|
|
1022
1030
|
const [sessionListing, setSessionListing] = useState(null);
|
|
1023
1031
|
const [sessionsError, setSessionsError] = useState(null);
|
|
@@ -1068,6 +1076,7 @@ function useSessionDirectoryState(send) {
|
|
|
1068
1076
|
if (target === null) return;
|
|
1069
1077
|
clearStoredRestoreId(browserNamespace);
|
|
1070
1078
|
if (target === listing.currentSessionId) return;
|
|
1079
|
+
if (isObserving()) return;
|
|
1071
1080
|
if (!keepsSessionsLive(listing)) return;
|
|
1072
1081
|
if (!listing.sessions.some((row) => row.id === target)) return;
|
|
1073
1082
|
send({
|
|
@@ -1075,7 +1084,11 @@ function useSessionDirectoryState(send) {
|
|
|
1075
1084
|
sessionId: target,
|
|
1076
1085
|
requestId: nextRequestId$1("session_change")
|
|
1077
1086
|
});
|
|
1078
|
-
}, [
|
|
1087
|
+
}, [
|
|
1088
|
+
send,
|
|
1089
|
+
browserNamespace,
|
|
1090
|
+
isObserving
|
|
1091
|
+
]);
|
|
1079
1092
|
return {
|
|
1080
1093
|
sessionListing,
|
|
1081
1094
|
sessionsError,
|
|
@@ -1124,12 +1137,12 @@ function nextRequestId(prefix) {
|
|
|
1124
1137
|
return `${prefix}_${requestCounter}_${Date.now()}`;
|
|
1125
1138
|
}
|
|
1126
1139
|
/**
|
|
1127
|
-
*
|
|
1140
|
+
* The Settings screen's own correlated request/response state, independent from chat
|
|
1128
1141
|
* state — the same shape `usePersonalUsageState` already uses for `get-personal-usage-report`.
|
|
1129
1142
|
*
|
|
1130
1143
|
* `openSettings` re-fetches every time (opening from the gear, `/settings`, or reopening after a
|
|
1131
1144
|
* close): the simplest correct way for a change made elsewhere — another typed command, another
|
|
1132
|
-
* client — to reach an open-then-reopened screen
|
|
1145
|
+
* client — to reach an open-then-reopened screen.
|
|
1133
1146
|
*
|
|
1134
1147
|
* A failed UPDATE never touches `settingsSnapshot`, so a control bound to it shows the value it had
|
|
1135
1148
|
* before the write — "a failed write leaves the control at its previous value" — while
|
|
@@ -1197,7 +1210,7 @@ function useSettingsState(send) {
|
|
|
1197
1210
|
//#endregion
|
|
1198
1211
|
//#region src/hooks/useSessionClient.ts
|
|
1199
1212
|
/**
|
|
1200
|
-
*
|
|
1213
|
+
* The transport-neutral session reducer for the GUI presentation layer. Reconstructs conversation
|
|
1201
1214
|
* state from the wire `TServerMessage` stream, independent of HOW the bytes arrive (a `TMakeSessionClient`
|
|
1202
1215
|
* factory over `{onMessage, onStatusChange}`). The core is **generic over its status type** (default
|
|
1203
1216
|
* `TConnectionStatus`) so a surface with extra connection states (e.g. the browser WebRTC surface) can widen it
|
|
@@ -1237,7 +1250,7 @@ function pushToolStartSegment(segments, tool) {
|
|
|
1237
1250
|
}];
|
|
1238
1251
|
}
|
|
1239
1252
|
/**
|
|
1240
|
-
*
|
|
1253
|
+
* Attribute a `tool_end` to its call by executionId first — two same-named parallel calls
|
|
1241
1254
|
* can finish out of start order, and matching by name+running alone would close whichever running
|
|
1242
1255
|
* call of that name is found first, not the one that actually finished. Falls back to name+running
|
|
1243
1256
|
* only when the event carries no executionId (legacy fixtures / hosts that predate it).
|
|
@@ -1294,7 +1307,7 @@ function applyToolEndToSegments(segments, state) {
|
|
|
1294
1307
|
});
|
|
1295
1308
|
}
|
|
1296
1309
|
/**
|
|
1297
|
-
*
|
|
1310
|
+
* The files an Edit/Write call touched during the turn, aggregated by path — the "Changed
|
|
1298
1311
|
* files" row's data. `diffLines` keeps the LATEST edit's diff (what a click on the file opens); the
|
|
1299
1312
|
* +/- counts sum every edit to that path within the turn.
|
|
1300
1313
|
*/
|
|
@@ -1327,7 +1340,7 @@ function activeToolFromHistoricalState(state) {
|
|
|
1327
1340
|
};
|
|
1328
1341
|
}
|
|
1329
1342
|
/**
|
|
1330
|
-
*
|
|
1343
|
+
* Turns the server's chronological `IHistoryDisplaySegment[]` (a reload/reconnect/resume
|
|
1331
1344
|
* replay) into the SAME `TConversationEntry[]` shape a live turn's `finishTurn` produces — reusing its
|
|
1332
1345
|
* exact segment-accumulation and "Changed files" logic, so the two paths cannot draw the turn/grouping
|
|
1333
1346
|
* boundary differently. A `'text', role: 'user'` segment is both its own entry AND the turn boundary:
|
|
@@ -1370,6 +1383,9 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
|
|
|
1370
1383
|
function useSessionClient(makeClient) {
|
|
1371
1384
|
const [status, setStatus] = useState("disconnected");
|
|
1372
1385
|
const statusRef = useRef("disconnected");
|
|
1386
|
+
const [role, setRole] = useState(null);
|
|
1387
|
+
const roleRef = useRef(null);
|
|
1388
|
+
const isObserving = useCallback(() => roleRef.current === "observe", []);
|
|
1373
1389
|
const [messages, setMessages] = useState([]);
|
|
1374
1390
|
const [activeTools, setActiveTools] = useState([]);
|
|
1375
1391
|
const [streamingText, setStreamingText] = useState("");
|
|
@@ -1401,7 +1417,7 @@ function useSessionClient(makeClient) {
|
|
|
1401
1417
|
}, []);
|
|
1402
1418
|
/**
|
|
1403
1419
|
* End the running turn: its text and tool calls stay in the conversation as SEPARATE entries, in
|
|
1404
|
-
* the order they happened
|
|
1420
|
+
* the order they happened — not one tools-block grouped ahead of a single merged reply.
|
|
1405
1421
|
*/
|
|
1406
1422
|
const finishTurn = useCallback((toolStatus) => {
|
|
1407
1423
|
const segments = turnSegmentsRef.current;
|
|
@@ -1437,8 +1453,9 @@ function useSessionClient(makeClient) {
|
|
|
1437
1453
|
const [executionDetailInput, setExecutionDetailInput] = useState();
|
|
1438
1454
|
const executionDetailInputRef = useRef(null);
|
|
1439
1455
|
const send = useCallback((msg) => {
|
|
1456
|
+
const refused = isObserving() && !isObserverMessageType(msg.type);
|
|
1440
1457
|
if (msg.type === "send-background-task") {
|
|
1441
|
-
if (executionDetailInputRef.current !== null) return;
|
|
1458
|
+
if (refused || executionDetailInputRef.current !== null) return;
|
|
1442
1459
|
executionDetailInputRef.current = msg.taskId;
|
|
1443
1460
|
setExecutionDetailInput({
|
|
1444
1461
|
taskId: msg.taskId,
|
|
@@ -1461,21 +1478,23 @@ function useSessionClient(makeClient) {
|
|
|
1461
1478
|
});
|
|
1462
1479
|
return;
|
|
1463
1480
|
}
|
|
1481
|
+
if (refused) return;
|
|
1464
1482
|
commandsInFlightRef.current += 1;
|
|
1465
1483
|
}
|
|
1484
|
+
if (refused) return;
|
|
1466
1485
|
if (msg.type === "switch-session" || msg.type === "new-session") {
|
|
1467
1486
|
sessionChangesInFlightRef.current += 1;
|
|
1468
1487
|
if (msg.requestId !== void 0) sessionChangeRequestIdsRef.current.add(msg.requestId);
|
|
1469
1488
|
}
|
|
1470
1489
|
clientRef.current?.send(msg);
|
|
1471
|
-
}, [appendEntry]);
|
|
1490
|
+
}, [appendEntry, isObserving]);
|
|
1472
1491
|
/** One session change this surface asked for has been answered. */
|
|
1473
1492
|
const settleSessionChange = useCallback((requestId) => {
|
|
1474
1493
|
sessionChangesInFlightRef.current = Math.max(0, sessionChangesInFlightRef.current - 1);
|
|
1475
1494
|
if (requestId !== void 0) sessionChangeRequestIdsRef.current.delete(requestId);
|
|
1476
1495
|
}, []);
|
|
1477
1496
|
/**
|
|
1478
|
-
*
|
|
1497
|
+
* Run a command the same way `send` does, but flag it as controls-triggered —
|
|
1479
1498
|
* the model/mode/effort pop-up menus use this instead of `send` directly, so their change is
|
|
1480
1499
|
* confirmed by the control's own label (via the `get-status` refresh `command_result` already
|
|
1481
1500
|
* triggers) rather than a conversation card. A typed `/command` still goes through plain `send`
|
|
@@ -1484,7 +1503,7 @@ function useSessionClient(makeClient) {
|
|
|
1484
1503
|
* `TClientMessage` — giving `command` a `requestId` there would let ANY caller of `send` opt a typed
|
|
1485
1504
|
* command into silence, which is exactly what this must not allow.
|
|
1486
1505
|
*
|
|
1487
|
-
*
|
|
1506
|
+
* `send` also intercepts `help` and an excluded command before either reaches the wire —
|
|
1488
1507
|
* for `help` that opens the sheet regardless of how it was asked for, but an excluded command's
|
|
1489
1508
|
* interception adds a VISIBLE info card, defeating the silence this function promises. Every current
|
|
1490
1509
|
* caller (model/mode/effort) names neither, so this is dormant, not exercised — a future caller must
|
|
@@ -1504,8 +1523,8 @@ function useSessionClient(makeClient) {
|
|
|
1504
1523
|
const { handleExecutionDetailMessage, handleExecutionWorkspace, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
|
|
1505
1524
|
const { handleModelListMessage, ...modelListState } = useModelListState(send);
|
|
1506
1525
|
const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
|
|
1507
|
-
const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, useCallback(() => statusRef.current === "connected", []));
|
|
1508
|
-
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);
|
|
1509
1528
|
const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
|
|
1510
1529
|
const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
|
|
1511
1530
|
const settingsOpenRef = useRef(false);
|
|
@@ -1527,6 +1546,10 @@ function useSessionClient(makeClient) {
|
|
|
1527
1546
|
if (handleAgentDefinitionsMessage(msg)) return;
|
|
1528
1547
|
if (handleSchedulesMessage(msg)) return;
|
|
1529
1548
|
switch (msg.type) {
|
|
1549
|
+
case "surface_role":
|
|
1550
|
+
roleRef.current = msg.role;
|
|
1551
|
+
setRole(msg.role);
|
|
1552
|
+
break;
|
|
1530
1553
|
case "messages": {
|
|
1531
1554
|
const reconstructed = msg.display ? buildConversationEntriesFromDisplaySegments(msg.display) : msg.messages.flatMap((m) => {
|
|
1532
1555
|
if (m.role !== "user" && m.role !== "assistant") return [];
|
|
@@ -1898,6 +1921,10 @@ function useSessionClient(makeClient) {
|
|
|
1898
1921
|
const onStatusChange = (next) => {
|
|
1899
1922
|
statusRef.current = next;
|
|
1900
1923
|
setStatus(next);
|
|
1924
|
+
if (next !== "connected") {
|
|
1925
|
+
roleRef.current = null;
|
|
1926
|
+
setRole(null);
|
|
1927
|
+
}
|
|
1901
1928
|
if (next !== "connected") {
|
|
1902
1929
|
rejectPendingAppRequests();
|
|
1903
1930
|
clearCompactionNotices();
|
|
@@ -1916,6 +1943,7 @@ function useSessionClient(makeClient) {
|
|
|
1916
1943
|
sessionChangesInFlightRef.current = 0;
|
|
1917
1944
|
sessionChangeRequestIdsRef.current.clear();
|
|
1918
1945
|
pendingIntentRef.current = null;
|
|
1946
|
+
client.send({ type: "get-surface-role" });
|
|
1919
1947
|
client.send({ type: "get-commands" });
|
|
1920
1948
|
client.send({ type: "get-status" });
|
|
1921
1949
|
client.send({ type: "get-pending" });
|
|
@@ -1924,10 +1952,18 @@ function useSessionClient(makeClient) {
|
|
|
1924
1952
|
requestSchedules();
|
|
1925
1953
|
}
|
|
1926
1954
|
};
|
|
1927
|
-
const
|
|
1955
|
+
const inner = makeClient({
|
|
1928
1956
|
onMessage: handleMessage,
|
|
1929
1957
|
onStatusChange
|
|
1930
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
|
+
};
|
|
1931
1967
|
clientRef.current = client;
|
|
1932
1968
|
client.connect();
|
|
1933
1969
|
return () => {
|
|
@@ -1956,6 +1992,7 @@ function useSessionClient(makeClient) {
|
|
|
1956
1992
|
commandCatalog,
|
|
1957
1993
|
sessionStatus,
|
|
1958
1994
|
ownDriverId,
|
|
1995
|
+
role,
|
|
1959
1996
|
send,
|
|
1960
1997
|
sendCommandSilently,
|
|
1961
1998
|
pendingPrompts,
|
|
@@ -2093,7 +2130,7 @@ function resolveClientRuntimeHost(environment) {
|
|
|
2093
2130
|
//#endregion
|
|
2094
2131
|
//#region src/clipboard.ts
|
|
2095
2132
|
/**
|
|
2096
|
-
* Copies `text` to the clipboard for the "Copy code" / "Copy message" buttons
|
|
2133
|
+
* Copies `text` to the clipboard for the "Copy code" / "Copy message" buttons: the async
|
|
2097
2134
|
* Clipboard API first, and — for a page that denies it (no permission granted yet, a browser that
|
|
2098
2135
|
* never implements it, or a context that disallows it outright) — a same-tick `execCommand('copy')`
|
|
2099
2136
|
* fallback through a throwaway, invisible textarea. Rejects only once both have failed, so a caller
|
|
@@ -2140,7 +2177,7 @@ const FEEDBACK_MS = 2e3;
|
|
|
2140
2177
|
/**
|
|
2141
2178
|
* The "Copy code" / "Copy message" buttons' shared feedback state: `copied` or `error` for a couple of
|
|
2142
2179
|
* seconds, then back to `idle` — drives both the button's own label swap and an `aria-live` region a
|
|
2143
|
-
* screen reader announces without moving focus
|
|
2180
|
+
* screen reader announces without moving focus.
|
|
2144
2181
|
*/
|
|
2145
2182
|
function useCopyFeedback() {
|
|
2146
2183
|
const [status, setStatus] = useState("idle");
|
|
@@ -2160,7 +2197,7 @@ function useCopyFeedback() {
|
|
|
2160
2197
|
//#endregion
|
|
2161
2198
|
//#region src/driver-labels.ts
|
|
2162
2199
|
/**
|
|
2163
|
-
*
|
|
2200
|
+
* Plain, human labels for a driver id, and whether one names the SAME KIND
|
|
2164
2201
|
* of surface as this connection's own. Shared by every place the GUI shows who drove a turn (a
|
|
2165
2202
|
* message, a permission/ask prompt): the person's own input never carries a label, and a turn from a
|
|
2166
2203
|
* different kind of surface does — in words, never a raw id.
|
|
@@ -2214,7 +2251,7 @@ function humanDriverLabel(driverId) {
|
|
|
2214
2251
|
}
|
|
2215
2252
|
/**
|
|
2216
2253
|
* The ready-to-render attribution phrase: "from <label>" for a co-driver, or "Automatic — loop"
|
|
2217
|
-
* for the agent's own wake-ups — "from automatic" reads as a typo, not a source.
|
|
2254
|
+
* for the agent's own wake-ups — "from automatic" reads as a typo, not a source. Every
|
|
2218
2255
|
* `AGENT_DRIVER_ID` turn is, today, a loop's own injected iteration (fixed or self-paced —
|
|
2219
2256
|
* `requestWakeup` in `interactive-session.ts` is the one place that stamps this id); if a
|
|
2220
2257
|
* differently-sourced agent-wakeup turn is ever added, this label needs to name that source
|
|
@@ -2559,7 +2596,7 @@ function AppViewFrame({ tool, host, sandboxUrl, viewKey, sourceId }) {
|
|
|
2559
2596
|
//#endregion
|
|
2560
2597
|
//#region src/components/DiffLines.tsx
|
|
2561
2598
|
/**
|
|
2562
|
-
*
|
|
2599
|
+
* Renders a unified diff already built server-side (the ONE diff builder, in
|
|
2563
2600
|
* `interactive-session-streaming.ts`) — a tool row's expanded result and the Edit/Write permission
|
|
2564
2601
|
* prompt both show the SAME diff through this, so approving a change and reviewing it afterwards
|
|
2565
2602
|
* look identical. This component never computes a diff itself, only displays one.
|
|
@@ -2624,7 +2661,7 @@ function nodeText(node) {
|
|
|
2624
2661
|
return "";
|
|
2625
2662
|
}
|
|
2626
2663
|
/**
|
|
2627
|
-
* "Copy code" / "Copy message"
|
|
2664
|
+
* "Copy code" / "Copy message": shows on hover and keyboard focus, always on touch (no
|
|
2628
2665
|
* `hover` capability to gate on there). The transient "Copied" / "Couldn't copy" feedback lives in a
|
|
2629
2666
|
* visually hidden `aria-live` region that is a SIBLING of the button, not a descendant of it — ARIA
|
|
2630
2667
|
* name computation only ever looks at an element's own subtree, so a live region placed anywhere
|
|
@@ -2668,7 +2705,7 @@ function CopyButton({ label, getText, className = "" }) {
|
|
|
2668
2705
|
/**
|
|
2669
2706
|
* A fenced code block: unwrapped and horizontally scrollable (a visible scrollbar via
|
|
2670
2707
|
* `.gui-code-scroll`, `tabIndex=0` and a name so a keyboard user without a mouse wheel can still reach
|
|
2671
|
-
* the rest of a long line), with its own "Copy code" button revealed on hover/focus
|
|
2708
|
+
* the rest of a long line), with its own "Copy code" button revealed on hover/focus.
|
|
2672
2709
|
*/
|
|
2673
2710
|
function CodeBlockRenderer({ children }) {
|
|
2674
2711
|
const text = nodeText(children).replace(/\n$/, "");
|
|
@@ -2812,7 +2849,7 @@ function ToolIcon({ name, className }) {
|
|
|
2812
2849
|
return /* @__PURE__ */ jsx(Wrench, { ...props });
|
|
2813
2850
|
}
|
|
2814
2851
|
/**
|
|
2815
|
-
*
|
|
2852
|
+
* A long path's directory is shortened FROM THE LEFT so the filename — the part someone
|
|
2816
2853
|
* actually needs to recognise — is never the part that gets cut.
|
|
2817
2854
|
*/
|
|
2818
2855
|
const PATH_DISPLAY_MAX_CHARS = 52;
|
|
@@ -3008,7 +3045,7 @@ const COMMAND_TONE = {
|
|
|
3008
3045
|
}
|
|
3009
3046
|
};
|
|
3010
3047
|
/**
|
|
3011
|
-
* A slash command's outcome, where it was typed: line breaks kept, long output folded.
|
|
3048
|
+
* A slash command's outcome, where it was typed: line breaks kept, long output folded. The
|
|
3012
3049
|
* card itself still marks the command (the `/name` header stays monospace, a fixed-width label rather
|
|
3013
3050
|
* than prose), but its BODY reads as normal text — proportional, normal size — unless it is actually a
|
|
3014
3051
|
* long listing (the same measure that decides whether to fold it: many raw lines, or tall once
|
|
@@ -3115,7 +3152,7 @@ function ToolGroup({ tools }) {
|
|
|
3115
3152
|
]
|
|
3116
3153
|
});
|
|
3117
3154
|
}
|
|
3118
|
-
/**
|
|
3155
|
+
/** A turn that changed files ends with this compact row — a click opens that file's diff. */
|
|
3119
3156
|
function ChangedFilesRow({ files }) {
|
|
3120
3157
|
const [openPath, setOpenPath] = useState(null);
|
|
3121
3158
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -3170,7 +3207,7 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
|
|
|
3170
3207
|
setIsAtBottomState(value);
|
|
3171
3208
|
};
|
|
3172
3209
|
/**
|
|
3173
|
-
*
|
|
3210
|
+
* Follows new content only while the person is already at the bottom: streaming text, a
|
|
3174
3211
|
* finished message, a tool starting or finishing. Expanding something already on screen (a tool row,
|
|
3175
3212
|
* a folded command card) touches none of these, so it never causes a jump — the effect simply does
|
|
3176
3213
|
* not run for it. An instant jump rather than a smooth one, so it can never itself be mistaken by
|
|
@@ -3356,7 +3393,7 @@ const SECONDARY_BUTTON$2 = `${BUTTON$2} bg-raised text-foreground hover:bg-hover
|
|
|
3356
3393
|
const DESTRUCTIVE_BUTTON$2 = `${BUTTON$2} bg-destructive text-destructive-foreground hover:opacity-90`;
|
|
3357
3394
|
/**
|
|
3358
3395
|
* A title, a body, Cancel focused by default, and a confirm button that can be styled destructive.
|
|
3359
|
-
*
|
|
3396
|
+
* `destructive` also picks the dialog's ARIA role — `"alertdialog"` when it is (an action
|
|
3360
3397
|
* that demands a decision before anything else, e.g. deleting a session), `"dialog"` otherwise.
|
|
3361
3398
|
*/
|
|
3362
3399
|
function ConfirmDialog({ open, title, body, confirmLabel, cancelLabel = "Cancel", destructive = false, onConfirm, onCancel, restoreFocusTo }) {
|
|
@@ -3569,7 +3606,7 @@ function AgentActivityPanel({ tasks, schedules = [], goal = null, onPauseSchedul
|
|
|
3569
3606
|
task,
|
|
3570
3607
|
onPause: onPauseSchedule,
|
|
3571
3608
|
onResume: onResumeSchedule,
|
|
3572
|
-
onRequestDelete: () => setPendingDeleteId(task.id)
|
|
3609
|
+
onRequestDelete: onDeleteSchedule ? () => setPendingDeleteId(task.id) : void 0
|
|
3573
3610
|
}, task.id))
|
|
3574
3611
|
})] }),
|
|
3575
3612
|
goal && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
@@ -3652,7 +3689,7 @@ function AgentCard({ entry, selected, onOpen, onStop }) {
|
|
|
3652
3689
|
]
|
|
3653
3690
|
});
|
|
3654
3691
|
}
|
|
3655
|
-
/**
|
|
3692
|
+
/** One row of the "Scheduled" group — what it runs, when next, status, controls. */
|
|
3656
3693
|
function ScheduleCard({ task, onPause, onResume, onRequestDelete }) {
|
|
3657
3694
|
const isPaused = task.status === "paused";
|
|
3658
3695
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -3670,37 +3707,37 @@ function ScheduleCard({ task, onPause, onResume, onRequestDelete }) {
|
|
|
3670
3707
|
describeScheduleStatus(task)
|
|
3671
3708
|
]
|
|
3672
3709
|
}),
|
|
3673
|
-
/* @__PURE__ */ jsxs("div", {
|
|
3710
|
+
onResume || onPause || onRequestDelete ? /* @__PURE__ */ jsxs("div", {
|
|
3674
3711
|
className: "mt-2 flex items-center gap-3",
|
|
3675
|
-
children: [isPaused ? /* @__PURE__ */ jsxs("button", {
|
|
3712
|
+
children: [isPaused ? onResume && /* @__PURE__ */ jsxs("button", {
|
|
3676
3713
|
type: "button",
|
|
3677
|
-
onClick: () => onResume
|
|
3714
|
+
onClick: () => onResume(task.id),
|
|
3678
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",
|
|
3679
3716
|
children: [/* @__PURE__ */ jsx(Play, {
|
|
3680
3717
|
size: 11,
|
|
3681
3718
|
fill: "currentColor",
|
|
3682
3719
|
"aria-hidden": "true"
|
|
3683
3720
|
}), "Resume"]
|
|
3684
|
-
}) : /* @__PURE__ */ jsxs("button", {
|
|
3721
|
+
}) : onPause && /* @__PURE__ */ jsxs("button", {
|
|
3685
3722
|
type: "button",
|
|
3686
|
-
onClick: () => onPause
|
|
3723
|
+
onClick: () => onPause(task.id),
|
|
3687
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",
|
|
3688
3725
|
children: [/* @__PURE__ */ jsx(Pause, {
|
|
3689
3726
|
size: 11,
|
|
3690
3727
|
fill: "currentColor",
|
|
3691
3728
|
"aria-hidden": "true"
|
|
3692
3729
|
}), "Pause"]
|
|
3693
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
3730
|
+
}), onRequestDelete && /* @__PURE__ */ jsx("button", {
|
|
3694
3731
|
type: "button",
|
|
3695
3732
|
onClick: onRequestDelete,
|
|
3696
3733
|
className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-destructive hover:bg-destructive/10",
|
|
3697
3734
|
children: "Delete…"
|
|
3698
3735
|
})]
|
|
3699
|
-
})
|
|
3736
|
+
}) : null
|
|
3700
3737
|
]
|
|
3701
3738
|
});
|
|
3702
3739
|
}
|
|
3703
|
-
/**
|
|
3740
|
+
/** The current goal — objective, plain-word status, and Cancel. */
|
|
3704
3741
|
function GoalCard({ goal, onCancel }) {
|
|
3705
3742
|
return /* @__PURE__ */ jsxs("div", {
|
|
3706
3743
|
className: "rounded-xl bg-card px-3.5 py-3",
|
|
@@ -3720,7 +3757,7 @@ function GoalCard({ goal, onCancel }) {
|
|
|
3720
3757
|
className: "mt-1.5 pl-[26px] text-[13px] text-muted-foreground",
|
|
3721
3758
|
children: describeGoalStatus(goal)
|
|
3722
3759
|
}),
|
|
3723
|
-
goal.status === "active" && /* @__PURE__ */ jsx("div", {
|
|
3760
|
+
goal.status === "active" && onCancel && /* @__PURE__ */ jsx("div", {
|
|
3724
3761
|
className: "mt-2 pl-[26px]",
|
|
3725
3762
|
children: /* @__PURE__ */ jsxs("button", {
|
|
3726
3763
|
type: "button",
|
|
@@ -3787,12 +3824,12 @@ function AttentionTag({ attention, status, deniedToolCalls }) {
|
|
|
3787
3824
|
//#endregion
|
|
3788
3825
|
//#region src/components/AgentSwitcherSheet.tsx
|
|
3789
3826
|
/**
|
|
3790
|
-
*
|
|
3827
|
+
* The agent switcher: `show-agent-switcher` (`/agent` with no args) opens this
|
|
3791
3828
|
* sheet instead of the surface's old "not available" line. It lists every agent this session can
|
|
3792
3829
|
* run — name, one-line description, and where each is defined in plain words — with the current one
|
|
3793
3830
|
* checked. Choosing a row runs the SAME path as typing `/agent <name>` (a plain `command`, wired in
|
|
3794
3831
|
* `useAgentSwitcherState`); the reply is this sheet's own confirmation line, never a conversation
|
|
3795
|
-
* card
|
|
3832
|
+
* card.
|
|
3796
3833
|
*/
|
|
3797
3834
|
function AgentSwitcherSheet({ state }) {
|
|
3798
3835
|
if (!state.agentSwitcherOpen) return null;
|
|
@@ -3874,7 +3911,7 @@ function isEditableElement(element) {
|
|
|
3874
3911
|
return element !== null && element.matches("input, textarea, select, [contenteditable]");
|
|
3875
3912
|
}
|
|
3876
3913
|
/**
|
|
3877
|
-
*
|
|
3914
|
+
* The one destructive option in the provider profile action menu (the ask built
|
|
3878
3915
|
* by `askProviderProfileAction` in `agent-command`'s `provider-command-profile.ts`, `request.id`
|
|
3879
3916
|
* `'provider-profile-action'`): "Delete" answers to no digit and renders apart from Switch/Edit/Test/
|
|
3880
3917
|
* Duplicate, styled destructive — the exact audited bug ("Delete looks the same as Switch, answers to
|
|
@@ -4289,7 +4326,7 @@ const BUTTON$1 = "inline-flex items-center gap-2 rounded-lg px-3.5 py-1.5 text-[
|
|
|
4289
4326
|
const PRIMARY_BUTTON$1 = `${BUTTON$1} bg-primary text-primary-foreground hover:opacity-90`;
|
|
4290
4327
|
const SECONDARY_BUTTON$1 = `${BUTTON$1} bg-raised text-foreground hover:bg-hover`;
|
|
4291
4328
|
const GHOST_BUTTON = `${BUTTON$1} text-muted-foreground hover:bg-hover hover:text-foreground`;
|
|
4292
|
-
/**
|
|
4329
|
+
/** A destructive ask option (Delete) — visually distinct from Switch/Edit/Test/Duplicate. */
|
|
4293
4330
|
const DESTRUCTIVE_BUTTON$1 = `${BUTTON$1} bg-destructive/12 text-destructive hover:bg-destructive/20`;
|
|
4294
4331
|
/**
|
|
4295
4332
|
* The free-text answer to one ask prompt. Its value lives ONLY here, in a component the parent
|
|
@@ -4315,7 +4352,7 @@ const FreeTextField = forwardRef(function FreeTextField({ masked, placeholder, a
|
|
|
4315
4352
|
onCancel();
|
|
4316
4353
|
};
|
|
4317
4354
|
/**
|
|
4318
|
-
* A key that reaches the field types into it — the prompt's own shortcuts never see it
|
|
4355
|
+
* A key that reaches the field types into it — the prompt's own shortcuts never see it.
|
|
4319
4356
|
* The Enter that only finishes an IME composition (Korean/Japanese/Chinese) must not submit the
|
|
4320
4357
|
* still-uncommitted text — `isComposing` (or `keyCode` 229, on browsers that predate it) marks that
|
|
4321
4358
|
* Enter; the keystroke is left alone so the browser can commit the composition normally.
|
|
@@ -4400,7 +4437,7 @@ function ToolArgs({ args }) {
|
|
|
4400
4437
|
});
|
|
4401
4438
|
}
|
|
4402
4439
|
/**
|
|
4403
|
-
*
|
|
4440
|
+
* A Shell request shows just its command — and the working directory only when the server
|
|
4404
4441
|
* says it differs from the workspace — instead of every raw argument.
|
|
4405
4442
|
*/
|
|
4406
4443
|
function ShellCommandPreview({ command, cwd }) {
|
|
@@ -4435,7 +4472,7 @@ function formatUpdatedAt(updatedAt, now = Date.now()) {
|
|
|
4435
4472
|
return new Date(at).toISOString().slice(0, 10);
|
|
4436
4473
|
}
|
|
4437
4474
|
/**
|
|
4438
|
-
* A session's title
|
|
4475
|
+
* A session's title: its name, else its stable first-message title, else "New session".
|
|
4439
4476
|
* Never `preview` — that field is the raw latest reply, which is what changes every turn and is what
|
|
4440
4477
|
* this title exists to stop showing.
|
|
4441
4478
|
*/
|
|
@@ -4466,12 +4503,12 @@ function lockedSessionsLine(count) {
|
|
|
4466
4503
|
return count === 1 ? "1 saved session is sealed and not listed here." : `${count} saved sessions are sealed and not listed here.`;
|
|
4467
4504
|
}
|
|
4468
4505
|
/**
|
|
4469
|
-
*
|
|
4506
|
+
* This workspace's sessions on the left of the conversation, as in Claude Code Desktop:
|
|
4470
4507
|
* start a new one, or click another to make it current. Rows the host could not read are listed,
|
|
4471
4508
|
* disabled, so a damaged session never looks deleted. A refused switch comes back as a notice.
|
|
4472
4509
|
* A host that keeps several sessions live marks the rows running now and counts who else is on them.
|
|
4473
4510
|
*
|
|
4474
|
-
*
|
|
4511
|
+
* Each row also carries a "More" menu (or right-click) with Rename (inline) and Delete
|
|
4475
4512
|
* (confirmed). Rename on the current row reuses the `/rename` command, which also updates the live
|
|
4476
4513
|
* session's own name and title bar; rename on any other row writes the stored record directly.
|
|
4477
4514
|
*/
|
|
@@ -5355,10 +5392,10 @@ function ProductWordmark({ surface }) {
|
|
|
5355
5392
|
//#endregion
|
|
5356
5393
|
//#region src/hooks/command-menu.ts
|
|
5357
5394
|
/**
|
|
5358
|
-
*
|
|
5395
|
+
* What the composer's `/` menu offers for a draft. Pure, so the matching is testable without
|
|
5359
5396
|
* rendering: the TUI's autocomplete offers commands and skills the same way.
|
|
5360
5397
|
*
|
|
5361
|
-
*
|
|
5398
|
+
* A command the GUI cannot run at all (`isExcludedCommand`) never appears here — there is
|
|
5362
5399
|
* nothing useful for choosing it to do, and typing it by hand is answered separately (see
|
|
5363
5400
|
* `excluded-commands.ts`, `useSessionClient.ts`). Commands are listed before skills, each in its own
|
|
5364
5401
|
* group, so the menu can head them "Commands" and "Skills".
|
|
@@ -5398,7 +5435,7 @@ function byName(a, b) {
|
|
|
5398
5435
|
//#endregion
|
|
5399
5436
|
//#region src/components/composer-attachments.ts
|
|
5400
5437
|
/**
|
|
5401
|
-
*
|
|
5438
|
+
* Pure logic behind the composer's attach button and drag-and-drop, kept apart from
|
|
5402
5439
|
* `Composer.tsx` so it is testable without React or a DOM.
|
|
5403
5440
|
*
|
|
5404
5441
|
* The runtime resolves `@relative/path` references already (`prompt-file-reference-resolver.ts` in
|
|
@@ -5414,7 +5451,7 @@ function byName(a, b) {
|
|
|
5414
5451
|
const MAX_ATTACHMENT_FILE_BYTES = 64 * 1024;
|
|
5415
5452
|
/** Mirrors `DEFAULT_MAX_TOTAL_BYTES` in the same resolver. */
|
|
5416
5453
|
const MAX_ATTACHMENT_TOTAL_BYTES = 256 * 1024;
|
|
5417
|
-
/** Shown whenever a file cannot be resolved to a path inside the workspace — rule 3
|
|
5454
|
+
/** Shown whenever a file cannot be resolved to a path inside the workspace — rule 3. */
|
|
5418
5455
|
const ATTACHMENT_NO_PATH_NOTICE = "Only files inside this project folder can be attached.";
|
|
5419
5456
|
/** Shown once the running count of attachments would pass `MAX_ATTACHMENT_COUNT`. */
|
|
5420
5457
|
const ATTACHMENT_COUNT_LIMIT_NOTICE = `You can attach up to 8 files to one message.`;
|
|
@@ -5597,11 +5634,11 @@ function evaluateCandidateFile(file, workspacePath, existingTotalBytes, existing
|
|
|
5597
5634
|
};
|
|
5598
5635
|
}
|
|
5599
5636
|
/**
|
|
5600
|
-
* Inserts each chip as the runtime's existing `@relative/path` reference syntax — rule 2
|
|
5637
|
+
* Inserts each chip as the runtime's existing `@relative/path` reference syntax — rule 2.
|
|
5601
5638
|
* Always prefixed with `./`: the parser's `isPathLikeReference` only treats a token as a file
|
|
5602
5639
|
* reference when it has a `.` somewhere or an explicit relative-path prefix, so a dotless name like
|
|
5603
5640
|
* `Makefile`, `Dockerfile` or `LICENSE` — common, useful, and never refused for lacking one — would
|
|
5604
|
-
* otherwise be sent as plain text with no file and no diagnostic
|
|
5641
|
+
* otherwise be sent as plain text with no file and no diagnostic. `./` makes
|
|
5605
5642
|
* every path path-like regardless of its name, and the resolver collapses it back to the plain
|
|
5606
5643
|
* relative path before use.
|
|
5607
5644
|
*/
|
|
@@ -5613,7 +5650,7 @@ function buildPromptWithAttachments(prompt, attachments) {
|
|
|
5613
5650
|
//#endregion
|
|
5614
5651
|
//#region src/components/PopupMenu.tsx
|
|
5615
5652
|
/**
|
|
5616
|
-
* A HIG pop-up menu
|
|
5653
|
+
* A HIG pop-up menu: `role="menu"`, its choices as `menuitemradio` with `aria-checked`
|
|
5617
5654
|
* (or `menuitem` for a plain action, e.g. "Manage providers…"), grouped into optional labelled
|
|
5618
5655
|
* sections. Arrow keys move a roving focus, Enter/Space activates the focused item, and Esc — or a
|
|
5619
5656
|
* click outside — closes the menu and returns focus to the trigger that opened it, so the person
|
|
@@ -5729,7 +5766,7 @@ function PopupMenu({ label, sections, onClose, triggerRef }) {
|
|
|
5729
5766
|
//#endregion
|
|
5730
5767
|
//#region src/components/StatusControls.tsx
|
|
5731
5768
|
/**
|
|
5732
|
-
*
|
|
5769
|
+
* The plain label for each effort selection. Kept in step with
|
|
5733
5770
|
* `EFFORT_LEVEL_LABELS` in `packages/agent-framework/src/effort/effort-resolution.ts` (which
|
|
5734
5771
|
* `agent-command`'s `/effort` re-exports and reports as its own outcome text) — this package cannot
|
|
5735
5772
|
* import that one directly (it is a Node-side package this browser bundle does not depend on), so the
|
|
@@ -5746,7 +5783,7 @@ const EFFORT_LEVEL_LABELS = {
|
|
|
5746
5783
|
xhigh: "Extra high",
|
|
5747
5784
|
max: "Maximum"
|
|
5748
5785
|
};
|
|
5749
|
-
/** The primary row
|
|
5786
|
+
/** The primary row; the rest sit under "More". */
|
|
5750
5787
|
const EFFORT_PRIMARY = [
|
|
5751
5788
|
"auto",
|
|
5752
5789
|
"low",
|
|
@@ -5769,7 +5806,7 @@ function modeLabel(catalog, mode) {
|
|
|
5769
5806
|
}
|
|
5770
5807
|
const CHIP_CLASS = "flex min-w-0 items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-transparent";
|
|
5771
5808
|
const CHIP_WARNING_CLASS = "text-warning hover:text-warning";
|
|
5772
|
-
/** One status chip that opens a `PopupMenu` HIG pop-up beneath it
|
|
5809
|
+
/** One status chip that opens a `PopupMenu` HIG pop-up beneath it. */
|
|
5773
5810
|
function StatusChip({ chipLabel, value, icon, connected, warning = false, menuLabel, sections, onOpen }) {
|
|
5774
5811
|
const [open, setOpen] = useState(false);
|
|
5775
5812
|
const triggerRef = useRef(null);
|
|
@@ -5806,7 +5843,7 @@ const ICON_PROPS = {
|
|
|
5806
5843
|
"aria-hidden": true
|
|
5807
5844
|
};
|
|
5808
5845
|
/**
|
|
5809
|
-
* Model · mode · effort, each a pop-up menu
|
|
5846
|
+
* Model · mode · effort, each a pop-up menu, and the context the conversation fills.
|
|
5810
5847
|
* Every menu applies its choice through the same command wire path the equivalent typed slash
|
|
5811
5848
|
* command uses — `onSilentCommand` — so `/model <id>`, `/mode <name>` and `/effort <value>` and a
|
|
5812
5849
|
* click in these menus can never drift apart; the control's own chip label (fed by the session's next
|
|
@@ -5939,7 +5976,7 @@ const EMPTY_DRAFT = {
|
|
|
5939
5976
|
attachments: []
|
|
5940
5977
|
};
|
|
5941
5978
|
/**
|
|
5942
|
-
*
|
|
5979
|
+
* A command whose whole job is opening a GUI screen — choosing it from the `/` menu runs
|
|
5943
5980
|
* it at once (its `ui_intent` opens the screen the normal way) instead of filling the draft and
|
|
5944
5981
|
* waiting for Enter, since there is nothing useful to type after it. `help` never reaches the
|
|
5945
5982
|
* session at all (`useSessionClient.ts`'s `send` opens the Help sheet locally). Every other command
|
|
@@ -5958,14 +5995,14 @@ function menuGroupLabel(item) {
|
|
|
5958
5995
|
return item.kind === "skill" ? "Skills" : "Commands";
|
|
5959
5996
|
}
|
|
5960
5997
|
/**
|
|
5961
|
-
*
|
|
5998
|
+
* The unsent draft survives a Chat → Usage → Chat switch (the composer unmounts), a page
|
|
5962
5999
|
* reload, and a desktop relaunch — `localStorage`, not `sessionStorage`, since only `localStorage`
|
|
5963
6000
|
* survives a closed window/tab being reopened. Namespaced (the configured draft prefix, matching
|
|
5964
6001
|
* the configured restore key in `use-session-directory.ts`) so a page hosting other state under the
|
|
5965
6002
|
* same origin does not collide. Per session id when one is known; a single fallback key before the
|
|
5966
6003
|
* first status arrives (the gap is brief and is reconciled once it does — see the effect below).
|
|
5967
6004
|
*
|
|
5968
|
-
*
|
|
6005
|
+
* The stored value is now JSON (`IStoredDraft`), not the bare text string it used to be —
|
|
5969
6006
|
* `parseStoredDraft` treats anything that does not parse as that shape (including a draft saved
|
|
5970
6007
|
* before this change shipped) as plain text with no attachments, so an old stored draft still loads.
|
|
5971
6008
|
*/
|
|
@@ -6063,7 +6100,7 @@ const ComposerInstance = forwardRef(function Composer({ draftMode = "persistent"
|
|
|
6063
6100
|
useEffect(() => {
|
|
6064
6101
|
menuOptionRefs.current[selected]?.scrollIntoView?.({ block: "nearest" });
|
|
6065
6102
|
}, [selected, menu]);
|
|
6066
|
-
/**
|
|
6103
|
+
/** Every draft change is persisted at once, so a reload or relaunch loses nothing. */
|
|
6067
6104
|
const setDraft = (value) => {
|
|
6068
6105
|
stateRef.current = {
|
|
6069
6106
|
...stateRef.current,
|
|
@@ -6072,7 +6109,7 @@ const ComposerInstance = forwardRef(function Composer({ draftMode = "persistent"
|
|
|
6072
6109
|
setDraftState(value);
|
|
6073
6110
|
if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
|
|
6074
6111
|
};
|
|
6075
|
-
/**
|
|
6112
|
+
/** Attachment chips persist alongside the text, under the same per-session draft key. */
|
|
6076
6113
|
const setAttachments = (updater) => {
|
|
6077
6114
|
const next = typeof updater === "function" ? updater(stateRef.current.attachments) : updater;
|
|
6078
6115
|
stateRef.current = {
|
|
@@ -6230,7 +6267,7 @@ const ComposerInstance = forwardRef(function Composer({ draftMode = "persistent"
|
|
|
6230
6267
|
chooseMenuItem(item);
|
|
6231
6268
|
return true;
|
|
6232
6269
|
};
|
|
6233
|
-
/**
|
|
6270
|
+
/** Edit puts the queued text back in the draft and cancels the queue behind it (the wire
|
|
6234
6271
|
* has no per-message cancel — only a whole-queue clear). Offered only when exactly one prompt is
|
|
6235
6272
|
* queued: with more than one, `cancel-queue` would still drop every one of them, but only the
|
|
6236
6273
|
* shown prompt's text is known here, so "Edit" would silently lose the rest — the row offers only
|
|
@@ -6516,7 +6553,7 @@ function GoalBar({ status, onStop }) {
|
|
|
6516
6553
|
goal.maxIterations
|
|
6517
6554
|
]
|
|
6518
6555
|
}),
|
|
6519
|
-
/* @__PURE__ */ jsxs("button", {
|
|
6556
|
+
onStop && /* @__PURE__ */ jsxs("button", {
|
|
6520
6557
|
type: "button",
|
|
6521
6558
|
"aria-label": "Stop goal",
|
|
6522
6559
|
onClick: onStop,
|
|
@@ -6553,7 +6590,7 @@ const RECORD_KIND_LABEL = {
|
|
|
6553
6590
|
group_summary: "Summary"
|
|
6554
6591
|
};
|
|
6555
6592
|
/**
|
|
6556
|
-
*
|
|
6593
|
+
* Opening an Agents panel entry — status, what it was asked (one line, from its
|
|
6557
6594
|
* headline), its transcript so far (paginated via `onLoadMore`) and, once it has one, its result.
|
|
6558
6595
|
* `entry === null` renders nothing; the caller owns showing/hiding this over the panel.
|
|
6559
6596
|
*/
|
|
@@ -6773,7 +6810,7 @@ const SHORTCUTS = [
|
|
|
6773
6810
|
}
|
|
6774
6811
|
];
|
|
6775
6812
|
/**
|
|
6776
|
-
* The GUI's Help sheet
|
|
6813
|
+
* The GUI's Help sheet, a `Dialog` opened by `/help` — client-only: `useSessionClient.ts`'s
|
|
6777
6814
|
* `send` catches `/help` before it ever reaches the session and opens this instead, so there is no
|
|
6778
6815
|
* round trip and nothing terminal-style to replace it with. It lists the same commands and skills the
|
|
6779
6816
|
* `/` menu offers (`commandMenuFor`, unfiltered by any query — an excluded command has no row here
|
|
@@ -7097,7 +7134,7 @@ const VIEW_LABEL = {
|
|
|
7097
7134
|
project: "Project"
|
|
7098
7135
|
};
|
|
7099
7136
|
/**
|
|
7100
|
-
*
|
|
7137
|
+
* The folder this session works in, wherever `document.title` is settable: a page (the
|
|
7101
7138
|
* browser tab) and the desktop window that loads it (Electron follows the page's title unless told
|
|
7102
7139
|
* otherwise, and nothing in `apps/agent-app/electron/main.ts` overrides it — see its own SPEC note).
|
|
7103
7140
|
* Absent while the host has not said, so a not-yet-connected surface keeps whatever title it loaded
|
|
@@ -7172,7 +7209,7 @@ function SessionTitleBar({ status, surface, title, workspace, showBrand, view, o
|
|
|
7172
7209
|
/** The column every part of the conversation shares (matches `SessionSurface`'s `COLUMN`). */
|
|
7173
7210
|
const BANNER_COLUMN = "mx-auto w-full max-w-[760px] px-6";
|
|
7174
7211
|
/**
|
|
7175
|
-
* A connection that drops after a working session
|
|
7212
|
+
* A connection that drops after a working session: a banner directly above the
|
|
7176
7213
|
* conversation — never a full-screen replacement — so the conversation and its history stay visible
|
|
7177
7214
|
* and scrollable throughout. Shown as soon as the drop happens; says nothing on the very first
|
|
7178
7215
|
* connect, because nothing has been lost yet.
|
|
@@ -7261,7 +7298,7 @@ function displayProvider(provider) {
|
|
|
7261
7298
|
return PROVIDER_DISPLAY_NAMES[provider] ?? provider[0].toUpperCase() + provider.slice(1);
|
|
7262
7299
|
}
|
|
7263
7300
|
/**
|
|
7264
|
-
* A provider/session error classified at the wire boundary
|
|
7301
|
+
* A provider/session error classified at the wire boundary as one plain sentence naming
|
|
7265
7302
|
* what happened and, where there is one, the next step — `null` for a notice this classification does
|
|
7266
7303
|
* not cover, which keeps showing its own `message` instead.
|
|
7267
7304
|
*
|
|
@@ -7451,7 +7488,7 @@ function SettingsLanguageField({ label, current, recommended, note, onChange, di
|
|
|
7451
7488
|
//#region src/components/SettingsGeneralSection.tsx
|
|
7452
7489
|
/**
|
|
7453
7490
|
* Language, output style and preset — reusing the exact choices and descriptions the runtime
|
|
7454
|
-
* already builds for `/language`, `/output-style` and `/preset
|
|
7491
|
+
* already builds for `/language`, `/output-style` and `/preset`. Never internal ids:
|
|
7455
7492
|
* every pop-up shows the runtime's human labels.
|
|
7456
7493
|
*/
|
|
7457
7494
|
function SettingsGeneralSection({ snapshot, onUpdate, onRequestPresetChange }) {
|
|
@@ -7569,7 +7606,7 @@ function ServerRow({ server, expanded, onToggleExpanded, onToggleEnabled }) {
|
|
|
7569
7606
|
});
|
|
7570
7607
|
}
|
|
7571
7608
|
/**
|
|
7572
|
-
* The MCP Servers section
|
|
7609
|
+
* The MCP Servers section: every configured server's name, where it is
|
|
7573
7610
|
* configured, its live connection status and tool count, an enable/disable switch (approve/reject),
|
|
7574
7611
|
* an expandable list of its tool names, and a section-level Reload. Adding or editing a server is
|
|
7575
7612
|
* left out — the runtime has no command path that writes MCP config (recorded in
|
|
@@ -7664,7 +7701,7 @@ function PermissionRuleRow({ rule, onRequestRemove }) {
|
|
|
7664
7701
|
});
|
|
7665
7702
|
}
|
|
7666
7703
|
/**
|
|
7667
|
-
* Default permission mode, the rules `/permissions` shows, and the sandbox switch
|
|
7704
|
+
* Default permission mode, the rules `/permissions` shows, and the sandbox switch.
|
|
7668
7705
|
* "Skip all checks" as the mode and a preset that would set it both go through
|
|
7669
7706
|
* `onRequestModeChange`'s parent-owned confirmation before anything is sent.
|
|
7670
7707
|
*/
|
|
@@ -7775,7 +7812,7 @@ function PluginRow({ plugin, canInstall, onToggleEnabled, onRequestUninstall })
|
|
|
7775
7812
|
});
|
|
7776
7813
|
}
|
|
7777
7814
|
/**
|
|
7778
|
-
* The Plugins section
|
|
7815
|
+
* The Plugins section: every installed plugin's name, description and enabled
|
|
7779
7816
|
* state, an enable/disable switch, a section-level Reload, and — local surfaces only — install and
|
|
7780
7817
|
* uninstall, since installing runs third-party code. `canInstall` (from the snapshot) mirrors the
|
|
7781
7818
|
* refusal `/plugin install|uninstall` itself gives a remote device or an observer.
|
|
@@ -7866,7 +7903,7 @@ const BUTTON = "rounded-lg px-2.5 py-1.5 text-[12.5px] font-medium transition-co
|
|
|
7866
7903
|
const PRIMARY_BUTTON = `${BUTTON} bg-primary text-primary-foreground hover:opacity-90`;
|
|
7867
7904
|
const SECONDARY_BUTTON = `${BUTTON} bg-raised text-foreground hover:bg-hover`;
|
|
7868
7905
|
const DESTRUCTIVE_BUTTON = `${BUTTON} text-destructive hover:bg-destructive/10`;
|
|
7869
|
-
/** The "Model" action: a `PopupMenu` of this ONE profile's catalog models
|
|
7906
|
+
/** The "Model" action: a `PopupMenu` of this ONE profile's catalog models. */
|
|
7870
7907
|
function ProviderModelButton({ profile, modelList, onSelect }) {
|
|
7871
7908
|
const [open, setOpen] = useState(false);
|
|
7872
7909
|
const triggerRef = useRef(null);
|
|
@@ -7972,7 +8009,7 @@ function ProviderProfileRow({ profile, modelList, onUse, onModelChange, onComman
|
|
|
7972
8009
|
});
|
|
7973
8010
|
}
|
|
7974
8011
|
/**
|
|
7975
|
-
* The configured provider profiles
|
|
8012
|
+
* The configured provider profiles, each row a plain provider name, its model's label,
|
|
7976
8013
|
* a checkmark on the one in use, and a plain connection state when one is known. Every action goes
|
|
7977
8014
|
* through the same path the equivalent command does: Use/Model apply through `state.updateSettings`
|
|
7978
8015
|
* (the same `field` patterns Preset/Mode/Sandbox use); Edit/Test/Duplicate/Add dispatch the raw
|
|
@@ -8020,7 +8057,7 @@ function SettingsProvidersSection({ snapshot, modelList, onRequestModelList, onU
|
|
|
8020
8057
|
//#endregion
|
|
8021
8058
|
//#region src/components/SettingsScreen.tsx
|
|
8022
8059
|
/**
|
|
8023
|
-
* The Settings screen's sections
|
|
8060
|
+
* The Settings screen's sections. Adding a later section (Advisor) is one entry here plus
|
|
8024
8061
|
* one component in the switch below — nothing else in this file changes.
|
|
8025
8062
|
*/
|
|
8026
8063
|
const SECTIONS = [
|
|
@@ -8046,7 +8083,7 @@ const SECTIONS = [
|
|
|
8046
8083
|
}
|
|
8047
8084
|
];
|
|
8048
8085
|
/**
|
|
8049
|
-
* A large modal sheet with a section sidebar on the left and content on the right
|
|
8086
|
+
* A large modal sheet with a section sidebar on the left and content on the right — the
|
|
8050
8087
|
* GUI's Settings screen, opened by the sidebar's gear button, the desktop app's ⌘, menu item, or
|
|
8051
8088
|
* `/settings` (its `show-settings` `ui_intent` calls `state.openSettings()`, wired in
|
|
8052
8089
|
* `useSessionClient`). Every change applies at once; there is no Save button. Esc and the Close
|
|
@@ -8321,7 +8358,7 @@ function describePluginAction(action) {
|
|
|
8321
8358
|
//#endregion
|
|
8322
8359
|
//#region src/hooks/use-media-query.ts
|
|
8323
8360
|
/**
|
|
8324
|
-
* Tracks whether `query` currently matches, reactively (
|
|
8361
|
+
* Tracks whether `query` currently matches, reactively (the narrow-window session sheet
|
|
8325
8362
|
* below `md`). A host without `window.matchMedia` (an older embedding, or a non-browser test) reports
|
|
8326
8363
|
* `false` and never subscribes, rather than throwing.
|
|
8327
8364
|
*/
|
|
@@ -8357,7 +8394,7 @@ const COLUMN = "mx-auto w-full max-w-[760px] px-6";
|
|
|
8357
8394
|
const APP_VIEW_HOST_VERSION = "1.0.0";
|
|
8358
8395
|
/**
|
|
8359
8396
|
* Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
|
|
8360
|
-
* (never together), so it carries the same `main` landmark and label
|
|
8397
|
+
* (never together), so it carries the same `main` landmark and label — nothing
|
|
8361
8398
|
* above this slot supplies one.
|
|
8362
8399
|
*/
|
|
8363
8400
|
function EmptyState() {
|
|
@@ -8381,14 +8418,14 @@ function EmptyState() {
|
|
|
8381
8418
|
});
|
|
8382
8419
|
}
|
|
8383
8420
|
/**
|
|
8384
|
-
*
|
|
8421
|
+
* Shown in place of the conversation while `sessionStatus.setupRequired` holds: the
|
|
8385
8422
|
* session is running (the GUI itself connected fine), but has no provider to reply with yet. "Set up
|
|
8386
8423
|
* provider" runs `/provider add` as a command; its questions dock above where the composer would be
|
|
8387
8424
|
* (the same `PermissionPrompt` any ask uses), and the panel clears itself once the session reports a
|
|
8388
8425
|
* real provider, live — no restart, no reload.
|
|
8389
8426
|
*
|
|
8390
8427
|
* Renders in the same slot as `ConversationView`/`EmptyState` (never together), so it carries the
|
|
8391
|
-
* page's `main` landmark here too
|
|
8428
|
+
* page's `main` landmark here too — nothing above this slot supplies one.
|
|
8392
8429
|
*/
|
|
8393
8430
|
function SetupPanel({ onSetUp, disabled }) {
|
|
8394
8431
|
return /* @__PURE__ */ jsx("main", {
|
|
@@ -8421,7 +8458,8 @@ function SetupPanel({ onSetUp, disabled }) {
|
|
|
8421
8458
|
* The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
|
|
8422
8459
|
* mark (e.g. "app").
|
|
8423
8460
|
*/
|
|
8424
|
-
function SessionSurface({ state, surface, personalUsageEnabled = false, draftMode = "persistent", 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";
|
|
8425
8463
|
const [view, setView] = useState("chat");
|
|
8426
8464
|
const { identity } = useProductIdentity();
|
|
8427
8465
|
const appViewHost = useMemo(() => ({
|
|
@@ -8440,6 +8478,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8440
8478
|
identity.displayName,
|
|
8441
8479
|
surface
|
|
8442
8480
|
]);
|
|
8481
|
+
const personalUsageEnabled = personalUsageRequested && !readOnly;
|
|
8443
8482
|
const composerRef = useRef(null);
|
|
8444
8483
|
const tasks = state.executionWorkspace?.entries ?? [];
|
|
8445
8484
|
const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
|
|
@@ -8459,7 +8498,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8459
8498
|
const hasWorkInProgress = hasTasks || state.scheduledTasks.length > 0 || currentGoal !== null;
|
|
8460
8499
|
const isEmpty = state.messages.length === 0 && !state.streamingText && !state.isThinking && state.activeTools.length === 0;
|
|
8461
8500
|
const setupRequired = state.sessionStatus?.setupRequired === true;
|
|
8462
|
-
const hasSessionList = (state.sessionListing ?? null) !== null || state.sessionsError?.code === "list_failed";
|
|
8501
|
+
const hasSessionList = !readOnly && ((state.sessionListing ?? null) !== null || state.sessionsError?.code === "list_failed");
|
|
8463
8502
|
const sidebarOpen = hasSessionList && state.sessionSidebarOpen;
|
|
8464
8503
|
const usage = personalUsageEnabled && view === "usage";
|
|
8465
8504
|
const project = view === "project";
|
|
@@ -8529,7 +8568,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8529
8568
|
connectionLost: state.connectionLost ?? false
|
|
8530
8569
|
}), usage ? /* @__PURE__ */ jsxs("div", {
|
|
8531
8570
|
className: "min-h-0 flex-1",
|
|
8532
|
-
children: [/* @__PURE__ */ jsx(PersonalUsageDashboard, { state }), /* @__PURE__ */ jsx(PermissionPrompt, {
|
|
8571
|
+
children: [/* @__PURE__ */ jsx(PersonalUsageDashboard, { state }), readOnly ? null : /* @__PURE__ */ jsx(PermissionPrompt, {
|
|
8533
8572
|
layout: "modal",
|
|
8534
8573
|
prompts: state.pendingPrompts,
|
|
8535
8574
|
onAnswerPermission: state.answerPermission,
|
|
@@ -8541,7 +8580,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8541
8580
|
children: [/* @__PURE__ */ jsx(ProjectPanel, {
|
|
8542
8581
|
state,
|
|
8543
8582
|
onOpenMemoryInEditor
|
|
8544
|
-
}), /* @__PURE__ */ jsx(PermissionPrompt, {
|
|
8583
|
+
}), readOnly ? null : /* @__PURE__ */ jsx(PermissionPrompt, {
|
|
8545
8584
|
layout: "modal",
|
|
8546
8585
|
prompts: state.pendingPrompts,
|
|
8547
8586
|
onAnswerPermission: state.answerPermission,
|
|
@@ -8561,7 +8600,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8561
8600
|
}),
|
|
8562
8601
|
/* @__PURE__ */ jsx("div", {
|
|
8563
8602
|
className: "min-h-0 flex-1 overflow-hidden",
|
|
8564
|
-
children: setupRequired ? /* @__PURE__ */ jsx(SetupPanel, {
|
|
8603
|
+
children: setupRequired && !readOnly ? /* @__PURE__ */ jsx(SetupPanel, {
|
|
8565
8604
|
onSetUp: () => state.send({
|
|
8566
8605
|
type: "command",
|
|
8567
8606
|
name: "provider",
|
|
@@ -8569,7 +8608,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8569
8608
|
}),
|
|
8570
8609
|
disabled: state.status !== "connected"
|
|
8571
8610
|
}) : isEmpty ? /* @__PURE__ */ jsx(EmptyState, {}) : /* @__PURE__ */ jsx(AppViewHostContext.Provider, {
|
|
8572
|
-
value: appViewHost,
|
|
8611
|
+
value: readOnly ? null : appViewHost,
|
|
8573
8612
|
children: /* @__PURE__ */ jsx(ConversationView, {
|
|
8574
8613
|
messages: state.messages,
|
|
8575
8614
|
activeTools: state.activeTools,
|
|
@@ -8584,13 +8623,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8584
8623
|
children: [
|
|
8585
8624
|
/* @__PURE__ */ jsx(GoalBar, {
|
|
8586
8625
|
status: state.sessionStatus ?? null,
|
|
8587
|
-
onStop: () => state.send({
|
|
8626
|
+
onStop: readOnly ? void 0 : () => state.send({
|
|
8588
8627
|
type: "command",
|
|
8589
8628
|
name: "goal",
|
|
8590
8629
|
args: "cancel"
|
|
8591
8630
|
})
|
|
8592
8631
|
}),
|
|
8593
|
-
/* @__PURE__ */ jsx(PermissionPrompt, {
|
|
8632
|
+
readOnly ? null : /* @__PURE__ */ jsx(PermissionPrompt, {
|
|
8594
8633
|
layout: "dock",
|
|
8595
8634
|
prompts: state.pendingPrompts,
|
|
8596
8635
|
onAnswerPermission: state.answerPermission,
|
|
@@ -8598,7 +8637,11 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8598
8637
|
ownDriverId: state.ownDriverId,
|
|
8599
8638
|
onFocusReturn: () => composerRef.current?.focus()
|
|
8600
8639
|
}),
|
|
8601
|
-
|
|
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, {
|
|
8602
8645
|
ref: composerRef,
|
|
8603
8646
|
draftMode,
|
|
8604
8647
|
catalog: state.commandCatalog ?? null,
|
|
@@ -8653,13 +8696,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8653
8696
|
selectedEntryId: state.openEntryId ?? void 0,
|
|
8654
8697
|
onSelect: (entry) => state.openExecutionDetail(entry.id),
|
|
8655
8698
|
onReturnToConversation: () => state.closeExecutionDetail(),
|
|
8656
|
-
onStop: stopExecutionEntry,
|
|
8699
|
+
onStop: readOnly ? void 0 : stopExecutionEntry,
|
|
8657
8700
|
schedules: state.scheduledTasks,
|
|
8658
8701
|
goal: currentGoal,
|
|
8659
|
-
onPauseSchedule: state.pauseSchedule,
|
|
8660
|
-
onResumeSchedule: state.resumeSchedule,
|
|
8661
|
-
onDeleteSchedule: state.deleteSchedule,
|
|
8662
|
-
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({
|
|
8663
8706
|
type: "command",
|
|
8664
8707
|
name: "goal",
|
|
8665
8708
|
args: "cancel"
|
|
@@ -8674,18 +8717,18 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8674
8717
|
complete: state.executionDetailComplete,
|
|
8675
8718
|
onClose: () => state.closeExecutionDetail(),
|
|
8676
8719
|
onLoadMore: () => state.loadMoreExecutionDetail(),
|
|
8677
|
-
onStop: openEntry ? () => stopExecutionEntry(openEntry) : void 0,
|
|
8720
|
+
onStop: openEntry && !readOnly ? () => stopExecutionEntry(openEntry) : void 0,
|
|
8678
8721
|
inputState: state.executionDetailInput,
|
|
8679
8722
|
mode: state.executionDetailMode,
|
|
8680
8723
|
unreadable: state.executionDetailUnreadable,
|
|
8681
8724
|
onShowEarlier: state.showEarlierExecutionDetail,
|
|
8682
8725
|
onShowLive: state.showLiveExecutionDetail,
|
|
8683
|
-
onSend: openEntry ? (prompt) => state.send({
|
|
8726
|
+
onSend: openEntry && !readOnly ? (prompt) => state.send({
|
|
8684
8727
|
type: "send-background-task",
|
|
8685
8728
|
taskId: openEntry.sourceId,
|
|
8686
8729
|
input: { prompt }
|
|
8687
8730
|
}) : void 0,
|
|
8688
|
-
onContinue: openEntry?.resumeSessionId ? () => {
|
|
8731
|
+
onContinue: openEntry?.resumeSessionId && !readOnly ? () => {
|
|
8689
8732
|
if (openEntry.resumeSessionId) state.switchSession(openEntry.resumeSessionId);
|
|
8690
8733
|
} : void 0
|
|
8691
8734
|
})
|
|
@@ -8693,8 +8736,8 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8693
8736
|
})]
|
|
8694
8737
|
}),
|
|
8695
8738
|
/* @__PURE__ */ jsx(SessionNotices, { state }),
|
|
8696
|
-
/* @__PURE__ */ jsx(SettingsScreen, { state }),
|
|
8697
|
-
/* @__PURE__ */ jsx(AgentSwitcherSheet, { state }),
|
|
8739
|
+
readOnly ? null : /* @__PURE__ */ jsx(SettingsScreen, { state }),
|
|
8740
|
+
readOnly ? null : /* @__PURE__ */ jsx(AgentSwitcherSheet, { state }),
|
|
8698
8741
|
/* @__PURE__ */ jsx(HelpSheet, {
|
|
8699
8742
|
open: state.helpOpen,
|
|
8700
8743
|
onClose: () => state.closeHelp(),
|
|
@@ -8705,7 +8748,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8705
8748
|
}
|
|
8706
8749
|
/**
|
|
8707
8750
|
* A centered chrome frame for the pre-session (loading) and fatal states — reused by app shells. Its
|
|
8708
|
-
* content area is this page's `main` landmark
|
|
8751
|
+
* content area is this page's `main` landmark: nothing above `CenteredChrome`'s
|
|
8709
8752
|
* three call sites (starting, fatal, the trust question) supplies one, and none of the three ever
|
|
8710
8753
|
* render alongside `SessionSurface`'s own. `header` stays a sibling of `main`, not inside it, so it
|
|
8711
8754
|
* keeps its own implicit `banner` role.
|