@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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The transport-neutral session reducer for the GUI presentation layer. Reconstructs conversation
|
|
3
3
|
* state from the wire `TServerMessage` stream, independent of HOW the bytes arrive (a `TMakeSessionClient`
|
|
4
4
|
* factory over `{onMessage, onStatusChange}`). The core is **generic over its status type** (default
|
|
5
5
|
* `TConnectionStatus`) so a surface with extra connection states (e.g. the browser WebRTC surface) can widen it
|
|
6
6
|
* WITHOUT this core depending on that surface's types — keeping the dependency direction acyclic.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isObserverMessageType } from '@robota-sdk/agent-transport';
|
|
9
10
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
10
11
|
|
|
11
12
|
import { excludedCommandMessage, isExcludedCommand } from './excluded-commands.js';
|
|
@@ -53,7 +54,7 @@ import type {
|
|
|
53
54
|
TPermissionResultValue,
|
|
54
55
|
} from '@robota-sdk/agent-interface-session';
|
|
55
56
|
import type { IExecutionWorkspaceSnapshot } from '@robota-sdk/agent-interface-execution';
|
|
56
|
-
import type { TServerMessage } from '@robota-sdk/agent-transport';
|
|
57
|
+
import type { TServerMessage, TSessionSurfaceRole } from '@robota-sdk/agent-transport';
|
|
57
58
|
|
|
58
59
|
export type {
|
|
59
60
|
IActiveTool,
|
|
@@ -82,7 +83,7 @@ function nextSilentCommandRequestId(): string {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
|
-
*
|
|
86
|
+
* A finished turn's text and tool calls, in the order they actually happened — a running
|
|
86
87
|
* turn accumulates these alongside (not instead of) the legacy `streamingText`/`activeTools` live
|
|
87
88
|
* view, and `finishTurn` flushes them as SEPARATE conversation entries in order, instead of one
|
|
88
89
|
* tools-block followed by one merged text block. Consecutive tool calls with no text between them
|
|
@@ -117,7 +118,7 @@ function pushToolStartSegment(segments: TTurnSegment[], tool: IActiveTool): TTur
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
|
-
*
|
|
121
|
+
* Attribute a `tool_end` to its call by executionId first — two same-named parallel calls
|
|
121
122
|
* can finish out of start order, and matching by name+running alone would close whichever running
|
|
122
123
|
* call of that name is found first, not the one that actually finished. Falls back to name+running
|
|
123
124
|
* only when the event carries no executionId (legacy fixtures / hosts that predate it).
|
|
@@ -188,7 +189,7 @@ function applyToolEndToSegments(segments: TTurnSegment[], state: IToolState): TT
|
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
/**
|
|
191
|
-
*
|
|
192
|
+
* The files an Edit/Write call touched during the turn, aggregated by path — the "Changed
|
|
192
193
|
* files" row's data. `diffLines` keeps the LATEST edit's diff (what a click on the file opens); the
|
|
193
194
|
* +/- counts sum every edit to that path within the turn.
|
|
194
195
|
*/
|
|
@@ -220,7 +221,7 @@ function activeToolFromHistoricalState(state: IToolState): IActiveTool {
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
/**
|
|
223
|
-
*
|
|
224
|
+
* Turns the server's chronological `IHistoryDisplaySegment[]` (a reload/reconnect/resume
|
|
224
225
|
* replay) into the SAME `TConversationEntry[]` shape a live turn's `finishTurn` produces — reusing its
|
|
225
226
|
* exact segment-accumulation and "Changed files" logic, so the two paths cannot draw the turn/grouping
|
|
226
227
|
* boundary differently. A `'text', role: 'user'` segment is both its own entry AND the turn boundary:
|
|
@@ -272,6 +273,10 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
272
273
|
const [status, setStatus] = useState<TStatus>('disconnected' as TStatus);
|
|
273
274
|
// The app-view requests read this to refuse at once when no connection exists to answer them.
|
|
274
275
|
const statusRef = useRef<TStatus>('disconnected' as TStatus);
|
|
276
|
+
// The role the host gave this connection; null until it answers and again once the connection drops.
|
|
277
|
+
const [role, setRole] = useState<TSessionSurfaceRole | null>(null);
|
|
278
|
+
const roleRef = useRef<TSessionSurfaceRole | null>(null);
|
|
279
|
+
const isObserving = useCallback((): boolean => roleRef.current === 'observe', []);
|
|
275
280
|
const [messages, setMessages] = useState<TConversationEntry[]>([]);
|
|
276
281
|
const [activeTools, setActiveTools] = useState<IActiveTool[]>([]);
|
|
277
282
|
const [streamingText, setStreamingText] = useState('');
|
|
@@ -284,15 +289,15 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
284
289
|
const [sessionName, setSessionName] = useState<string | null>(null);
|
|
285
290
|
const [sessionNotices, setSessionNotices] = useState<readonly ISessionNotice[]>([]);
|
|
286
291
|
const [commandCatalog, setCommandCatalog] = useState<TCommandCatalog | null>(null);
|
|
287
|
-
//
|
|
292
|
+
// `send` (below) reads this on every `command` message to catch an excluded command —
|
|
288
293
|
// a ref (not the state above) so `send`'s identity never changes as the catalog arrives or is
|
|
289
294
|
// refreshed, matching every other value a stable callback in this file reads this way.
|
|
290
295
|
const commandCatalogRef = useRef<TCommandCatalog | null>(null);
|
|
291
296
|
const [sessionStatus, setSessionStatus] = useState<TSessionStatus | null>(null);
|
|
292
|
-
//
|
|
297
|
+
// Learned from the first frame the server sends this connection, so its own messages and
|
|
293
298
|
// prompts never carry a "from" label — only ANOTHER driver's do.
|
|
294
299
|
const [ownDriverId, setOwnDriverId] = useState<TDriverId | null>(null);
|
|
295
|
-
//
|
|
300
|
+
// `/help` opens the GUI's own Help sheet — it never reaches the session (`send` below
|
|
296
301
|
// catches it), so there is no round trip and no stale terminal-style text dump to replace.
|
|
297
302
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
298
303
|
const openHelp = useCallback((): void => setHelpOpen(true), []);
|
|
@@ -302,7 +307,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
302
307
|
const streamingTextRef = useRef('');
|
|
303
308
|
// The tools of the running turn, mirrored so the turn's end can keep them in the conversation.
|
|
304
309
|
const activeToolsRef = useRef<IActiveTool[]>([]);
|
|
305
|
-
//
|
|
310
|
+
// The running turn's text/tools in the order they happened — see `TTurnSegment` above.
|
|
306
311
|
const turnSegmentsRef = useRef<TTurnSegment[]>([]);
|
|
307
312
|
// A screen this surface's command asked for: it answers the command in place of the command's own
|
|
308
313
|
// reply — with the "not available" line (`text`), or with nothing when the screen opened (null).
|
|
@@ -323,7 +328,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
323
328
|
}, []);
|
|
324
329
|
/**
|
|
325
330
|
* End the running turn: its text and tool calls stay in the conversation as SEPARATE entries, in
|
|
326
|
-
* the order they happened
|
|
331
|
+
* the order they happened — not one tools-block grouped ahead of a single merged reply.
|
|
327
332
|
*/
|
|
328
333
|
const finishTurn = useCallback(
|
|
329
334
|
(toolStatus: (tool: IActiveTool) => IActiveTool['status']): void => {
|
|
@@ -344,7 +349,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
344
349
|
appendEntry({ id: segment.id, role: 'assistant', content: segment.text });
|
|
345
350
|
}
|
|
346
351
|
}
|
|
347
|
-
//
|
|
352
|
+
// A turn that changed files ends with one compact summary row, after everything else.
|
|
348
353
|
const changedFiles = collectChangedFiles(segments);
|
|
349
354
|
if (changedFiles.length > 0) {
|
|
350
355
|
appendEntry({ id: nextId(), role: 'changed-files', files: changedFiles });
|
|
@@ -354,7 +359,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
354
359
|
);
|
|
355
360
|
// Commands this surface sent whose result has not come back — a screen request pairs with one.
|
|
356
361
|
const commandsInFlightRef = useRef(0);
|
|
357
|
-
//
|
|
362
|
+
// The `requestId`s of commands sent via `sendCommandSilently`, still awaiting
|
|
358
363
|
// their `command_result`. The wire already correlates a command with its reply by `requestId` (see
|
|
359
364
|
// `TClientMessage`'s `command` / `TServerMessage`'s `command_result` in `@robota-sdk/agent-transport`)
|
|
360
365
|
// — a plain `send` command and a `sendCommandSilently` command can be outstanding at once, and only
|
|
@@ -375,18 +380,21 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
375
380
|
const executionDetailInputRef = useRef<string | null>(null);
|
|
376
381
|
const send = useCallback(
|
|
377
382
|
(msg: TClientMessage): void => {
|
|
383
|
+
// A frame an observer may not send never leaves, so nothing may start waiting for its reply.
|
|
384
|
+
// The surface's own answers (`/help`, an excluded command) still run below.
|
|
385
|
+
const refused = isObserving() && !isObserverMessageType(msg.type);
|
|
378
386
|
if (msg.type === 'send-background-task') {
|
|
379
|
-
if (executionDetailInputRef.current !== null) return;
|
|
387
|
+
if (refused || executionDetailInputRef.current !== null) return;
|
|
380
388
|
executionDetailInputRef.current = msg.taskId;
|
|
381
389
|
setExecutionDetailInput({ taskId: msg.taskId, status: 'pending' });
|
|
382
390
|
}
|
|
383
391
|
if (msg.type === 'command') {
|
|
384
|
-
//
|
|
392
|
+
// `/help` is the GUI's own Help sheet — never sent to the session.
|
|
385
393
|
if (msg.name === 'help') {
|
|
386
394
|
setHelpOpen(true);
|
|
387
395
|
return;
|
|
388
396
|
}
|
|
389
|
-
//
|
|
397
|
+
// A command the GUI leaves out on purpose is caught here, before the session ever
|
|
390
398
|
// sees it — its own plain sentence answers in place of the session's refusal, exactly like a
|
|
391
399
|
// typed command's own result would (same entry shape, same place in the conversation). The
|
|
392
400
|
// catalog entry (when known) carries `runner`/`surfaces`, so a client-only command excluded
|
|
@@ -403,15 +411,17 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
403
411
|
});
|
|
404
412
|
return;
|
|
405
413
|
}
|
|
414
|
+
if (refused) return;
|
|
406
415
|
commandsInFlightRef.current += 1;
|
|
407
416
|
}
|
|
417
|
+
if (refused) return;
|
|
408
418
|
if (msg.type === 'switch-session' || msg.type === 'new-session') {
|
|
409
419
|
sessionChangesInFlightRef.current += 1;
|
|
410
420
|
if (msg.requestId !== undefined) sessionChangeRequestIdsRef.current.add(msg.requestId);
|
|
411
421
|
}
|
|
412
422
|
clientRef.current?.send(msg);
|
|
413
423
|
},
|
|
414
|
-
[appendEntry],
|
|
424
|
+
[appendEntry, isObserving],
|
|
415
425
|
);
|
|
416
426
|
/** One session change this surface asked for has been answered. */
|
|
417
427
|
const settleSessionChange = useCallback((requestId?: string): void => {
|
|
@@ -419,7 +429,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
419
429
|
if (requestId !== undefined) sessionChangeRequestIdsRef.current.delete(requestId);
|
|
420
430
|
}, []);
|
|
421
431
|
/**
|
|
422
|
-
*
|
|
432
|
+
* Run a command the same way `send` does, but flag it as controls-triggered —
|
|
423
433
|
* the model/mode/effort pop-up menus use this instead of `send` directly, so their change is
|
|
424
434
|
* confirmed by the control's own label (via the `get-status` refresh `command_result` already
|
|
425
435
|
* triggers) rather than a conversation card. A typed `/command` still goes through plain `send`
|
|
@@ -428,7 +438,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
428
438
|
* `TClientMessage` — giving `command` a `requestId` there would let ANY caller of `send` opt a typed
|
|
429
439
|
* command into silence, which is exactly what this must not allow.
|
|
430
440
|
*
|
|
431
|
-
*
|
|
441
|
+
* `send` also intercepts `help` and an excluded command before either reaches the wire —
|
|
432
442
|
* for `help` that opens the sheet regardless of how it was asked for, but an excluded command's
|
|
433
443
|
* interception adds a VISIBLE info card, defeating the silence this function promises. Every current
|
|
434
444
|
* caller (model/mode/effort) names neither, so this is dormant, not exercised — a future caller must
|
|
@@ -453,18 +463,18 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
453
463
|
const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
|
|
454
464
|
const isConnected = useCallback((): boolean => statusRef.current === 'connected', []);
|
|
455
465
|
const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } =
|
|
456
|
-
useAppViewRequests(send, isConnected);
|
|
466
|
+
useAppViewRequests(send, isConnected, isObserving);
|
|
457
467
|
const {
|
|
458
468
|
handleSessionsMessage,
|
|
459
469
|
canListSessions,
|
|
460
470
|
markCurrent,
|
|
461
471
|
armRestore,
|
|
462
472
|
...sessionDirectoryState
|
|
463
|
-
} = useSessionDirectoryState(send);
|
|
473
|
+
} = useSessionDirectoryState(send, isObserving);
|
|
464
474
|
const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
|
|
465
475
|
const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } =
|
|
466
476
|
useSettingsState(send);
|
|
467
|
-
//
|
|
477
|
+
// Read inside `handleMessage` via a ref, never `settingsState.settingsOpen` directly —
|
|
468
478
|
// that reactive value would have to sit in `handleMessage`'s own dependency array, which sits in
|
|
469
479
|
// the connection effect's dependency array below: opening/closing Settings would give
|
|
470
480
|
// `handleMessage` a new identity each time, tearing down and reopening the WebSocket underneath it.
|
|
@@ -486,7 +496,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
486
496
|
|
|
487
497
|
const handleMessage = useCallback(
|
|
488
498
|
(msg: TServerMessage): void => {
|
|
489
|
-
//
|
|
499
|
+
// Touching the exhaustive registry here keeps every decoded variant tied to an
|
|
490
500
|
// explicit GUI ownership decision, including variants intentionally handled by focused views.
|
|
491
501
|
void SERVER_MESSAGE_HANDLING[msg.type];
|
|
492
502
|
if (handleUsageMessage(msg)) return;
|
|
@@ -498,14 +508,19 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
498
508
|
if (handleSessionsMessage(msg)) return;
|
|
499
509
|
if (handleSettingsMessage(msg)) return;
|
|
500
510
|
if (handleAgentDefinitionsMessage(msg)) return;
|
|
501
|
-
//
|
|
511
|
+
// Exclusively owns `background_tasks` (the Scheduled group's roster); a
|
|
502
512
|
// schedule write instead rides the generic `command`/`cancel-background-task` messages, so
|
|
503
513
|
// for THOSE types this always returns false — it only refreshes on top of whatever else those
|
|
504
514
|
// types already do below, never in place of it.
|
|
505
515
|
if (handleSchedulesMessage(msg)) return;
|
|
506
516
|
switch (msg.type) {
|
|
517
|
+
case 'surface_role': {
|
|
518
|
+
roleRef.current = msg.role;
|
|
519
|
+
setRole(msg.role);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
507
522
|
case 'messages': {
|
|
508
|
-
//
|
|
523
|
+
// `display` is the server's projection of this same history into tool rows,
|
|
509
524
|
// diffs and "Changed files" — a reload/reconnect/resume replay renders THAT. `display` is
|
|
510
525
|
// absent only for a host that predates it; that host still gets a working (text-only)
|
|
511
526
|
// transcript, exactly what this branch always did.
|
|
@@ -538,7 +553,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
538
553
|
const next = streamingTextRef.current + msg.delta;
|
|
539
554
|
streamingTextRef.current = next;
|
|
540
555
|
setStreamingText(next);
|
|
541
|
-
//
|
|
556
|
+
// Also track WHERE this text sits relative to the turn's tool calls, for finishTurn.
|
|
542
557
|
turnSegmentsRef.current = appendTextDeltaToSegments(turnSegmentsRef.current, msg.delta);
|
|
543
558
|
break;
|
|
544
559
|
}
|
|
@@ -555,7 +570,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
555
570
|
}
|
|
556
571
|
case 'tool_end': {
|
|
557
572
|
const { state } = msg;
|
|
558
|
-
//
|
|
573
|
+
// Attribute to the ONE matching running call (executionId-first) — never map over
|
|
559
574
|
// every running same-named entry, which would close all of them at once.
|
|
560
575
|
updateActiveTools((prev) => {
|
|
561
576
|
const idx = findRunningToolIndex(prev, state);
|
|
@@ -572,7 +587,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
572
587
|
setExecutionWorkspace(msg.snapshot);
|
|
573
588
|
break;
|
|
574
589
|
}
|
|
575
|
-
//
|
|
590
|
+
// A Stop that did not take effect (e.g. the task/loop had already finished) is
|
|
576
591
|
// worth telling the operator; a success shows itself through the snapshot update above.
|
|
577
592
|
case 'background_task_control_result': {
|
|
578
593
|
if (msg.action === 'send' && executionDetailInputRef.current === msg.taskId) {
|
|
@@ -596,7 +611,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
596
611
|
}
|
|
597
612
|
break;
|
|
598
613
|
}
|
|
599
|
-
//
|
|
614
|
+
// The prompt queued behind a running turn — shown above the composer, editable
|
|
600
615
|
// and removable (`cancel-queue`). `pending: null` means nothing waits; the row disappears.
|
|
601
616
|
case 'pending': {
|
|
602
617
|
setQueuedPrompt(
|
|
@@ -607,12 +622,12 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
607
622
|
case 'permission_request':
|
|
608
623
|
case 'ask_request':
|
|
609
624
|
case 'prompt_resolved': {
|
|
610
|
-
//
|
|
625
|
+
// The paired owner renders + answers its own prompts (local == remote).
|
|
611
626
|
setPendingPrompts((prev) => applyPromptEvent(prev, msg));
|
|
612
627
|
break;
|
|
613
628
|
}
|
|
614
629
|
case 'ui_intent': {
|
|
615
|
-
//
|
|
630
|
+
// `/resume` asks for the session picker — the sidebar, when the host lists sessions.
|
|
616
631
|
if (guiScreenForUiIntent(msg.event.intent) === 'session-sidebar' && canListSessions()) {
|
|
617
632
|
setSessionSidebarOpen(true);
|
|
618
633
|
requestSessions();
|
|
@@ -624,7 +639,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
624
639
|
}
|
|
625
640
|
break;
|
|
626
641
|
}
|
|
627
|
-
//
|
|
642
|
+
// `/settings` asks for the Settings screen — always available on this surface.
|
|
628
643
|
if (guiScreenForUiIntent(msg.event.intent) === 'settings') {
|
|
629
644
|
openSettings();
|
|
630
645
|
if (commandsInFlightRef.current > 0) {
|
|
@@ -635,7 +650,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
635
650
|
}
|
|
636
651
|
break;
|
|
637
652
|
}
|
|
638
|
-
//
|
|
653
|
+
// `/plugin` asks for the plugin manager — the Settings screen's
|
|
639
654
|
// Plugins section, the way `show-settings` opens the screen itself above.
|
|
640
655
|
if (guiScreenForUiIntent(msg.event.intent) === 'settings-plugins') {
|
|
641
656
|
openSettings('plugins');
|
|
@@ -647,7 +662,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
647
662
|
}
|
|
648
663
|
break;
|
|
649
664
|
}
|
|
650
|
-
//
|
|
665
|
+
// `/agent` (bare) asks for the switcher — always available on this
|
|
651
666
|
// surface, unlike the session picker, which needs a host that lists sessions.
|
|
652
667
|
if (guiScreenForUiIntent(msg.event.intent) === 'agent-switcher') {
|
|
653
668
|
openAgentSwitcher();
|
|
@@ -659,8 +674,8 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
659
674
|
}
|
|
660
675
|
break;
|
|
661
676
|
}
|
|
662
|
-
//
|
|
663
|
-
// The command's result follows; the unavailable line answers it (
|
|
677
|
+
// A command this surface issued requested a screen the GUI does not have.
|
|
678
|
+
// The command's result follows; the unavailable line answers it (never silent).
|
|
664
679
|
const unavailable = {
|
|
665
680
|
name: uiIntentCommandName(msg.event.intent),
|
|
666
681
|
text: describeUiIntentForGui(msg.event.intent),
|
|
@@ -679,14 +694,14 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
679
694
|
}
|
|
680
695
|
break;
|
|
681
696
|
}
|
|
682
|
-
//
|
|
697
|
+
// Broadcast session events — a rename/clear executed by the HOST (from any
|
|
683
698
|
// surface, co-driving included) is reflected here; never a silent drop.
|
|
684
699
|
case 'session_renamed': {
|
|
685
700
|
setSessionName(msg.event.name);
|
|
686
701
|
requestSessions();
|
|
687
702
|
break;
|
|
688
703
|
}
|
|
689
|
-
//
|
|
704
|
+
// The host made another session current — drop what this one showed, re-read it all.
|
|
690
705
|
case 'session_switched': {
|
|
691
706
|
rejectPendingAppRequests('The active session changed.');
|
|
692
707
|
settleSessionChange();
|
|
@@ -747,7 +762,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
747
762
|
]);
|
|
748
763
|
break;
|
|
749
764
|
}
|
|
750
|
-
//
|
|
765
|
+
// A new or switch this surface asked for was refused; the reason is the toast, and
|
|
751
766
|
// commands in flight are untouched. Only the client that asked is told.
|
|
752
767
|
case 'session_change_failed': {
|
|
753
768
|
if (
|
|
@@ -763,7 +778,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
763
778
|
]);
|
|
764
779
|
break;
|
|
765
780
|
}
|
|
766
|
-
//
|
|
781
|
+
// A row's rename landed — refresh the listing so its title (or the live one, if it
|
|
767
782
|
// was current) is current everywhere it shows.
|
|
768
783
|
case 'session_renamed_in_list': {
|
|
769
784
|
requestSessions();
|
|
@@ -776,7 +791,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
776
791
|
]);
|
|
777
792
|
break;
|
|
778
793
|
}
|
|
779
|
-
//
|
|
794
|
+
// A row was deleted. If it was this surface's own session, `session_switched`
|
|
780
795
|
// already arrived (the host switches away before answering) and reset the conversation; this
|
|
781
796
|
// only needs the listing refreshed so the row is gone everywhere it shows.
|
|
782
797
|
case 'session_deleted': {
|
|
@@ -865,7 +880,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
865
880
|
send({ type: 'get-status' });
|
|
866
881
|
send({ type: 'get-commands' });
|
|
867
882
|
commandsInFlightRef.current = Math.max(0, commandsInFlightRef.current - 1);
|
|
868
|
-
//
|
|
883
|
+
// A successful `/provider` command run through the raw command path (Add, Edit,
|
|
869
884
|
// Duplicate — Use/Model/Delete already return a fresh snapshot from their own
|
|
870
885
|
// `update-settings` reply) changes the "Providers & Models" list; refresh it if the screen
|
|
871
886
|
// is open, the same way `get-status` above refreshes the model/mode chips. Never touches
|
|
@@ -873,13 +888,13 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
873
888
|
if (msg.name === 'provider' && msg.success && settingsOpenRef.current) {
|
|
874
889
|
refreshSettings();
|
|
875
890
|
}
|
|
876
|
-
//
|
|
891
|
+
// Identify silence by THIS reply's own `requestId`, not by position or
|
|
877
892
|
// count — see `silentCommandRequestIdsRef` above. No `requestId` (an older host, or a typed
|
|
878
893
|
// command) always falls through to `silent = false`, never the reverse.
|
|
879
894
|
const silent =
|
|
880
895
|
msg.requestId !== undefined && silentCommandRequestIdsRef.current.delete(msg.requestId);
|
|
881
|
-
//
|
|
882
|
-
// confirmation line answers it, never a conversation card
|
|
896
|
+
// A selection made in the agent switcher sheet — its own plain
|
|
897
|
+
// confirmation line answers it, never a conversation card.
|
|
883
898
|
if (resolveSwitchResult(msg.requestId, msg)) break;
|
|
884
899
|
const unavailable = pendingIntentRef.current;
|
|
885
900
|
pendingIntentRef.current = null;
|
|
@@ -898,7 +913,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
898
913
|
}
|
|
899
914
|
// A command that starts a turn (a skill) says nothing itself; the turn is its answer.
|
|
900
915
|
if (msg.message.trim().length === 0) break;
|
|
901
|
-
//
|
|
916
|
+
// A change made through a status control confirms itself through the
|
|
902
917
|
// control's own label (the `get-status` refresh above lands it) — no conversation card. A
|
|
903
918
|
// failure still needs saying, as a plain notice rather than a card, and the label is left
|
|
904
919
|
// unchanged since nothing actually applied. A typed command (plain `send`) keeps its card
|
|
@@ -924,7 +939,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
924
939
|
case 'complete':
|
|
925
940
|
case 'interrupted': {
|
|
926
941
|
send({ type: 'get-status' });
|
|
927
|
-
//
|
|
942
|
+
// The turn ending can advance the queue (its next entry now runs) or, if this
|
|
928
943
|
// was an abort, drop it entirely (`abort()` clears the whole queue) — either way the
|
|
929
944
|
// composer's queued-message row is stale until this reply refreshes it.
|
|
930
945
|
send({ type: 'get-pending' });
|
|
@@ -984,6 +999,10 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
984
999
|
const onStatusChange = (next: TStatus): void => {
|
|
985
1000
|
statusRef.current = next;
|
|
986
1001
|
setStatus(next);
|
|
1002
|
+
if (next !== 'connected') {
|
|
1003
|
+
roleRef.current = null;
|
|
1004
|
+
setRole(null);
|
|
1005
|
+
}
|
|
987
1006
|
// A reply lost with the connection never arrives; the views waiting on one learn that now.
|
|
988
1007
|
if (next !== 'connected') {
|
|
989
1008
|
rejectPendingAppRequests();
|
|
@@ -1002,9 +1021,11 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
1002
1021
|
sessionChangesInFlightRef.current = 0;
|
|
1003
1022
|
sessionChangeRequestIdsRef.current.clear();
|
|
1004
1023
|
pendingIntentRef.current = null;
|
|
1024
|
+
// First, so its reply precedes the session listing the reconnect restore reads.
|
|
1025
|
+
client.send({ type: 'get-surface-role' });
|
|
1005
1026
|
client.send({ type: 'get-commands' });
|
|
1006
1027
|
client.send({ type: 'get-status' });
|
|
1007
|
-
//
|
|
1028
|
+
// A reconnect that lands back on the SAME session (the common case) fires no
|
|
1008
1029
|
// `session_switched` — its own `get-pending` would be missed — so ask here too, or a stale
|
|
1009
1030
|
// `queuedPrompt` from before the drop keeps showing (with working Edit/Remove) until the
|
|
1010
1031
|
// turn it was queued behind happens to end.
|
|
@@ -1015,7 +1036,16 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
1015
1036
|
requestSchedules();
|
|
1016
1037
|
}
|
|
1017
1038
|
};
|
|
1018
|
-
const
|
|
1039
|
+
const inner = makeClient({ onMessage: handleMessage, onStatusChange });
|
|
1040
|
+
// The host is the authority; this only keeps an observer from sending what the host would refuse.
|
|
1041
|
+
const client: ISessionClientHandle = {
|
|
1042
|
+
connect: () => inner.connect(),
|
|
1043
|
+
disconnect: () => inner.disconnect(),
|
|
1044
|
+
send: (msg) => {
|
|
1045
|
+
if (roleRef.current === 'observe' && !isObserverMessageType(msg.type)) return;
|
|
1046
|
+
inner.send(msg);
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1019
1049
|
clientRef.current = client;
|
|
1020
1050
|
client.connect();
|
|
1021
1051
|
return () => {
|
|
@@ -1045,6 +1075,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
1045
1075
|
commandCatalog,
|
|
1046
1076
|
sessionStatus,
|
|
1047
1077
|
ownDriverId,
|
|
1078
|
+
role,
|
|
1048
1079
|
send,
|
|
1049
1080
|
sendCommandSilently,
|
|
1050
1081
|
pendingPrompts,
|
|
@@ -1077,7 +1108,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
1077
1108
|
export function useWsSession(
|
|
1078
1109
|
url: string,
|
|
1079
1110
|
): IWsSessionState<TConnectionStatus> & { connectionLost: boolean } {
|
|
1080
|
-
//
|
|
1111
|
+
// Retries gave up — the runtime is not coming back by itself. A presentation layer
|
|
1081
1112
|
// (e.g. `SessionSurface`) reads this to show a banner instead of a callback-driven screen swap.
|
|
1082
1113
|
const [connectionLost, setConnectionLost] = useState(false);
|
|
1083
1114
|
const makeClient = useCallback<TMakeSessionClient<TConnectionStatus>>(
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type {
|
|
|
23
23
|
TSessionListing,
|
|
24
24
|
TSessionsError,
|
|
25
25
|
} from './hooks/session-client-types.js';
|
|
26
|
-
//
|
|
26
|
+
// A host that can restart the runtime calls this before reloading the page, so the
|
|
27
27
|
// first session listing after reconnecting can switch back to the session the person was in.
|
|
28
28
|
export { rememberSessionForRestore } from './hooks/use-session-directory.js';
|
|
29
29
|
|
|
@@ -46,7 +46,7 @@ export type {
|
|
|
46
46
|
export { applyPromptEvent, permissionResponse, askResponse } from './hooks/prompt-state.js';
|
|
47
47
|
export type { TPendingPrompt } from './hooks/prompt-state.js';
|
|
48
48
|
|
|
49
|
-
// ── UI-intent (command screen-request) state —
|
|
49
|
+
// ── UI-intent (command screen-request) state — ──
|
|
50
50
|
export { describeUiIntentForGui, guiScreenForUiIntent } from './hooks/ui-intent-state.js';
|
|
51
51
|
|
|
52
52
|
// ── Presentation components ─────────────────────────────────
|
|
@@ -60,12 +60,12 @@ export { SessionSurface, CenteredChrome } from './components/SessionSurface.js';
|
|
|
60
60
|
export type { IComposerHandle, IPickedFile } from './components/Composer.js';
|
|
61
61
|
export { SessionSidebar } from './components/SessionSidebar.js';
|
|
62
62
|
export { SessionMonitor } from './components/SessionMonitor.js';
|
|
63
|
-
//
|
|
64
|
-
// and
|
|
63
|
+
// The shared modal-shell primitive (also consumed by the mode-change confirmation
|
|
64
|
+
// and the session-delete confirmation) and the Settings screen it backs.
|
|
65
65
|
export { Dialog, ConfirmDialog } from './components/Dialog.js';
|
|
66
66
|
export type { IDialogProps, IConfirmDialogProps } from './components/Dialog.js';
|
|
67
67
|
export { SettingsScreen } from './components/SettingsScreen.js';
|
|
68
|
-
//
|
|
68
|
+
// The Help sheet `/help` opens instead of the terminal-style text list.
|
|
69
69
|
export { HelpSheet } from './components/HelpSheet.js';
|
|
70
70
|
// Pieces of a surface's chrome, not roots: render them inside a `agent-ui` scope.
|
|
71
71
|
export { ProductMark, ProductWordmark } from './components/Brand.js';
|
package/src/schedule-view.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Plain-words presentation for the Agents panel's "Scheduled" group. Never an
|
|
3
3
|
* ISO string or a cron expression: a concrete `nextFireAt` is shown in local time as "Today 9:00" /
|
|
4
4
|
* "Tomorrow 9:00" / a weekday / a date; a handful of common cron shapes are phrased ("Every weekday
|
|
5
5
|
* at 9:00"); anything else falls back to "Repeats".
|
package/src/styles/surface.css
CHANGED
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/*
|
|
219
|
-
* A code block's own horizontal scrollbar
|
|
219
|
+
* A code block's own horizontal scrollbar stays visible rather than only-on-hover like the
|
|
220
220
|
* rest of the page (`theme.css`'s quiet overlay scrollbar) — a long unwrapped line needs a standing
|
|
221
221
|
* signal that there is more of it, not one that only appears once the person already found the edge.
|
|
222
222
|
* `.gui-code-scroll::-webkit-scrollbar-thumb` outranks that page-wide bare `::-webkit-scrollbar-thumb`
|