@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.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
+
let _robota_sdk_agent_transport = require("@robota-sdk/agent-transport");
|
|
24
25
|
let react = require("react");
|
|
25
26
|
react = __toESM(react, 1);
|
|
26
27
|
let _robota_sdk_agent_transport_client = require("@robota-sdk/agent-transport/client");
|
|
@@ -126,12 +127,12 @@ function askResponse(id, response) {
|
|
|
126
127
|
//#endregion
|
|
127
128
|
//#region src/hooks/ui-intent-state.ts
|
|
128
129
|
/**
|
|
129
|
-
*
|
|
130
|
+
* How the GUI surface answers a `ui_intent`.
|
|
130
131
|
*
|
|
131
132
|
* The server requester-routes `ui_intent`, so everything that arrives here was asked for by THIS
|
|
132
133
|
* surface and must be answered visibly. An intent with a GUI screen opens it (the session picker is
|
|
133
|
-
* the session sidebar
|
|
134
|
-
*
|
|
134
|
+
* the session sidebar; settings and the plugin manager both open the Settings modal; the agent
|
|
135
|
+
* switcher is a sheet). An intent whose
|
|
135
136
|
* command is on the GUI's exclusion list never reaches here at all (`excluded-commands.ts` catches it
|
|
136
137
|
* before the session is asked) — this module only answers a screen this HOST cannot back even though
|
|
137
138
|
* the GUI knows how to render it (the session picker, when the host cannot list sessions), or a kind
|
|
@@ -161,7 +162,7 @@ function guiScreenForUiIntent(intent) {
|
|
|
161
162
|
* Describe one intent for this surface: a plain, honest sentence for a screen this HOST cannot back
|
|
162
163
|
* right now (a session picker with no session list), the `/theme` exclusion this intent defensively
|
|
163
164
|
* reuses, or an unrecognized future kind. Never called for `show-settings`, `show-plugin-manager` or
|
|
164
|
-
* `show-agent-switcher` — all three always have a screen now
|
|
165
|
+
* `show-agent-switcher` — all three always have a screen now —
|
|
165
166
|
* but their cases are left out of this switch rather than kept as dead branches that would claim
|
|
166
167
|
* otherwise; an unknown wire-level kind (a newer host) still yields an explicit notice naming the raw
|
|
167
168
|
* kind, so the "never a silent no-op" floor holds for future intents too.
|
|
@@ -265,7 +266,7 @@ function createWsSessionClient(url, callbacks) {
|
|
|
265
266
|
//#endregion
|
|
266
267
|
//#region src/hooks/server-message-handling.ts
|
|
267
268
|
/**
|
|
268
|
-
*
|
|
269
|
+
* Exhaustive ownership for every decoded server variant on the shared GUI surface.
|
|
269
270
|
* Adding a wire variant without deciding its GUI disposition is a compile error instead of a silent
|
|
270
271
|
* reducer omission. `intentionally-not-rendered` is an explicit disposition for server-side query
|
|
271
272
|
* snapshots and domain events that this session/usage surface does not currently visualize.
|
|
@@ -332,6 +333,7 @@ const SERVER_MESSAGE_HANDLING = {
|
|
|
332
333
|
background_task_control_result: "visible-notice",
|
|
333
334
|
protocol_error: "visible-notice",
|
|
334
335
|
resume_gap: "transport-control",
|
|
336
|
+
surface_role: "reducer-state",
|
|
335
337
|
settings: "reducer-state",
|
|
336
338
|
settings_error: "reducer-state"
|
|
337
339
|
};
|
|
@@ -343,10 +345,10 @@ function nextRequestId$7(prefix) {
|
|
|
343
345
|
return `${prefix}_${requestCounter$7}_${Date.now()}`;
|
|
344
346
|
}
|
|
345
347
|
/**
|
|
346
|
-
*
|
|
348
|
+
* The agent switcher sheet's own correlated request/response state, the same
|
|
347
349
|
* shape `useSettingsState` already uses for `get-settings`/`update-settings` — independent from the
|
|
348
350
|
* chat conversation, because choosing an agent is answered by the sheet's own confirmation line
|
|
349
|
-
* ("Default agent: Explore"), never a conversation card
|
|
351
|
+
* ("Default agent: Explore"), never a conversation card.
|
|
350
352
|
*
|
|
351
353
|
* `openAgentSwitcher` re-fetches every time (`/agent` bare, or reopening after a close), so a change
|
|
352
354
|
* made elsewhere reaches an open-then-reopened sheet, matching `useSettingsState`'s own reasoning.
|
|
@@ -426,6 +428,7 @@ function nextRequestId$6(prefix) {
|
|
|
426
428
|
return `${prefix}_${requestCounter$6}_${Date.now()}`;
|
|
427
429
|
}
|
|
428
430
|
const CONNECTION_CLOSED = "The connection to the session closed.";
|
|
431
|
+
const READ_ONLY = "This session is read-only.";
|
|
429
432
|
/**
|
|
430
433
|
* The key a finished call's app view is read and called by, taken from the tool row's `appView`
|
|
431
434
|
* reference; `undefined` when the row gives none. Everything outside this module treats it as an
|
|
@@ -457,7 +460,7 @@ function callAppToolMessage(requestId, viewKey, name, args) {
|
|
|
457
460
|
* keeps its own entry. A reply that lost its connection never arrives: the owner of the connection
|
|
458
461
|
* rejects every outstanding request when it drops, is replaced or goes away (`rejectPendingAppRequests`).
|
|
459
462
|
*/
|
|
460
|
-
function useAppViewRequests(send, isConnected) {
|
|
463
|
+
function useAppViewRequests(send, isConnected, isObserving) {
|
|
461
464
|
const pendingRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
462
465
|
const [requestOwner] = (0, react.useState)(() => nextRequestId$6("app_request"));
|
|
463
466
|
const rejectPendingAppRequests = (0, react.useCallback)((reason = CONNECTION_CLOSED) => {
|
|
@@ -473,6 +476,10 @@ function useAppViewRequests(send, isConnected) {
|
|
|
473
476
|
reject(/* @__PURE__ */ new Error(signal?.aborted ? "The app view closed." : CONNECTION_CLOSED));
|
|
474
477
|
return;
|
|
475
478
|
}
|
|
479
|
+
if (isObserving()) {
|
|
480
|
+
reject(/* @__PURE__ */ new Error(READ_ONLY));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
476
483
|
const requestId = nextRequestId$6(`${requestOwner}_${prefix}`);
|
|
477
484
|
const dispose = () => signal?.removeEventListener("abort", abort);
|
|
478
485
|
const abort = () => {
|
|
@@ -491,6 +498,7 @@ function useAppViewRequests(send, isConnected) {
|
|
|
491
498
|
}
|
|
492
499
|
}), [
|
|
493
500
|
isConnected,
|
|
501
|
+
isObserving,
|
|
494
502
|
requestOwner,
|
|
495
503
|
send
|
|
496
504
|
]);
|
|
@@ -702,7 +710,7 @@ function nextRequestId$4() {
|
|
|
702
710
|
return `model_list_${requestCounter$4}_${Date.now()}`;
|
|
703
711
|
}
|
|
704
712
|
/**
|
|
705
|
-
*
|
|
713
|
+
* The model list the model control's pop-up menu is built from, requested on
|
|
706
714
|
* demand (`list-models`) and correlated by `requestId` (mirrors `usePersonalUsageState`'s pattern) so
|
|
707
715
|
* a reply to an older request — one already superseded by a newer one, e.g. across a reconnect — is
|
|
708
716
|
* never applied over the list a still-open menu is currently showing.
|
|
@@ -827,7 +835,7 @@ function nextRequestId$2(prefix) {
|
|
|
827
835
|
return `${prefix}_${requestCounter$2}_${Date.now()}`;
|
|
828
836
|
}
|
|
829
837
|
/**
|
|
830
|
-
*
|
|
838
|
+
* The Project panel's correlated request/response state (status, one file's diff, project
|
|
831
839
|
* memory), independent from chat state. Mirrors `usePersonalUsageState`'s shape: a status per read, a
|
|
832
840
|
* `requestId` ref that drops a stale reply (a diff request superseded by clicking another file before
|
|
833
841
|
* the first answered), and a request function per read.
|
|
@@ -913,15 +921,15 @@ function useProjectPanelState(send) {
|
|
|
913
921
|
//#endregion
|
|
914
922
|
//#region src/hooks/use-schedules-state.ts
|
|
915
923
|
/**
|
|
916
|
-
*
|
|
924
|
+
* The Agents panel's "Scheduled" group. No `requestId` correlates
|
|
917
925
|
* `get-background-tasks`/`background_tasks` (the message carries none — see `wire-messages.ts`), so
|
|
918
926
|
* this owns that whole message type for the GUI: every reply is a full refresh of the roster,
|
|
919
927
|
* filtered server-side to `kind: 'scheduled'` (the same tasks `/schedule list` shows).
|
|
920
928
|
*
|
|
921
929
|
* Pause/resume send the SAME `command` text `/schedule pause <id>` / `/schedule resume <id>` runs.
|
|
922
930
|
* Delete sends the existing `cancel-background-task` write — the same one Stop already uses for any
|
|
923
|
-
* other background-task kind
|
|
924
|
-
* one is permanent (distinct from the reversible pause)
|
|
931
|
+
* other background-task kind — since a schedule IS a background task, and cancelling
|
|
932
|
+
* one is permanent (distinct from the reversible pause).
|
|
925
933
|
*/
|
|
926
934
|
function useSchedulesState(send) {
|
|
927
935
|
const [scheduledTasks, setScheduledTasks] = (0, react.useState)([]);
|
|
@@ -999,7 +1007,7 @@ function keepsSessionsLive(listing) {
|
|
|
999
1007
|
return listing.sessions.some((row) => typeof row.live === "boolean");
|
|
1000
1008
|
}
|
|
1001
1009
|
/**
|
|
1002
|
-
*
|
|
1010
|
+
* A desktop Reconnect reloads the page, which loses every bit of JS state — so the
|
|
1003
1011
|
* session to go back to cannot live in a ref like `lastSwitchedIdRef` below. `sessionStorage` survives
|
|
1004
1012
|
* the reload (and only that tab), so it carries the id across it. Namespaced so a page hosting other
|
|
1005
1013
|
* state under the same origin does not collide.
|
|
@@ -1036,14 +1044,14 @@ function initialSidebarOpen() {
|
|
|
1036
1044
|
return window.matchMedia("(min-width: 768px)").matches;
|
|
1037
1045
|
}
|
|
1038
1046
|
/**
|
|
1039
|
-
*
|
|
1047
|
+
* The host's session list beside the conversation: requests correlated by id (the latest
|
|
1040
1048
|
* wins), the start/switch commands, and whether the sidebar is shown.
|
|
1041
1049
|
*
|
|
1042
1050
|
* A host that keeps sessions live binds each connection to its own session and puts a new connection
|
|
1043
1051
|
* on its primary one. After a reconnect to such a host this surface goes back to the session it was
|
|
1044
1052
|
* on; an older host has one session for everyone, so there is nothing to go back to.
|
|
1045
1053
|
*/
|
|
1046
|
-
function useSessionDirectoryState(send) {
|
|
1054
|
+
function useSessionDirectoryState(send, isObserving) {
|
|
1047
1055
|
const { storage: { browserNamespace } } = useProductIdentity();
|
|
1048
1056
|
const [sessionListing, setSessionListing] = (0, react.useState)(null);
|
|
1049
1057
|
const [sessionsError, setSessionsError] = (0, react.useState)(null);
|
|
@@ -1094,6 +1102,7 @@ function useSessionDirectoryState(send) {
|
|
|
1094
1102
|
if (target === null) return;
|
|
1095
1103
|
clearStoredRestoreId(browserNamespace);
|
|
1096
1104
|
if (target === listing.currentSessionId) return;
|
|
1105
|
+
if (isObserving()) return;
|
|
1097
1106
|
if (!keepsSessionsLive(listing)) return;
|
|
1098
1107
|
if (!listing.sessions.some((row) => row.id === target)) return;
|
|
1099
1108
|
send({
|
|
@@ -1101,7 +1110,11 @@ function useSessionDirectoryState(send) {
|
|
|
1101
1110
|
sessionId: target,
|
|
1102
1111
|
requestId: nextRequestId$1("session_change")
|
|
1103
1112
|
});
|
|
1104
|
-
}, [
|
|
1113
|
+
}, [
|
|
1114
|
+
send,
|
|
1115
|
+
browserNamespace,
|
|
1116
|
+
isObserving
|
|
1117
|
+
]);
|
|
1105
1118
|
return {
|
|
1106
1119
|
sessionListing,
|
|
1107
1120
|
sessionsError,
|
|
@@ -1150,12 +1163,12 @@ function nextRequestId(prefix) {
|
|
|
1150
1163
|
return `${prefix}_${requestCounter}_${Date.now()}`;
|
|
1151
1164
|
}
|
|
1152
1165
|
/**
|
|
1153
|
-
*
|
|
1166
|
+
* The Settings screen's own correlated request/response state, independent from chat
|
|
1154
1167
|
* state — the same shape `usePersonalUsageState` already uses for `get-personal-usage-report`.
|
|
1155
1168
|
*
|
|
1156
1169
|
* `openSettings` re-fetches every time (opening from the gear, `/settings`, or reopening after a
|
|
1157
1170
|
* close): the simplest correct way for a change made elsewhere — another typed command, another
|
|
1158
|
-
* client — to reach an open-then-reopened screen
|
|
1171
|
+
* client — to reach an open-then-reopened screen.
|
|
1159
1172
|
*
|
|
1160
1173
|
* A failed UPDATE never touches `settingsSnapshot`, so a control bound to it shows the value it had
|
|
1161
1174
|
* before the write — "a failed write leaves the control at its previous value" — while
|
|
@@ -1223,7 +1236,7 @@ function useSettingsState(send) {
|
|
|
1223
1236
|
//#endregion
|
|
1224
1237
|
//#region src/hooks/useSessionClient.ts
|
|
1225
1238
|
/**
|
|
1226
|
-
*
|
|
1239
|
+
* The transport-neutral session reducer for the GUI presentation layer. Reconstructs conversation
|
|
1227
1240
|
* state from the wire `TServerMessage` stream, independent of HOW the bytes arrive (a `TMakeSessionClient`
|
|
1228
1241
|
* factory over `{onMessage, onStatusChange}`). The core is **generic over its status type** (default
|
|
1229
1242
|
* `TConnectionStatus`) so a surface with extra connection states (e.g. the browser WebRTC surface) can widen it
|
|
@@ -1263,7 +1276,7 @@ function pushToolStartSegment(segments, tool) {
|
|
|
1263
1276
|
}];
|
|
1264
1277
|
}
|
|
1265
1278
|
/**
|
|
1266
|
-
*
|
|
1279
|
+
* Attribute a `tool_end` to its call by executionId first — two same-named parallel calls
|
|
1267
1280
|
* can finish out of start order, and matching by name+running alone would close whichever running
|
|
1268
1281
|
* call of that name is found first, not the one that actually finished. Falls back to name+running
|
|
1269
1282
|
* only when the event carries no executionId (legacy fixtures / hosts that predate it).
|
|
@@ -1320,7 +1333,7 @@ function applyToolEndToSegments(segments, state) {
|
|
|
1320
1333
|
});
|
|
1321
1334
|
}
|
|
1322
1335
|
/**
|
|
1323
|
-
*
|
|
1336
|
+
* The files an Edit/Write call touched during the turn, aggregated by path — the "Changed
|
|
1324
1337
|
* files" row's data. `diffLines` keeps the LATEST edit's diff (what a click on the file opens); the
|
|
1325
1338
|
* +/- counts sum every edit to that path within the turn.
|
|
1326
1339
|
*/
|
|
@@ -1353,7 +1366,7 @@ function activeToolFromHistoricalState(state) {
|
|
|
1353
1366
|
};
|
|
1354
1367
|
}
|
|
1355
1368
|
/**
|
|
1356
|
-
*
|
|
1369
|
+
* Turns the server's chronological `IHistoryDisplaySegment[]` (a reload/reconnect/resume
|
|
1357
1370
|
* replay) into the SAME `TConversationEntry[]` shape a live turn's `finishTurn` produces — reusing its
|
|
1358
1371
|
* exact segment-accumulation and "Changed files" logic, so the two paths cannot draw the turn/grouping
|
|
1359
1372
|
* boundary differently. A `'text', role: 'user'` segment is both its own entry AND the turn boundary:
|
|
@@ -1396,6 +1409,9 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
|
|
|
1396
1409
|
function useSessionClient(makeClient) {
|
|
1397
1410
|
const [status, setStatus] = (0, react.useState)("disconnected");
|
|
1398
1411
|
const statusRef = (0, react.useRef)("disconnected");
|
|
1412
|
+
const [role, setRole] = (0, react.useState)(null);
|
|
1413
|
+
const roleRef = (0, react.useRef)(null);
|
|
1414
|
+
const isObserving = (0, react.useCallback)(() => roleRef.current === "observe", []);
|
|
1399
1415
|
const [messages, setMessages] = (0, react.useState)([]);
|
|
1400
1416
|
const [activeTools, setActiveTools] = (0, react.useState)([]);
|
|
1401
1417
|
const [streamingText, setStreamingText] = (0, react.useState)("");
|
|
@@ -1427,7 +1443,7 @@ function useSessionClient(makeClient) {
|
|
|
1427
1443
|
}, []);
|
|
1428
1444
|
/**
|
|
1429
1445
|
* End the running turn: its text and tool calls stay in the conversation as SEPARATE entries, in
|
|
1430
|
-
* the order they happened
|
|
1446
|
+
* the order they happened — not one tools-block grouped ahead of a single merged reply.
|
|
1431
1447
|
*/
|
|
1432
1448
|
const finishTurn = (0, react.useCallback)((toolStatus) => {
|
|
1433
1449
|
const segments = turnSegmentsRef.current;
|
|
@@ -1463,8 +1479,9 @@ function useSessionClient(makeClient) {
|
|
|
1463
1479
|
const [executionDetailInput, setExecutionDetailInput] = (0, react.useState)();
|
|
1464
1480
|
const executionDetailInputRef = (0, react.useRef)(null);
|
|
1465
1481
|
const send = (0, react.useCallback)((msg) => {
|
|
1482
|
+
const refused = isObserving() && !(0, _robota_sdk_agent_transport.isObserverMessageType)(msg.type);
|
|
1466
1483
|
if (msg.type === "send-background-task") {
|
|
1467
|
-
if (executionDetailInputRef.current !== null) return;
|
|
1484
|
+
if (refused || executionDetailInputRef.current !== null) return;
|
|
1468
1485
|
executionDetailInputRef.current = msg.taskId;
|
|
1469
1486
|
setExecutionDetailInput({
|
|
1470
1487
|
taskId: msg.taskId,
|
|
@@ -1487,21 +1504,23 @@ function useSessionClient(makeClient) {
|
|
|
1487
1504
|
});
|
|
1488
1505
|
return;
|
|
1489
1506
|
}
|
|
1507
|
+
if (refused) return;
|
|
1490
1508
|
commandsInFlightRef.current += 1;
|
|
1491
1509
|
}
|
|
1510
|
+
if (refused) return;
|
|
1492
1511
|
if (msg.type === "switch-session" || msg.type === "new-session") {
|
|
1493
1512
|
sessionChangesInFlightRef.current += 1;
|
|
1494
1513
|
if (msg.requestId !== void 0) sessionChangeRequestIdsRef.current.add(msg.requestId);
|
|
1495
1514
|
}
|
|
1496
1515
|
clientRef.current?.send(msg);
|
|
1497
|
-
}, [appendEntry]);
|
|
1516
|
+
}, [appendEntry, isObserving]);
|
|
1498
1517
|
/** One session change this surface asked for has been answered. */
|
|
1499
1518
|
const settleSessionChange = (0, react.useCallback)((requestId) => {
|
|
1500
1519
|
sessionChangesInFlightRef.current = Math.max(0, sessionChangesInFlightRef.current - 1);
|
|
1501
1520
|
if (requestId !== void 0) sessionChangeRequestIdsRef.current.delete(requestId);
|
|
1502
1521
|
}, []);
|
|
1503
1522
|
/**
|
|
1504
|
-
*
|
|
1523
|
+
* Run a command the same way `send` does, but flag it as controls-triggered —
|
|
1505
1524
|
* the model/mode/effort pop-up menus use this instead of `send` directly, so their change is
|
|
1506
1525
|
* confirmed by the control's own label (via the `get-status` refresh `command_result` already
|
|
1507
1526
|
* triggers) rather than a conversation card. A typed `/command` still goes through plain `send`
|
|
@@ -1510,7 +1529,7 @@ function useSessionClient(makeClient) {
|
|
|
1510
1529
|
* `TClientMessage` — giving `command` a `requestId` there would let ANY caller of `send` opt a typed
|
|
1511
1530
|
* command into silence, which is exactly what this must not allow.
|
|
1512
1531
|
*
|
|
1513
|
-
*
|
|
1532
|
+
* `send` also intercepts `help` and an excluded command before either reaches the wire —
|
|
1514
1533
|
* for `help` that opens the sheet regardless of how it was asked for, but an excluded command's
|
|
1515
1534
|
* interception adds a VISIBLE info card, defeating the silence this function promises. Every current
|
|
1516
1535
|
* caller (model/mode/effort) names neither, so this is dormant, not exercised — a future caller must
|
|
@@ -1530,8 +1549,8 @@ function useSessionClient(makeClient) {
|
|
|
1530
1549
|
const { handleExecutionDetailMessage, handleExecutionWorkspace, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
|
|
1531
1550
|
const { handleModelListMessage, ...modelListState } = useModelListState(send);
|
|
1532
1551
|
const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
|
|
1533
|
-
const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []));
|
|
1534
|
-
const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
|
|
1552
|
+
const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []), isObserving);
|
|
1553
|
+
const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send, isObserving);
|
|
1535
1554
|
const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
|
|
1536
1555
|
const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
|
|
1537
1556
|
const settingsOpenRef = (0, react.useRef)(false);
|
|
@@ -1553,6 +1572,10 @@ function useSessionClient(makeClient) {
|
|
|
1553
1572
|
if (handleAgentDefinitionsMessage(msg)) return;
|
|
1554
1573
|
if (handleSchedulesMessage(msg)) return;
|
|
1555
1574
|
switch (msg.type) {
|
|
1575
|
+
case "surface_role":
|
|
1576
|
+
roleRef.current = msg.role;
|
|
1577
|
+
setRole(msg.role);
|
|
1578
|
+
break;
|
|
1556
1579
|
case "messages": {
|
|
1557
1580
|
const reconstructed = msg.display ? buildConversationEntriesFromDisplaySegments(msg.display) : msg.messages.flatMap((m) => {
|
|
1558
1581
|
if (m.role !== "user" && m.role !== "assistant") return [];
|
|
@@ -1924,6 +1947,10 @@ function useSessionClient(makeClient) {
|
|
|
1924
1947
|
const onStatusChange = (next) => {
|
|
1925
1948
|
statusRef.current = next;
|
|
1926
1949
|
setStatus(next);
|
|
1950
|
+
if (next !== "connected") {
|
|
1951
|
+
roleRef.current = null;
|
|
1952
|
+
setRole(null);
|
|
1953
|
+
}
|
|
1927
1954
|
if (next !== "connected") {
|
|
1928
1955
|
rejectPendingAppRequests();
|
|
1929
1956
|
clearCompactionNotices();
|
|
@@ -1942,6 +1969,7 @@ function useSessionClient(makeClient) {
|
|
|
1942
1969
|
sessionChangesInFlightRef.current = 0;
|
|
1943
1970
|
sessionChangeRequestIdsRef.current.clear();
|
|
1944
1971
|
pendingIntentRef.current = null;
|
|
1972
|
+
client.send({ type: "get-surface-role" });
|
|
1945
1973
|
client.send({ type: "get-commands" });
|
|
1946
1974
|
client.send({ type: "get-status" });
|
|
1947
1975
|
client.send({ type: "get-pending" });
|
|
@@ -1950,10 +1978,18 @@ function useSessionClient(makeClient) {
|
|
|
1950
1978
|
requestSchedules();
|
|
1951
1979
|
}
|
|
1952
1980
|
};
|
|
1953
|
-
const
|
|
1981
|
+
const inner = makeClient({
|
|
1954
1982
|
onMessage: handleMessage,
|
|
1955
1983
|
onStatusChange
|
|
1956
1984
|
});
|
|
1985
|
+
const client = {
|
|
1986
|
+
connect: () => inner.connect(),
|
|
1987
|
+
disconnect: () => inner.disconnect(),
|
|
1988
|
+
send: (msg) => {
|
|
1989
|
+
if (roleRef.current === "observe" && !(0, _robota_sdk_agent_transport.isObserverMessageType)(msg.type)) return;
|
|
1990
|
+
inner.send(msg);
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1957
1993
|
clientRef.current = client;
|
|
1958
1994
|
client.connect();
|
|
1959
1995
|
return () => {
|
|
@@ -1982,6 +2018,7 @@ function useSessionClient(makeClient) {
|
|
|
1982
2018
|
commandCatalog,
|
|
1983
2019
|
sessionStatus,
|
|
1984
2020
|
ownDriverId,
|
|
2021
|
+
role,
|
|
1985
2022
|
send,
|
|
1986
2023
|
sendCommandSilently,
|
|
1987
2024
|
pendingPrompts,
|
|
@@ -2119,7 +2156,7 @@ function resolveClientRuntimeHost(environment) {
|
|
|
2119
2156
|
//#endregion
|
|
2120
2157
|
//#region src/clipboard.ts
|
|
2121
2158
|
/**
|
|
2122
|
-
* Copies `text` to the clipboard for the "Copy code" / "Copy message" buttons
|
|
2159
|
+
* Copies `text` to the clipboard for the "Copy code" / "Copy message" buttons: the async
|
|
2123
2160
|
* Clipboard API first, and — for a page that denies it (no permission granted yet, a browser that
|
|
2124
2161
|
* never implements it, or a context that disallows it outright) — a same-tick `execCommand('copy')`
|
|
2125
2162
|
* fallback through a throwaway, invisible textarea. Rejects only once both have failed, so a caller
|
|
@@ -2166,7 +2203,7 @@ const FEEDBACK_MS = 2e3;
|
|
|
2166
2203
|
/**
|
|
2167
2204
|
* The "Copy code" / "Copy message" buttons' shared feedback state: `copied` or `error` for a couple of
|
|
2168
2205
|
* seconds, then back to `idle` — drives both the button's own label swap and an `aria-live` region a
|
|
2169
|
-
* screen reader announces without moving focus
|
|
2206
|
+
* screen reader announces without moving focus.
|
|
2170
2207
|
*/
|
|
2171
2208
|
function useCopyFeedback() {
|
|
2172
2209
|
const [status, setStatus] = (0, react.useState)("idle");
|
|
@@ -2186,7 +2223,7 @@ function useCopyFeedback() {
|
|
|
2186
2223
|
//#endregion
|
|
2187
2224
|
//#region src/driver-labels.ts
|
|
2188
2225
|
/**
|
|
2189
|
-
*
|
|
2226
|
+
* Plain, human labels for a driver id, and whether one names the SAME KIND
|
|
2190
2227
|
* of surface as this connection's own. Shared by every place the GUI shows who drove a turn (a
|
|
2191
2228
|
* message, a permission/ask prompt): the person's own input never carries a label, and a turn from a
|
|
2192
2229
|
* different kind of surface does — in words, never a raw id.
|
|
@@ -2240,7 +2277,7 @@ function humanDriverLabel(driverId) {
|
|
|
2240
2277
|
}
|
|
2241
2278
|
/**
|
|
2242
2279
|
* The ready-to-render attribution phrase: "from <label>" for a co-driver, or "Automatic — loop"
|
|
2243
|
-
* for the agent's own wake-ups — "from automatic" reads as a typo, not a source.
|
|
2280
|
+
* for the agent's own wake-ups — "from automatic" reads as a typo, not a source. Every
|
|
2244
2281
|
* `AGENT_DRIVER_ID` turn is, today, a loop's own injected iteration (fixed or self-paced —
|
|
2245
2282
|
* `requestWakeup` in `interactive-session.ts` is the one place that stamps this id); if a
|
|
2246
2283
|
* differently-sourced agent-wakeup turn is ever added, this label needs to name that source
|
|
@@ -2585,7 +2622,7 @@ function AppViewFrame({ tool, host, sandboxUrl, viewKey, sourceId }) {
|
|
|
2585
2622
|
//#endregion
|
|
2586
2623
|
//#region src/components/DiffLines.tsx
|
|
2587
2624
|
/**
|
|
2588
|
-
*
|
|
2625
|
+
* Renders a unified diff already built server-side (the ONE diff builder, in
|
|
2589
2626
|
* `interactive-session-streaming.ts`) — a tool row's expanded result and the Edit/Write permission
|
|
2590
2627
|
* prompt both show the SAME diff through this, so approving a change and reviewing it afterwards
|
|
2591
2628
|
* look identical. This component never computes a diff itself, only displays one.
|
|
@@ -2650,7 +2687,7 @@ function nodeText(node) {
|
|
|
2650
2687
|
return "";
|
|
2651
2688
|
}
|
|
2652
2689
|
/**
|
|
2653
|
-
* "Copy code" / "Copy message"
|
|
2690
|
+
* "Copy code" / "Copy message": shows on hover and keyboard focus, always on touch (no
|
|
2654
2691
|
* `hover` capability to gate on there). The transient "Copied" / "Couldn't copy" feedback lives in a
|
|
2655
2692
|
* visually hidden `aria-live` region that is a SIBLING of the button, not a descendant of it — ARIA
|
|
2656
2693
|
* name computation only ever looks at an element's own subtree, so a live region placed anywhere
|
|
@@ -2694,7 +2731,7 @@ function CopyButton({ label, getText, className = "" }) {
|
|
|
2694
2731
|
/**
|
|
2695
2732
|
* A fenced code block: unwrapped and horizontally scrollable (a visible scrollbar via
|
|
2696
2733
|
* `.gui-code-scroll`, `tabIndex=0` and a name so a keyboard user without a mouse wheel can still reach
|
|
2697
|
-
* the rest of a long line), with its own "Copy code" button revealed on hover/focus
|
|
2734
|
+
* the rest of a long line), with its own "Copy code" button revealed on hover/focus.
|
|
2698
2735
|
*/
|
|
2699
2736
|
function CodeBlockRenderer({ children }) {
|
|
2700
2737
|
const text = nodeText(children).replace(/\n$/, "");
|
|
@@ -2838,7 +2875,7 @@ function ToolIcon({ name, className }) {
|
|
|
2838
2875
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Wrench, { ...props });
|
|
2839
2876
|
}
|
|
2840
2877
|
/**
|
|
2841
|
-
*
|
|
2878
|
+
* A long path's directory is shortened FROM THE LEFT so the filename — the part someone
|
|
2842
2879
|
* actually needs to recognise — is never the part that gets cut.
|
|
2843
2880
|
*/
|
|
2844
2881
|
const PATH_DISPLAY_MAX_CHARS = 52;
|
|
@@ -3034,7 +3071,7 @@ const COMMAND_TONE = {
|
|
|
3034
3071
|
}
|
|
3035
3072
|
};
|
|
3036
3073
|
/**
|
|
3037
|
-
* A slash command's outcome, where it was typed: line breaks kept, long output folded.
|
|
3074
|
+
* A slash command's outcome, where it was typed: line breaks kept, long output folded. The
|
|
3038
3075
|
* card itself still marks the command (the `/name` header stays monospace, a fixed-width label rather
|
|
3039
3076
|
* than prose), but its BODY reads as normal text — proportional, normal size — unless it is actually a
|
|
3040
3077
|
* long listing (the same measure that decides whether to fold it: many raw lines, or tall once
|
|
@@ -3141,7 +3178,7 @@ function ToolGroup({ tools }) {
|
|
|
3141
3178
|
]
|
|
3142
3179
|
});
|
|
3143
3180
|
}
|
|
3144
|
-
/**
|
|
3181
|
+
/** A turn that changed files ends with this compact row — a click opens that file's diff. */
|
|
3145
3182
|
function ChangedFilesRow({ files }) {
|
|
3146
3183
|
const [openPath, setOpenPath] = (0, react.useState)(null);
|
|
3147
3184
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -3196,7 +3233,7 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
|
|
|
3196
3233
|
setIsAtBottomState(value);
|
|
3197
3234
|
};
|
|
3198
3235
|
/**
|
|
3199
|
-
*
|
|
3236
|
+
* Follows new content only while the person is already at the bottom: streaming text, a
|
|
3200
3237
|
* finished message, a tool starting or finishing. Expanding something already on screen (a tool row,
|
|
3201
3238
|
* a folded command card) touches none of these, so it never causes a jump — the effect simply does
|
|
3202
3239
|
* not run for it. An instant jump rather than a smooth one, so it can never itself be mistaken by
|
|
@@ -3382,7 +3419,7 @@ const SECONDARY_BUTTON$2 = `${BUTTON$2} bg-raised text-foreground hover:bg-hover
|
|
|
3382
3419
|
const DESTRUCTIVE_BUTTON$2 = `${BUTTON$2} bg-destructive text-destructive-foreground hover:opacity-90`;
|
|
3383
3420
|
/**
|
|
3384
3421
|
* A title, a body, Cancel focused by default, and a confirm button that can be styled destructive.
|
|
3385
|
-
*
|
|
3422
|
+
* `destructive` also picks the dialog's ARIA role — `"alertdialog"` when it is (an action
|
|
3386
3423
|
* that demands a decision before anything else, e.g. deleting a session), `"dialog"` otherwise.
|
|
3387
3424
|
*/
|
|
3388
3425
|
function ConfirmDialog({ open, title, body, confirmLabel, cancelLabel = "Cancel", destructive = false, onConfirm, onCancel, restoreFocusTo }) {
|
|
@@ -3595,7 +3632,7 @@ function AgentActivityPanel({ tasks, schedules = [], goal = null, onPauseSchedul
|
|
|
3595
3632
|
task,
|
|
3596
3633
|
onPause: onPauseSchedule,
|
|
3597
3634
|
onResume: onResumeSchedule,
|
|
3598
|
-
onRequestDelete: () => setPendingDeleteId(task.id)
|
|
3635
|
+
onRequestDelete: onDeleteSchedule ? () => setPendingDeleteId(task.id) : void 0
|
|
3599
3636
|
}, task.id))
|
|
3600
3637
|
})] }),
|
|
3601
3638
|
goal && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -3678,7 +3715,7 @@ function AgentCard({ entry, selected, onOpen, onStop }) {
|
|
|
3678
3715
|
]
|
|
3679
3716
|
});
|
|
3680
3717
|
}
|
|
3681
|
-
/**
|
|
3718
|
+
/** One row of the "Scheduled" group — what it runs, when next, status, controls. */
|
|
3682
3719
|
function ScheduleCard({ task, onPause, onResume, onRequestDelete }) {
|
|
3683
3720
|
const isPaused = task.status === "paused";
|
|
3684
3721
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -3696,37 +3733,37 @@ function ScheduleCard({ task, onPause, onResume, onRequestDelete }) {
|
|
|
3696
3733
|
describeScheduleStatus(task)
|
|
3697
3734
|
]
|
|
3698
3735
|
}),
|
|
3699
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3736
|
+
onResume || onPause || onRequestDelete ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3700
3737
|
className: "mt-2 flex items-center gap-3",
|
|
3701
|
-
children: [isPaused ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3738
|
+
children: [isPaused ? onResume && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3702
3739
|
type: "button",
|
|
3703
|
-
onClick: () => onResume
|
|
3740
|
+
onClick: () => onResume(task.id),
|
|
3704
3741
|
className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground",
|
|
3705
3742
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Play, {
|
|
3706
3743
|
size: 11,
|
|
3707
3744
|
fill: "currentColor",
|
|
3708
3745
|
"aria-hidden": "true"
|
|
3709
3746
|
}), "Resume"]
|
|
3710
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3747
|
+
}) : onPause && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3711
3748
|
type: "button",
|
|
3712
|
-
onClick: () => onPause
|
|
3749
|
+
onClick: () => onPause(task.id),
|
|
3713
3750
|
className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-muted-foreground hover:bg-hover hover:text-foreground",
|
|
3714
3751
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pause, {
|
|
3715
3752
|
size: 11,
|
|
3716
3753
|
fill: "currentColor",
|
|
3717
3754
|
"aria-hidden": "true"
|
|
3718
3755
|
}), "Pause"]
|
|
3719
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3756
|
+
}), onRequestDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3720
3757
|
type: "button",
|
|
3721
3758
|
onClick: onRequestDelete,
|
|
3722
3759
|
className: "flex items-center gap-1.5 rounded-lg px-2 py-1 text-[13px] text-destructive hover:bg-destructive/10",
|
|
3723
3760
|
children: "Delete…"
|
|
3724
3761
|
})]
|
|
3725
|
-
})
|
|
3762
|
+
}) : null
|
|
3726
3763
|
]
|
|
3727
3764
|
});
|
|
3728
3765
|
}
|
|
3729
|
-
/**
|
|
3766
|
+
/** The current goal — objective, plain-word status, and Cancel. */
|
|
3730
3767
|
function GoalCard({ goal, onCancel }) {
|
|
3731
3768
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3732
3769
|
className: "rounded-xl bg-card px-3.5 py-3",
|
|
@@ -3746,7 +3783,7 @@ function GoalCard({ goal, onCancel }) {
|
|
|
3746
3783
|
className: "mt-1.5 pl-[26px] text-[13px] text-muted-foreground",
|
|
3747
3784
|
children: describeGoalStatus(goal)
|
|
3748
3785
|
}),
|
|
3749
|
-
goal.status === "active" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3786
|
+
goal.status === "active" && onCancel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3750
3787
|
className: "mt-2 pl-[26px]",
|
|
3751
3788
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3752
3789
|
type: "button",
|
|
@@ -3813,12 +3850,12 @@ function AttentionTag({ attention, status, deniedToolCalls }) {
|
|
|
3813
3850
|
//#endregion
|
|
3814
3851
|
//#region src/components/AgentSwitcherSheet.tsx
|
|
3815
3852
|
/**
|
|
3816
|
-
*
|
|
3853
|
+
* The agent switcher: `show-agent-switcher` (`/agent` with no args) opens this
|
|
3817
3854
|
* sheet instead of the surface's old "not available" line. It lists every agent this session can
|
|
3818
3855
|
* run — name, one-line description, and where each is defined in plain words — with the current one
|
|
3819
3856
|
* checked. Choosing a row runs the SAME path as typing `/agent <name>` (a plain `command`, wired in
|
|
3820
3857
|
* `useAgentSwitcherState`); the reply is this sheet's own confirmation line, never a conversation
|
|
3821
|
-
* card
|
|
3858
|
+
* card.
|
|
3822
3859
|
*/
|
|
3823
3860
|
function AgentSwitcherSheet({ state }) {
|
|
3824
3861
|
if (!state.agentSwitcherOpen) return null;
|
|
@@ -3900,7 +3937,7 @@ function isEditableElement(element) {
|
|
|
3900
3937
|
return element !== null && element.matches("input, textarea, select, [contenteditable]");
|
|
3901
3938
|
}
|
|
3902
3939
|
/**
|
|
3903
|
-
*
|
|
3940
|
+
* The one destructive option in the provider profile action menu (the ask built
|
|
3904
3941
|
* by `askProviderProfileAction` in `agent-command`'s `provider-command-profile.ts`, `request.id`
|
|
3905
3942
|
* `'provider-profile-action'`): "Delete" answers to no digit and renders apart from Switch/Edit/Test/
|
|
3906
3943
|
* Duplicate, styled destructive — the exact audited bug ("Delete looks the same as Switch, answers to
|
|
@@ -4315,7 +4352,7 @@ const BUTTON$1 = "inline-flex items-center gap-2 rounded-lg px-3.5 py-1.5 text-[
|
|
|
4315
4352
|
const PRIMARY_BUTTON$1 = `${BUTTON$1} bg-primary text-primary-foreground hover:opacity-90`;
|
|
4316
4353
|
const SECONDARY_BUTTON$1 = `${BUTTON$1} bg-raised text-foreground hover:bg-hover`;
|
|
4317
4354
|
const GHOST_BUTTON = `${BUTTON$1} text-muted-foreground hover:bg-hover hover:text-foreground`;
|
|
4318
|
-
/**
|
|
4355
|
+
/** A destructive ask option (Delete) — visually distinct from Switch/Edit/Test/Duplicate. */
|
|
4319
4356
|
const DESTRUCTIVE_BUTTON$1 = `${BUTTON$1} bg-destructive/12 text-destructive hover:bg-destructive/20`;
|
|
4320
4357
|
/**
|
|
4321
4358
|
* The free-text answer to one ask prompt. Its value lives ONLY here, in a component the parent
|
|
@@ -4341,7 +4378,7 @@ const FreeTextField = (0, react.forwardRef)(function FreeTextField({ masked, pla
|
|
|
4341
4378
|
onCancel();
|
|
4342
4379
|
};
|
|
4343
4380
|
/**
|
|
4344
|
-
* A key that reaches the field types into it — the prompt's own shortcuts never see it
|
|
4381
|
+
* A key that reaches the field types into it — the prompt's own shortcuts never see it.
|
|
4345
4382
|
* The Enter that only finishes an IME composition (Korean/Japanese/Chinese) must not submit the
|
|
4346
4383
|
* still-uncommitted text — `isComposing` (or `keyCode` 229, on browsers that predate it) marks that
|
|
4347
4384
|
* Enter; the keystroke is left alone so the browser can commit the composition normally.
|
|
@@ -4426,7 +4463,7 @@ function ToolArgs({ args }) {
|
|
|
4426
4463
|
});
|
|
4427
4464
|
}
|
|
4428
4465
|
/**
|
|
4429
|
-
*
|
|
4466
|
+
* A Shell request shows just its command — and the working directory only when the server
|
|
4430
4467
|
* says it differs from the workspace — instead of every raw argument.
|
|
4431
4468
|
*/
|
|
4432
4469
|
function ShellCommandPreview({ command, cwd }) {
|
|
@@ -4461,7 +4498,7 @@ function formatUpdatedAt(updatedAt, now = Date.now()) {
|
|
|
4461
4498
|
return new Date(at).toISOString().slice(0, 10);
|
|
4462
4499
|
}
|
|
4463
4500
|
/**
|
|
4464
|
-
* A session's title
|
|
4501
|
+
* A session's title: its name, else its stable first-message title, else "New session".
|
|
4465
4502
|
* Never `preview` — that field is the raw latest reply, which is what changes every turn and is what
|
|
4466
4503
|
* this title exists to stop showing.
|
|
4467
4504
|
*/
|
|
@@ -4492,12 +4529,12 @@ function lockedSessionsLine(count) {
|
|
|
4492
4529
|
return count === 1 ? "1 saved session is sealed and not listed here." : `${count} saved sessions are sealed and not listed here.`;
|
|
4493
4530
|
}
|
|
4494
4531
|
/**
|
|
4495
|
-
*
|
|
4532
|
+
* This workspace's sessions on the left of the conversation, as in Claude Code Desktop:
|
|
4496
4533
|
* start a new one, or click another to make it current. Rows the host could not read are listed,
|
|
4497
4534
|
* disabled, so a damaged session never looks deleted. A refused switch comes back as a notice.
|
|
4498
4535
|
* A host that keeps several sessions live marks the rows running now and counts who else is on them.
|
|
4499
4536
|
*
|
|
4500
|
-
*
|
|
4537
|
+
* Each row also carries a "More" menu (or right-click) with Rename (inline) and Delete
|
|
4501
4538
|
* (confirmed). Rename on the current row reuses the `/rename` command, which also updates the live
|
|
4502
4539
|
* session's own name and title bar; rename on any other row writes the stored record directly.
|
|
4503
4540
|
*/
|
|
@@ -5381,10 +5418,10 @@ function ProductWordmark({ surface }) {
|
|
|
5381
5418
|
//#endregion
|
|
5382
5419
|
//#region src/hooks/command-menu.ts
|
|
5383
5420
|
/**
|
|
5384
|
-
*
|
|
5421
|
+
* What the composer's `/` menu offers for a draft. Pure, so the matching is testable without
|
|
5385
5422
|
* rendering: the TUI's autocomplete offers commands and skills the same way.
|
|
5386
5423
|
*
|
|
5387
|
-
*
|
|
5424
|
+
* A command the GUI cannot run at all (`isExcludedCommand`) never appears here — there is
|
|
5388
5425
|
* nothing useful for choosing it to do, and typing it by hand is answered separately (see
|
|
5389
5426
|
* `excluded-commands.ts`, `useSessionClient.ts`). Commands are listed before skills, each in its own
|
|
5390
5427
|
* group, so the menu can head them "Commands" and "Skills".
|
|
@@ -5424,7 +5461,7 @@ function byName(a, b) {
|
|
|
5424
5461
|
//#endregion
|
|
5425
5462
|
//#region src/components/composer-attachments.ts
|
|
5426
5463
|
/**
|
|
5427
|
-
*
|
|
5464
|
+
* Pure logic behind the composer's attach button and drag-and-drop, kept apart from
|
|
5428
5465
|
* `Composer.tsx` so it is testable without React or a DOM.
|
|
5429
5466
|
*
|
|
5430
5467
|
* The runtime resolves `@relative/path` references already (`prompt-file-reference-resolver.ts` in
|
|
@@ -5440,7 +5477,7 @@ function byName(a, b) {
|
|
|
5440
5477
|
const MAX_ATTACHMENT_FILE_BYTES = 64 * 1024;
|
|
5441
5478
|
/** Mirrors `DEFAULT_MAX_TOTAL_BYTES` in the same resolver. */
|
|
5442
5479
|
const MAX_ATTACHMENT_TOTAL_BYTES = 256 * 1024;
|
|
5443
|
-
/** Shown whenever a file cannot be resolved to a path inside the workspace — rule 3
|
|
5480
|
+
/** Shown whenever a file cannot be resolved to a path inside the workspace — rule 3. */
|
|
5444
5481
|
const ATTACHMENT_NO_PATH_NOTICE = "Only files inside this project folder can be attached.";
|
|
5445
5482
|
/** Shown once the running count of attachments would pass `MAX_ATTACHMENT_COUNT`. */
|
|
5446
5483
|
const ATTACHMENT_COUNT_LIMIT_NOTICE = `You can attach up to 8 files to one message.`;
|
|
@@ -5623,11 +5660,11 @@ function evaluateCandidateFile(file, workspacePath, existingTotalBytes, existing
|
|
|
5623
5660
|
};
|
|
5624
5661
|
}
|
|
5625
5662
|
/**
|
|
5626
|
-
* Inserts each chip as the runtime's existing `@relative/path` reference syntax — rule 2
|
|
5663
|
+
* Inserts each chip as the runtime's existing `@relative/path` reference syntax — rule 2.
|
|
5627
5664
|
* Always prefixed with `./`: the parser's `isPathLikeReference` only treats a token as a file
|
|
5628
5665
|
* reference when it has a `.` somewhere or an explicit relative-path prefix, so a dotless name like
|
|
5629
5666
|
* `Makefile`, `Dockerfile` or `LICENSE` — common, useful, and never refused for lacking one — would
|
|
5630
|
-
* otherwise be sent as plain text with no file and no diagnostic
|
|
5667
|
+
* otherwise be sent as plain text with no file and no diagnostic. `./` makes
|
|
5631
5668
|
* every path path-like regardless of its name, and the resolver collapses it back to the plain
|
|
5632
5669
|
* relative path before use.
|
|
5633
5670
|
*/
|
|
@@ -5639,7 +5676,7 @@ function buildPromptWithAttachments(prompt, attachments) {
|
|
|
5639
5676
|
//#endregion
|
|
5640
5677
|
//#region src/components/PopupMenu.tsx
|
|
5641
5678
|
/**
|
|
5642
|
-
* A HIG pop-up menu
|
|
5679
|
+
* A HIG pop-up menu: `role="menu"`, its choices as `menuitemradio` with `aria-checked`
|
|
5643
5680
|
* (or `menuitem` for a plain action, e.g. "Manage providers…"), grouped into optional labelled
|
|
5644
5681
|
* sections. Arrow keys move a roving focus, Enter/Space activates the focused item, and Esc — or a
|
|
5645
5682
|
* click outside — closes the menu and returns focus to the trigger that opened it, so the person
|
|
@@ -5755,7 +5792,7 @@ function PopupMenu({ label, sections, onClose, triggerRef }) {
|
|
|
5755
5792
|
//#endregion
|
|
5756
5793
|
//#region src/components/StatusControls.tsx
|
|
5757
5794
|
/**
|
|
5758
|
-
*
|
|
5795
|
+
* The plain label for each effort selection. Kept in step with
|
|
5759
5796
|
* `EFFORT_LEVEL_LABELS` in `packages/agent-framework/src/effort/effort-resolution.ts` (which
|
|
5760
5797
|
* `agent-command`'s `/effort` re-exports and reports as its own outcome text) — this package cannot
|
|
5761
5798
|
* import that one directly (it is a Node-side package this browser bundle does not depend on), so the
|
|
@@ -5772,7 +5809,7 @@ const EFFORT_LEVEL_LABELS = {
|
|
|
5772
5809
|
xhigh: "Extra high",
|
|
5773
5810
|
max: "Maximum"
|
|
5774
5811
|
};
|
|
5775
|
-
/** The primary row
|
|
5812
|
+
/** The primary row; the rest sit under "More". */
|
|
5776
5813
|
const EFFORT_PRIMARY = [
|
|
5777
5814
|
"auto",
|
|
5778
5815
|
"low",
|
|
@@ -5795,7 +5832,7 @@ function modeLabel(catalog, mode) {
|
|
|
5795
5832
|
}
|
|
5796
5833
|
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";
|
|
5797
5834
|
const CHIP_WARNING_CLASS = "text-warning hover:text-warning";
|
|
5798
|
-
/** One status chip that opens a `PopupMenu` HIG pop-up beneath it
|
|
5835
|
+
/** One status chip that opens a `PopupMenu` HIG pop-up beneath it. */
|
|
5799
5836
|
function StatusChip({ chipLabel, value, icon, connected, warning = false, menuLabel, sections, onOpen }) {
|
|
5800
5837
|
const [open, setOpen] = (0, react.useState)(false);
|
|
5801
5838
|
const triggerRef = (0, react.useRef)(null);
|
|
@@ -5832,7 +5869,7 @@ const ICON_PROPS = {
|
|
|
5832
5869
|
"aria-hidden": true
|
|
5833
5870
|
};
|
|
5834
5871
|
/**
|
|
5835
|
-
* Model · mode · effort, each a pop-up menu
|
|
5872
|
+
* Model · mode · effort, each a pop-up menu, and the context the conversation fills.
|
|
5836
5873
|
* Every menu applies its choice through the same command wire path the equivalent typed slash
|
|
5837
5874
|
* command uses — `onSilentCommand` — so `/model <id>`, `/mode <name>` and `/effort <value>` and a
|
|
5838
5875
|
* click in these menus can never drift apart; the control's own chip label (fed by the session's next
|
|
@@ -5965,7 +6002,7 @@ const EMPTY_DRAFT = {
|
|
|
5965
6002
|
attachments: []
|
|
5966
6003
|
};
|
|
5967
6004
|
/**
|
|
5968
|
-
*
|
|
6005
|
+
* A command whose whole job is opening a GUI screen — choosing it from the `/` menu runs
|
|
5969
6006
|
* it at once (its `ui_intent` opens the screen the normal way) instead of filling the draft and
|
|
5970
6007
|
* waiting for Enter, since there is nothing useful to type after it. `help` never reaches the
|
|
5971
6008
|
* session at all (`useSessionClient.ts`'s `send` opens the Help sheet locally). Every other command
|
|
@@ -5984,14 +6021,14 @@ function menuGroupLabel(item) {
|
|
|
5984
6021
|
return item.kind === "skill" ? "Skills" : "Commands";
|
|
5985
6022
|
}
|
|
5986
6023
|
/**
|
|
5987
|
-
*
|
|
6024
|
+
* The unsent draft survives a Chat → Usage → Chat switch (the composer unmounts), a page
|
|
5988
6025
|
* reload, and a desktop relaunch — `localStorage`, not `sessionStorage`, since only `localStorage`
|
|
5989
6026
|
* survives a closed window/tab being reopened. Namespaced (the configured draft prefix, matching
|
|
5990
6027
|
* the configured restore key in `use-session-directory.ts`) so a page hosting other state under the
|
|
5991
6028
|
* same origin does not collide. Per session id when one is known; a single fallback key before the
|
|
5992
6029
|
* first status arrives (the gap is brief and is reconciled once it does — see the effect below).
|
|
5993
6030
|
*
|
|
5994
|
-
*
|
|
6031
|
+
* The stored value is now JSON (`IStoredDraft`), not the bare text string it used to be —
|
|
5995
6032
|
* `parseStoredDraft` treats anything that does not parse as that shape (including a draft saved
|
|
5996
6033
|
* before this change shipped) as plain text with no attachments, so an old stored draft still loads.
|
|
5997
6034
|
*/
|
|
@@ -6089,7 +6126,7 @@ const ComposerInstance = (0, react.forwardRef)(function Composer({ draftMode = "
|
|
|
6089
6126
|
(0, react.useEffect)(() => {
|
|
6090
6127
|
menuOptionRefs.current[selected]?.scrollIntoView?.({ block: "nearest" });
|
|
6091
6128
|
}, [selected, menu]);
|
|
6092
|
-
/**
|
|
6129
|
+
/** Every draft change is persisted at once, so a reload or relaunch loses nothing. */
|
|
6093
6130
|
const setDraft = (value) => {
|
|
6094
6131
|
stateRef.current = {
|
|
6095
6132
|
...stateRef.current,
|
|
@@ -6098,7 +6135,7 @@ const ComposerInstance = (0, react.forwardRef)(function Composer({ draftMode = "
|
|
|
6098
6135
|
setDraftState(value);
|
|
6099
6136
|
if (draftMode === "persistent") writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
|
|
6100
6137
|
};
|
|
6101
|
-
/**
|
|
6138
|
+
/** Attachment chips persist alongside the text, under the same per-session draft key. */
|
|
6102
6139
|
const setAttachments = (updater) => {
|
|
6103
6140
|
const next = typeof updater === "function" ? updater(stateRef.current.attachments) : updater;
|
|
6104
6141
|
stateRef.current = {
|
|
@@ -6256,7 +6293,7 @@ const ComposerInstance = (0, react.forwardRef)(function Composer({ draftMode = "
|
|
|
6256
6293
|
chooseMenuItem(item);
|
|
6257
6294
|
return true;
|
|
6258
6295
|
};
|
|
6259
|
-
/**
|
|
6296
|
+
/** Edit puts the queued text back in the draft and cancels the queue behind it (the wire
|
|
6260
6297
|
* has no per-message cancel — only a whole-queue clear). Offered only when exactly one prompt is
|
|
6261
6298
|
* queued: with more than one, `cancel-queue` would still drop every one of them, but only the
|
|
6262
6299
|
* shown prompt's text is known here, so "Edit" would silently lose the rest — the row offers only
|
|
@@ -6542,7 +6579,7 @@ function GoalBar({ status, onStop }) {
|
|
|
6542
6579
|
goal.maxIterations
|
|
6543
6580
|
]
|
|
6544
6581
|
}),
|
|
6545
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6582
|
+
onStop && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6546
6583
|
type: "button",
|
|
6547
6584
|
"aria-label": "Stop goal",
|
|
6548
6585
|
onClick: onStop,
|
|
@@ -6579,7 +6616,7 @@ const RECORD_KIND_LABEL = {
|
|
|
6579
6616
|
group_summary: "Summary"
|
|
6580
6617
|
};
|
|
6581
6618
|
/**
|
|
6582
|
-
*
|
|
6619
|
+
* Opening an Agents panel entry — status, what it was asked (one line, from its
|
|
6583
6620
|
* headline), its transcript so far (paginated via `onLoadMore`) and, once it has one, its result.
|
|
6584
6621
|
* `entry === null` renders nothing; the caller owns showing/hiding this over the panel.
|
|
6585
6622
|
*/
|
|
@@ -6799,7 +6836,7 @@ const SHORTCUTS = [
|
|
|
6799
6836
|
}
|
|
6800
6837
|
];
|
|
6801
6838
|
/**
|
|
6802
|
-
* The GUI's Help sheet
|
|
6839
|
+
* The GUI's Help sheet, a `Dialog` opened by `/help` — client-only: `useSessionClient.ts`'s
|
|
6803
6840
|
* `send` catches `/help` before it ever reaches the session and opens this instead, so there is no
|
|
6804
6841
|
* round trip and nothing terminal-style to replace it with. It lists the same commands and skills the
|
|
6805
6842
|
* `/` menu offers (`commandMenuFor`, unfiltered by any query — an excluded command has no row here
|
|
@@ -7123,7 +7160,7 @@ const VIEW_LABEL = {
|
|
|
7123
7160
|
project: "Project"
|
|
7124
7161
|
};
|
|
7125
7162
|
/**
|
|
7126
|
-
*
|
|
7163
|
+
* The folder this session works in, wherever `document.title` is settable: a page (the
|
|
7127
7164
|
* browser tab) and the desktop window that loads it (Electron follows the page's title unless told
|
|
7128
7165
|
* otherwise, and nothing in `apps/agent-app/electron/main.ts` overrides it — see its own SPEC note).
|
|
7129
7166
|
* Absent while the host has not said, so a not-yet-connected surface keeps whatever title it loaded
|
|
@@ -7198,7 +7235,7 @@ function SessionTitleBar({ status, surface, title, workspace, showBrand, view, o
|
|
|
7198
7235
|
/** The column every part of the conversation shares (matches `SessionSurface`'s `COLUMN`). */
|
|
7199
7236
|
const BANNER_COLUMN = "mx-auto w-full max-w-[760px] px-6";
|
|
7200
7237
|
/**
|
|
7201
|
-
* A connection that drops after a working session
|
|
7238
|
+
* A connection that drops after a working session: a banner directly above the
|
|
7202
7239
|
* conversation — never a full-screen replacement — so the conversation and its history stay visible
|
|
7203
7240
|
* and scrollable throughout. Shown as soon as the drop happens; says nothing on the very first
|
|
7204
7241
|
* connect, because nothing has been lost yet.
|
|
@@ -7287,7 +7324,7 @@ function displayProvider(provider) {
|
|
|
7287
7324
|
return PROVIDER_DISPLAY_NAMES[provider] ?? provider[0].toUpperCase() + provider.slice(1);
|
|
7288
7325
|
}
|
|
7289
7326
|
/**
|
|
7290
|
-
* A provider/session error classified at the wire boundary
|
|
7327
|
+
* A provider/session error classified at the wire boundary as one plain sentence naming
|
|
7291
7328
|
* what happened and, where there is one, the next step — `null` for a notice this classification does
|
|
7292
7329
|
* not cover, which keeps showing its own `message` instead.
|
|
7293
7330
|
*
|
|
@@ -7477,7 +7514,7 @@ function SettingsLanguageField({ label, current, recommended, note, onChange, di
|
|
|
7477
7514
|
//#region src/components/SettingsGeneralSection.tsx
|
|
7478
7515
|
/**
|
|
7479
7516
|
* Language, output style and preset — reusing the exact choices and descriptions the runtime
|
|
7480
|
-
* already builds for `/language`, `/output-style` and `/preset
|
|
7517
|
+
* already builds for `/language`, `/output-style` and `/preset`. Never internal ids:
|
|
7481
7518
|
* every pop-up shows the runtime's human labels.
|
|
7482
7519
|
*/
|
|
7483
7520
|
function SettingsGeneralSection({ snapshot, onUpdate, onRequestPresetChange }) {
|
|
@@ -7595,7 +7632,7 @@ function ServerRow({ server, expanded, onToggleExpanded, onToggleEnabled }) {
|
|
|
7595
7632
|
});
|
|
7596
7633
|
}
|
|
7597
7634
|
/**
|
|
7598
|
-
* The MCP Servers section
|
|
7635
|
+
* The MCP Servers section: every configured server's name, where it is
|
|
7599
7636
|
* configured, its live connection status and tool count, an enable/disable switch (approve/reject),
|
|
7600
7637
|
* an expandable list of its tool names, and a section-level Reload. Adding or editing a server is
|
|
7601
7638
|
* left out — the runtime has no command path that writes MCP config (recorded in
|
|
@@ -7690,7 +7727,7 @@ function PermissionRuleRow({ rule, onRequestRemove }) {
|
|
|
7690
7727
|
});
|
|
7691
7728
|
}
|
|
7692
7729
|
/**
|
|
7693
|
-
* Default permission mode, the rules `/permissions` shows, and the sandbox switch
|
|
7730
|
+
* Default permission mode, the rules `/permissions` shows, and the sandbox switch.
|
|
7694
7731
|
* "Skip all checks" as the mode and a preset that would set it both go through
|
|
7695
7732
|
* `onRequestModeChange`'s parent-owned confirmation before anything is sent.
|
|
7696
7733
|
*/
|
|
@@ -7801,7 +7838,7 @@ function PluginRow({ plugin, canInstall, onToggleEnabled, onRequestUninstall })
|
|
|
7801
7838
|
});
|
|
7802
7839
|
}
|
|
7803
7840
|
/**
|
|
7804
|
-
* The Plugins section
|
|
7841
|
+
* The Plugins section: every installed plugin's name, description and enabled
|
|
7805
7842
|
* state, an enable/disable switch, a section-level Reload, and — local surfaces only — install and
|
|
7806
7843
|
* uninstall, since installing runs third-party code. `canInstall` (from the snapshot) mirrors the
|
|
7807
7844
|
* refusal `/plugin install|uninstall` itself gives a remote device or an observer.
|
|
@@ -7892,7 +7929,7 @@ const BUTTON = "rounded-lg px-2.5 py-1.5 text-[12.5px] font-medium transition-co
|
|
|
7892
7929
|
const PRIMARY_BUTTON = `${BUTTON} bg-primary text-primary-foreground hover:opacity-90`;
|
|
7893
7930
|
const SECONDARY_BUTTON = `${BUTTON} bg-raised text-foreground hover:bg-hover`;
|
|
7894
7931
|
const DESTRUCTIVE_BUTTON = `${BUTTON} text-destructive hover:bg-destructive/10`;
|
|
7895
|
-
/** The "Model" action: a `PopupMenu` of this ONE profile's catalog models
|
|
7932
|
+
/** The "Model" action: a `PopupMenu` of this ONE profile's catalog models. */
|
|
7896
7933
|
function ProviderModelButton({ profile, modelList, onSelect }) {
|
|
7897
7934
|
const [open, setOpen] = (0, react.useState)(false);
|
|
7898
7935
|
const triggerRef = (0, react.useRef)(null);
|
|
@@ -7998,7 +8035,7 @@ function ProviderProfileRow({ profile, modelList, onUse, onModelChange, onComman
|
|
|
7998
8035
|
});
|
|
7999
8036
|
}
|
|
8000
8037
|
/**
|
|
8001
|
-
* The configured provider profiles
|
|
8038
|
+
* The configured provider profiles, each row a plain provider name, its model's label,
|
|
8002
8039
|
* a checkmark on the one in use, and a plain connection state when one is known. Every action goes
|
|
8003
8040
|
* through the same path the equivalent command does: Use/Model apply through `state.updateSettings`
|
|
8004
8041
|
* (the same `field` patterns Preset/Mode/Sandbox use); Edit/Test/Duplicate/Add dispatch the raw
|
|
@@ -8046,7 +8083,7 @@ function SettingsProvidersSection({ snapshot, modelList, onRequestModelList, onU
|
|
|
8046
8083
|
//#endregion
|
|
8047
8084
|
//#region src/components/SettingsScreen.tsx
|
|
8048
8085
|
/**
|
|
8049
|
-
* The Settings screen's sections
|
|
8086
|
+
* The Settings screen's sections. Adding a later section (Advisor) is one entry here plus
|
|
8050
8087
|
* one component in the switch below — nothing else in this file changes.
|
|
8051
8088
|
*/
|
|
8052
8089
|
const SECTIONS = [
|
|
@@ -8072,7 +8109,7 @@ const SECTIONS = [
|
|
|
8072
8109
|
}
|
|
8073
8110
|
];
|
|
8074
8111
|
/**
|
|
8075
|
-
* A large modal sheet with a section sidebar on the left and content on the right
|
|
8112
|
+
* A large modal sheet with a section sidebar on the left and content on the right — the
|
|
8076
8113
|
* GUI's Settings screen, opened by the sidebar's gear button, the desktop app's ⌘, menu item, or
|
|
8077
8114
|
* `/settings` (its `show-settings` `ui_intent` calls `state.openSettings()`, wired in
|
|
8078
8115
|
* `useSessionClient`). Every change applies at once; there is no Save button. Esc and the Close
|
|
@@ -8347,7 +8384,7 @@ function describePluginAction(action) {
|
|
|
8347
8384
|
//#endregion
|
|
8348
8385
|
//#region src/hooks/use-media-query.ts
|
|
8349
8386
|
/**
|
|
8350
|
-
* Tracks whether `query` currently matches, reactively (
|
|
8387
|
+
* Tracks whether `query` currently matches, reactively (the narrow-window session sheet
|
|
8351
8388
|
* below `md`). A host without `window.matchMedia` (an older embedding, or a non-browser test) reports
|
|
8352
8389
|
* `false` and never subscribes, rather than throwing.
|
|
8353
8390
|
*/
|
|
@@ -8383,7 +8420,7 @@ const COLUMN = "mx-auto w-full max-w-[760px] px-6";
|
|
|
8383
8420
|
const APP_VIEW_HOST_VERSION = "1.0.0";
|
|
8384
8421
|
/**
|
|
8385
8422
|
* Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
|
|
8386
|
-
* (never together), so it carries the same `main` landmark and label
|
|
8423
|
+
* (never together), so it carries the same `main` landmark and label — nothing
|
|
8387
8424
|
* above this slot supplies one.
|
|
8388
8425
|
*/
|
|
8389
8426
|
function EmptyState() {
|
|
@@ -8407,14 +8444,14 @@ function EmptyState() {
|
|
|
8407
8444
|
});
|
|
8408
8445
|
}
|
|
8409
8446
|
/**
|
|
8410
|
-
*
|
|
8447
|
+
* Shown in place of the conversation while `sessionStatus.setupRequired` holds: the
|
|
8411
8448
|
* session is running (the GUI itself connected fine), but has no provider to reply with yet. "Set up
|
|
8412
8449
|
* provider" runs `/provider add` as a command; its questions dock above where the composer would be
|
|
8413
8450
|
* (the same `PermissionPrompt` any ask uses), and the panel clears itself once the session reports a
|
|
8414
8451
|
* real provider, live — no restart, no reload.
|
|
8415
8452
|
*
|
|
8416
8453
|
* Renders in the same slot as `ConversationView`/`EmptyState` (never together), so it carries the
|
|
8417
|
-
* page's `main` landmark here too
|
|
8454
|
+
* page's `main` landmark here too — nothing above this slot supplies one.
|
|
8418
8455
|
*/
|
|
8419
8456
|
function SetupPanel({ onSetUp, disabled }) {
|
|
8420
8457
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("main", {
|
|
@@ -8447,7 +8484,8 @@ function SetupPanel({ onSetUp, disabled }) {
|
|
|
8447
8484
|
* The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
|
|
8448
8485
|
* mark (e.g. "app").
|
|
8449
8486
|
*/
|
|
8450
|
-
function SessionSurface({ state, surface, personalUsageEnabled = false, draftMode = "persistent", onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
|
|
8487
|
+
function SessionSurface({ state, surface, personalUsageEnabled: personalUsageRequested = false, draftMode = "persistent", onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
|
|
8488
|
+
const readOnly = state.role === "observe";
|
|
8451
8489
|
const [view, setView] = (0, react.useState)("chat");
|
|
8452
8490
|
const { identity } = useProductIdentity();
|
|
8453
8491
|
const appViewHost = (0, react.useMemo)(() => ({
|
|
@@ -8466,6 +8504,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8466
8504
|
identity.displayName,
|
|
8467
8505
|
surface
|
|
8468
8506
|
]);
|
|
8507
|
+
const personalUsageEnabled = personalUsageRequested && !readOnly;
|
|
8469
8508
|
const composerRef = (0, react.useRef)(null);
|
|
8470
8509
|
const tasks = state.executionWorkspace?.entries ?? [];
|
|
8471
8510
|
const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
|
|
@@ -8485,7 +8524,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8485
8524
|
const hasWorkInProgress = hasTasks || state.scheduledTasks.length > 0 || currentGoal !== null;
|
|
8486
8525
|
const isEmpty = state.messages.length === 0 && !state.streamingText && !state.isThinking && state.activeTools.length === 0;
|
|
8487
8526
|
const setupRequired = state.sessionStatus?.setupRequired === true;
|
|
8488
|
-
const hasSessionList = (state.sessionListing ?? null) !== null || state.sessionsError?.code === "list_failed";
|
|
8527
|
+
const hasSessionList = !readOnly && ((state.sessionListing ?? null) !== null || state.sessionsError?.code === "list_failed");
|
|
8489
8528
|
const sidebarOpen = hasSessionList && state.sessionSidebarOpen;
|
|
8490
8529
|
const usage = personalUsageEnabled && view === "usage";
|
|
8491
8530
|
const project = view === "project";
|
|
@@ -8555,7 +8594,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8555
8594
|
connectionLost: state.connectionLost ?? false
|
|
8556
8595
|
}), usage ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8557
8596
|
className: "min-h-0 flex-1",
|
|
8558
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonalUsageDashboard, { state }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
|
|
8597
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonalUsageDashboard, { state }), readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
|
|
8559
8598
|
layout: "modal",
|
|
8560
8599
|
prompts: state.pendingPrompts,
|
|
8561
8600
|
onAnswerPermission: state.answerPermission,
|
|
@@ -8567,7 +8606,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8567
8606
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectPanel, {
|
|
8568
8607
|
state,
|
|
8569
8608
|
onOpenMemoryInEditor
|
|
8570
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
|
|
8609
|
+
}), readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
|
|
8571
8610
|
layout: "modal",
|
|
8572
8611
|
prompts: state.pendingPrompts,
|
|
8573
8612
|
onAnswerPermission: state.answerPermission,
|
|
@@ -8587,7 +8626,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8587
8626
|
}),
|
|
8588
8627
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8589
8628
|
className: "min-h-0 flex-1 overflow-hidden",
|
|
8590
|
-
children: setupRequired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupPanel, {
|
|
8629
|
+
children: setupRequired && !readOnly ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupPanel, {
|
|
8591
8630
|
onSetUp: () => state.send({
|
|
8592
8631
|
type: "command",
|
|
8593
8632
|
name: "provider",
|
|
@@ -8595,7 +8634,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8595
8634
|
}),
|
|
8596
8635
|
disabled: state.status !== "connected"
|
|
8597
8636
|
}) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppViewHostContext.Provider, {
|
|
8598
|
-
value: appViewHost,
|
|
8637
|
+
value: readOnly ? null : appViewHost,
|
|
8599
8638
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationView, {
|
|
8600
8639
|
messages: state.messages,
|
|
8601
8640
|
activeTools: state.activeTools,
|
|
@@ -8610,13 +8649,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8610
8649
|
children: [
|
|
8611
8650
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GoalBar, {
|
|
8612
8651
|
status: state.sessionStatus ?? null,
|
|
8613
|
-
onStop: () => state.send({
|
|
8652
|
+
onStop: readOnly ? void 0 : () => state.send({
|
|
8614
8653
|
type: "command",
|
|
8615
8654
|
name: "goal",
|
|
8616
8655
|
args: "cancel"
|
|
8617
8656
|
})
|
|
8618
8657
|
}),
|
|
8619
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
|
|
8658
|
+
readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionPrompt, {
|
|
8620
8659
|
layout: "dock",
|
|
8621
8660
|
prompts: state.pendingPrompts,
|
|
8622
8661
|
onAnswerPermission: state.answerPermission,
|
|
@@ -8624,7 +8663,11 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8624
8663
|
ownDriverId: state.ownDriverId,
|
|
8625
8664
|
onFocusReturn: () => composerRef.current?.focus()
|
|
8626
8665
|
}),
|
|
8627
|
-
|
|
8666
|
+
readOnly ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8667
|
+
role: "status",
|
|
8668
|
+
className: "rounded-2xl bg-card px-4 py-3 text-center text-[14px] text-muted-foreground",
|
|
8669
|
+
children: "Read-only: you are watching this session."
|
|
8670
|
+
}) : setupRequired ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Composer, {
|
|
8628
8671
|
ref: composerRef,
|
|
8629
8672
|
draftMode,
|
|
8630
8673
|
catalog: state.commandCatalog ?? null,
|
|
@@ -8679,13 +8722,13 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8679
8722
|
selectedEntryId: state.openEntryId ?? void 0,
|
|
8680
8723
|
onSelect: (entry) => state.openExecutionDetail(entry.id),
|
|
8681
8724
|
onReturnToConversation: () => state.closeExecutionDetail(),
|
|
8682
|
-
onStop: stopExecutionEntry,
|
|
8725
|
+
onStop: readOnly ? void 0 : stopExecutionEntry,
|
|
8683
8726
|
schedules: state.scheduledTasks,
|
|
8684
8727
|
goal: currentGoal,
|
|
8685
|
-
onPauseSchedule: state.pauseSchedule,
|
|
8686
|
-
onResumeSchedule: state.resumeSchedule,
|
|
8687
|
-
onDeleteSchedule: state.deleteSchedule,
|
|
8688
|
-
onCancelGoal: () => state.send({
|
|
8728
|
+
onPauseSchedule: readOnly ? void 0 : state.pauseSchedule,
|
|
8729
|
+
onResumeSchedule: readOnly ? void 0 : state.resumeSchedule,
|
|
8730
|
+
onDeleteSchedule: readOnly ? void 0 : state.deleteSchedule,
|
|
8731
|
+
onCancelGoal: readOnly ? void 0 : () => state.send({
|
|
8689
8732
|
type: "command",
|
|
8690
8733
|
name: "goal",
|
|
8691
8734
|
args: "cancel"
|
|
@@ -8700,18 +8743,18 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8700
8743
|
complete: state.executionDetailComplete,
|
|
8701
8744
|
onClose: () => state.closeExecutionDetail(),
|
|
8702
8745
|
onLoadMore: () => state.loadMoreExecutionDetail(),
|
|
8703
|
-
onStop: openEntry ? () => stopExecutionEntry(openEntry) : void 0,
|
|
8746
|
+
onStop: openEntry && !readOnly ? () => stopExecutionEntry(openEntry) : void 0,
|
|
8704
8747
|
inputState: state.executionDetailInput,
|
|
8705
8748
|
mode: state.executionDetailMode,
|
|
8706
8749
|
unreadable: state.executionDetailUnreadable,
|
|
8707
8750
|
onShowEarlier: state.showEarlierExecutionDetail,
|
|
8708
8751
|
onShowLive: state.showLiveExecutionDetail,
|
|
8709
|
-
onSend: openEntry ? (prompt) => state.send({
|
|
8752
|
+
onSend: openEntry && !readOnly ? (prompt) => state.send({
|
|
8710
8753
|
type: "send-background-task",
|
|
8711
8754
|
taskId: openEntry.sourceId,
|
|
8712
8755
|
input: { prompt }
|
|
8713
8756
|
}) : void 0,
|
|
8714
|
-
onContinue: openEntry?.resumeSessionId ? () => {
|
|
8757
|
+
onContinue: openEntry?.resumeSessionId && !readOnly ? () => {
|
|
8715
8758
|
if (openEntry.resumeSessionId) state.switchSession(openEntry.resumeSessionId);
|
|
8716
8759
|
} : void 0
|
|
8717
8760
|
})
|
|
@@ -8719,8 +8762,8 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8719
8762
|
})]
|
|
8720
8763
|
}),
|
|
8721
8764
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionNotices, { state }),
|
|
8722
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsScreen, { state }),
|
|
8723
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentSwitcherSheet, { state }),
|
|
8765
|
+
readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsScreen, { state }),
|
|
8766
|
+
readOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentSwitcherSheet, { state }),
|
|
8724
8767
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HelpSheet, {
|
|
8725
8768
|
open: state.helpOpen,
|
|
8726
8769
|
onClose: () => state.closeHelp(),
|
|
@@ -8731,7 +8774,7 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, draftMod
|
|
|
8731
8774
|
}
|
|
8732
8775
|
/**
|
|
8733
8776
|
* A centered chrome frame for the pre-session (loading) and fatal states — reused by app shells. Its
|
|
8734
|
-
* content area is this page's `main` landmark
|
|
8777
|
+
* content area is this page's `main` landmark: nothing above `CenteredChrome`'s
|
|
8735
8778
|
* three call sites (starting, fatal, the trust question) supplies one, and none of the three ever
|
|
8736
8779
|
* render alongside `SessionSurface`'s own. `header` stays a sibling of `main`, not inside it, so it
|
|
8737
8780
|
* keeps its own implicit `banner` role.
|