@robota-sdk/agent-ui-web 3.1.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/index.cjs +475 -97
- package/dist/index.d.cts +31 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +31 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +476 -99
- package/dist/index.js.map +1 -1
- package/docs/SPEC.md +5 -0
- package/package.json +6 -6
- package/src/client/desktop-session-client.ts +78 -0
- package/src/components/AgentActivityPanel.tsx +38 -31
- package/src/components/Composer.tsx +71 -24
- package/src/components/ExecutionDetailSheet.tsx +116 -6
- package/src/components/SessionSurface.tsx +89 -36
- package/src/hooks/server-message-handling.ts +2 -0
- package/src/hooks/session-client-types.ts +12 -1
- package/src/hooks/use-app-view-requests.ts +8 -1
- package/src/hooks/use-execution-detail.ts +133 -33
- package/src/hooks/use-session-directory.ts +7 -2
- package/src/hooks/useSessionClient.ts +81 -9
- package/src/index.ts +2 -0
|
@@ -6,6 +6,7 @@
|
|
|
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,
|
|
@@ -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('');
|
|
@@ -368,8 +373,21 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
368
373
|
// Their request ids, for this connection: a refusal names the request it answers. A switch
|
|
369
374
|
// answers with no id, so an id that succeeded stays here until the connection is replaced.
|
|
370
375
|
const sessionChangeRequestIdsRef = useRef(new Set<string>());
|
|
376
|
+
const [executionDetailInput, setExecutionDetailInput] = useState<{
|
|
377
|
+
taskId: string;
|
|
378
|
+
status: 'pending' | 'sent' | 'failed';
|
|
379
|
+
}>();
|
|
380
|
+
const executionDetailInputRef = useRef<string | null>(null);
|
|
371
381
|
const send = useCallback(
|
|
372
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);
|
|
386
|
+
if (msg.type === 'send-background-task') {
|
|
387
|
+
if (refused || executionDetailInputRef.current !== null) return;
|
|
388
|
+
executionDetailInputRef.current = msg.taskId;
|
|
389
|
+
setExecutionDetailInput({ taskId: msg.taskId, status: 'pending' });
|
|
390
|
+
}
|
|
373
391
|
if (msg.type === 'command') {
|
|
374
392
|
// #3282 §4e: `/help` is the GUI's own Help sheet — never sent to the session.
|
|
375
393
|
if (msg.name === 'help') {
|
|
@@ -393,15 +411,17 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
393
411
|
});
|
|
394
412
|
return;
|
|
395
413
|
}
|
|
414
|
+
if (refused) return;
|
|
396
415
|
commandsInFlightRef.current += 1;
|
|
397
416
|
}
|
|
417
|
+
if (refused) return;
|
|
398
418
|
if (msg.type === 'switch-session' || msg.type === 'new-session') {
|
|
399
419
|
sessionChangesInFlightRef.current += 1;
|
|
400
420
|
if (msg.requestId !== undefined) sessionChangeRequestIdsRef.current.add(msg.requestId);
|
|
401
421
|
}
|
|
402
422
|
clientRef.current?.send(msg);
|
|
403
423
|
},
|
|
404
|
-
[appendEntry],
|
|
424
|
+
[appendEntry, isObserving],
|
|
405
425
|
);
|
|
406
426
|
/** One session change this surface asked for has been answered. */
|
|
407
427
|
const settleSessionChange = useCallback((requestId?: string): void => {
|
|
@@ -433,20 +453,24 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
433
453
|
[send],
|
|
434
454
|
);
|
|
435
455
|
const { handleUsageMessage, ...personalUsageState } = usePersonalUsageState(send);
|
|
436
|
-
const {
|
|
437
|
-
|
|
456
|
+
const {
|
|
457
|
+
handleExecutionDetailMessage,
|
|
458
|
+
handleExecutionWorkspace,
|
|
459
|
+
closeExecutionDetail,
|
|
460
|
+
...executionDetailState
|
|
461
|
+
} = useExecutionDetailState(send);
|
|
438
462
|
const { handleModelListMessage, ...modelListState } = useModelListState(send);
|
|
439
463
|
const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
|
|
440
464
|
const isConnected = useCallback((): boolean => statusRef.current === 'connected', []);
|
|
441
465
|
const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } =
|
|
442
|
-
useAppViewRequests(send, isConnected);
|
|
466
|
+
useAppViewRequests(send, isConnected, isObserving);
|
|
443
467
|
const {
|
|
444
468
|
handleSessionsMessage,
|
|
445
469
|
canListSessions,
|
|
446
470
|
markCurrent,
|
|
447
471
|
armRestore,
|
|
448
472
|
...sessionDirectoryState
|
|
449
|
-
} = useSessionDirectoryState(send);
|
|
473
|
+
} = useSessionDirectoryState(send, isObserving);
|
|
450
474
|
const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
|
|
451
475
|
const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } =
|
|
452
476
|
useSettingsState(send);
|
|
@@ -465,7 +489,9 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
465
489
|
const { handleSchedulesMessage, requestSchedules, ...schedulesState } = useSchedulesState(send);
|
|
466
490
|
|
|
467
491
|
const clearCompactionNotices = useCallback((): void => {
|
|
468
|
-
setSessionNotices((previous) =>
|
|
492
|
+
setSessionNotices((previous) =>
|
|
493
|
+
previous.filter((notice) => notice.kind !== 'context-compacted'),
|
|
494
|
+
);
|
|
469
495
|
}, []);
|
|
470
496
|
|
|
471
497
|
const handleMessage = useCallback(
|
|
@@ -488,6 +514,11 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
488
514
|
// types already do below, never in place of it.
|
|
489
515
|
if (handleSchedulesMessage(msg)) return;
|
|
490
516
|
switch (msg.type) {
|
|
517
|
+
case 'surface_role': {
|
|
518
|
+
roleRef.current = msg.role;
|
|
519
|
+
setRole(msg.role);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
491
522
|
case 'messages': {
|
|
492
523
|
// #3288 §2: `display` is the server's projection of this same history into tool rows,
|
|
493
524
|
// diffs and "Changed files" — a reload/reconnect/resume replay renders THAT. `display` is
|
|
@@ -552,19 +583,29 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
552
583
|
break;
|
|
553
584
|
}
|
|
554
585
|
case 'execution_workspace_event': {
|
|
586
|
+
handleExecutionWorkspace(msg.snapshot);
|
|
555
587
|
setExecutionWorkspace(msg.snapshot);
|
|
556
588
|
break;
|
|
557
589
|
}
|
|
558
590
|
// #3288 §1: a Stop that did not take effect (e.g. the task/loop had already finished) is
|
|
559
591
|
// worth telling the operator; a success shows itself through the snapshot update above.
|
|
560
592
|
case 'background_task_control_result': {
|
|
593
|
+
if (msg.action === 'send' && executionDetailInputRef.current === msg.taskId) {
|
|
594
|
+
executionDetailInputRef.current = null;
|
|
595
|
+
setExecutionDetailInput({
|
|
596
|
+
taskId: msg.taskId,
|
|
597
|
+
status: msg.success ? 'sent' : 'failed',
|
|
598
|
+
});
|
|
599
|
+
}
|
|
561
600
|
if (!msg.success) {
|
|
562
601
|
setSessionNotices((previous) => [
|
|
563
602
|
...previous,
|
|
564
603
|
{
|
|
565
604
|
id: nextId(),
|
|
566
605
|
kind: 'background-task-control-failed',
|
|
567
|
-
message:
|
|
606
|
+
message:
|
|
607
|
+
msg.message ??
|
|
608
|
+
`Could not ${msg.action === 'send' ? 'send to' : 'stop'} ${msg.taskId}.`,
|
|
568
609
|
},
|
|
569
610
|
]);
|
|
570
611
|
}
|
|
@@ -765,6 +806,13 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
765
806
|
break;
|
|
766
807
|
}
|
|
767
808
|
case 'protocol_error': {
|
|
809
|
+
if (
|
|
810
|
+
executionDetailInputRef.current !== null &&
|
|
811
|
+
msg.message === 'Not permitted for an observer: send-background-task'
|
|
812
|
+
) {
|
|
813
|
+
setExecutionDetailInput({ taskId: executionDetailInputRef.current, status: 'failed' });
|
|
814
|
+
executionDetailInputRef.current = null;
|
|
815
|
+
}
|
|
768
816
|
if (sessionChangesInFlightRef.current > 0) {
|
|
769
817
|
// A host older than `session_change_failed` refuses a session change with this.
|
|
770
818
|
settleSessionChange();
|
|
@@ -911,6 +959,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
911
959
|
handleAgentDefinitionsMessage,
|
|
912
960
|
handleAppViewMessage,
|
|
913
961
|
handleExecutionDetailMessage,
|
|
962
|
+
handleExecutionWorkspace,
|
|
914
963
|
handleModelListMessage,
|
|
915
964
|
handleProjectMessage,
|
|
916
965
|
handleSchedulesMessage,
|
|
@@ -950,10 +999,19 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
950
999
|
const onStatusChange = (next: TStatus): void => {
|
|
951
1000
|
statusRef.current = next;
|
|
952
1001
|
setStatus(next);
|
|
1002
|
+
if (next !== 'connected') {
|
|
1003
|
+
roleRef.current = null;
|
|
1004
|
+
setRole(null);
|
|
1005
|
+
}
|
|
953
1006
|
// A reply lost with the connection never arrives; the views waiting on one learn that now.
|
|
954
1007
|
if (next !== 'connected') {
|
|
955
1008
|
rejectPendingAppRequests();
|
|
956
1009
|
clearCompactionNotices();
|
|
1010
|
+
closeExecutionDetail();
|
|
1011
|
+
if (executionDetailInputRef.current !== null) {
|
|
1012
|
+
setExecutionDetailInput({ taskId: executionDetailInputRef.current, status: 'failed' });
|
|
1013
|
+
executionDetailInputRef.current = null;
|
|
1014
|
+
}
|
|
957
1015
|
}
|
|
958
1016
|
// Every client learns what it can offer and what is in effect as soon as it is attached.
|
|
959
1017
|
if (next === 'connected') {
|
|
@@ -963,6 +1021,8 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
963
1021
|
sessionChangesInFlightRef.current = 0;
|
|
964
1022
|
sessionChangeRequestIdsRef.current.clear();
|
|
965
1023
|
pendingIntentRef.current = null;
|
|
1024
|
+
// First, so its reply precedes the session listing the reconnect restore reads.
|
|
1025
|
+
client.send({ type: 'get-surface-role' });
|
|
966
1026
|
client.send({ type: 'get-commands' });
|
|
967
1027
|
client.send({ type: 'get-status' });
|
|
968
1028
|
// #3280 §2: a reconnect that lands back on the SAME session (the common case) fires no
|
|
@@ -976,7 +1036,16 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
976
1036
|
requestSchedules();
|
|
977
1037
|
}
|
|
978
1038
|
};
|
|
979
|
-
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
|
+
};
|
|
980
1049
|
clientRef.current = client;
|
|
981
1050
|
client.connect();
|
|
982
1051
|
return () => {
|
|
@@ -988,6 +1057,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
988
1057
|
makeClient,
|
|
989
1058
|
handleMessage,
|
|
990
1059
|
clearCompactionNotices,
|
|
1060
|
+
closeExecutionDetail,
|
|
991
1061
|
requestSessions,
|
|
992
1062
|
requestSchedules,
|
|
993
1063
|
armRestore,
|
|
@@ -1005,6 +1075,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
1005
1075
|
commandCatalog,
|
|
1006
1076
|
sessionStatus,
|
|
1007
1077
|
ownDriverId,
|
|
1078
|
+
role,
|
|
1008
1079
|
send,
|
|
1009
1080
|
sendCommandSilently,
|
|
1010
1081
|
pendingPrompts,
|
|
@@ -1013,6 +1084,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
1013
1084
|
answerAsk,
|
|
1014
1085
|
...personalUsageState,
|
|
1015
1086
|
...executionDetailState,
|
|
1087
|
+
executionDetailInput,
|
|
1016
1088
|
closeExecutionDetail,
|
|
1017
1089
|
...modelListState,
|
|
1018
1090
|
...projectPanelState,
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ export { rememberSessionForRestore } from './hooks/use-session-directory.js';
|
|
|
29
29
|
|
|
30
30
|
// ── WS session client (loopback / localhost) ────────────────
|
|
31
31
|
export { createWsSessionClient } from './client/ws-session-client.js';
|
|
32
|
+
export { createDesktopSessionClient } from './client/desktop-session-client.js';
|
|
33
|
+
export type { IDesktopSessionBridge } from './client/desktop-session-client.js';
|
|
32
34
|
export type { IWsSessionClientCallbacks, TConnectionStatus } from './client/ws-session-client.js';
|
|
33
35
|
export { resolveClientRuntimeHost } from './client/runtime-host.js';
|
|
34
36
|
export type {
|