@robota-sdk/agent-ui-web 3.1.2 → 3.2.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 +25 -0
- package/dist/index.cjs +400 -65
- package/dist/index.d.cts +28 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +401 -67
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/client/desktop-session-client.ts +78 -0
- package/src/components/Composer.tsx +58 -14
- package/src/components/ExecutionDetailSheet.tsx +116 -6
- package/src/components/SessionSurface.tsx +26 -0
- package/src/hooks/session-client-types.ts +5 -0
- package/src/hooks/use-execution-detail.ts +133 -33
- package/src/hooks/useSessionClient.ts +45 -4
- package/src/index.ts +2 -0
|
@@ -1,33 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
* #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
|
|
3
|
-
* accumulated transcript records, and whether more can be loaded. Owned independently from chat
|
|
4
|
-
* state, the same way `use-personal-usage.ts` owns the usage-report protocol: one `requestId` per
|
|
5
|
-
* outstanding read, so a stale reply for an entry the operator already closed (or a page they moved
|
|
6
|
-
* past) is dropped rather than clobbering what is now on screen.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { useCallback, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
10
2
|
|
|
11
3
|
import type { TClientMessage } from '../client/ws-session-client.js';
|
|
12
4
|
import type {
|
|
5
|
+
IBackgroundTaskLogUnreadable,
|
|
13
6
|
IExecutionDetailCursor,
|
|
14
7
|
IExecutionDetailRecord,
|
|
8
|
+
IExecutionWorkspaceSnapshot,
|
|
15
9
|
} from '@robota-sdk/agent-interface-execution';
|
|
16
10
|
import type { TServerMessage } from '@robota-sdk/agent-transport';
|
|
17
11
|
|
|
18
12
|
export type TExecutionDetailStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
13
|
+
type TDetailMode = 'live' | 'transcript';
|
|
19
14
|
|
|
20
15
|
export interface IExecutionDetailState {
|
|
21
|
-
/** The entry currently open in the detail sheet, or null when it is closed. */
|
|
22
16
|
openEntryId: string | null;
|
|
23
17
|
executionDetailStatus: TExecutionDetailStatus;
|
|
24
|
-
/** Every record read so far for the open entry, oldest first — pages accumulate via loadMoreExecutionDetail. */
|
|
25
18
|
executionDetailRecords: readonly IExecutionDetailRecord[];
|
|
26
19
|
executionDetailError: string | null;
|
|
27
|
-
/** True once a page came back with no `nextCursor` — nothing more to load. */
|
|
28
20
|
executionDetailComplete: boolean;
|
|
21
|
+
executionDetailMode: TDetailMode;
|
|
22
|
+
executionDetailUnreadable: IBackgroundTaskLogUnreadable | undefined;
|
|
29
23
|
openExecutionDetail: (entryId: string) => void;
|
|
30
24
|
loadMoreExecutionDetail: () => void;
|
|
25
|
+
showEarlierExecutionDetail: () => void;
|
|
26
|
+
showLiveExecutionDetail: () => void;
|
|
31
27
|
closeExecutionDetail: () => void;
|
|
32
28
|
}
|
|
33
29
|
|
|
@@ -39,37 +35,65 @@ function nextRequestId(): string {
|
|
|
39
35
|
|
|
40
36
|
export function useExecutionDetailState(
|
|
41
37
|
send: (msg: TClientMessage) => void,
|
|
42
|
-
): IExecutionDetailState & {
|
|
38
|
+
): IExecutionDetailState & {
|
|
39
|
+
handleExecutionDetailMessage: (msg: TServerMessage) => boolean;
|
|
40
|
+
handleExecutionWorkspace: (snapshot: IExecutionWorkspaceSnapshot) => void;
|
|
41
|
+
} {
|
|
43
42
|
const [openEntryId, setOpenEntryId] = useState<string | null>(null);
|
|
44
43
|
const [status, setStatus] = useState<TExecutionDetailStatus>('idle');
|
|
45
44
|
const [records, setRecords] = useState<readonly IExecutionDetailRecord[]>([]);
|
|
46
45
|
const [error, setError] = useState<string | null>(null);
|
|
47
46
|
const [complete, setComplete] = useState(false);
|
|
47
|
+
const [mode, setMode] = useState<TDetailMode>('live');
|
|
48
|
+
const [unreadable, setUnreadable] = useState<IBackgroundTaskLogUnreadable>();
|
|
48
49
|
const requestIdRef = useRef<string | null>(null);
|
|
49
50
|
const entryIdRef = useRef<string | null>(null);
|
|
51
|
+
const modeRef = useRef<TDetailMode>('live');
|
|
50
52
|
const nextCursorRef = useRef<IExecutionDetailCursor | undefined>(undefined);
|
|
53
|
+
const queuedRef = useRef<{ cursor?: IExecutionDetailCursor; refresh: boolean } | null>(null);
|
|
54
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
55
|
+
const lastReadAtRef = useRef(0);
|
|
51
56
|
|
|
52
57
|
const readPage = useCallback(
|
|
53
|
-
(entryId: string, cursor?: IExecutionDetailCursor): void => {
|
|
58
|
+
(entryId: string, cursor?: IExecutionDetailCursor, refresh = false): void => {
|
|
59
|
+
if (requestIdRef.current !== null) {
|
|
60
|
+
if (!refresh || queuedRef.current === null || queuedRef.current.refresh) {
|
|
61
|
+
queuedRef.current = { cursor, refresh };
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
54
65
|
const requestId = nextRequestId();
|
|
55
66
|
requestIdRef.current = requestId;
|
|
56
|
-
|
|
57
|
-
setStatus('loading');
|
|
67
|
+
lastReadAtRef.current = Date.now();
|
|
68
|
+
if (!refresh) setStatus('loading');
|
|
58
69
|
setError(null);
|
|
59
70
|
send({ type: 'read-execution-detail', requestId, entryId, cursor });
|
|
60
71
|
},
|
|
61
72
|
[send],
|
|
62
73
|
);
|
|
63
74
|
|
|
75
|
+
const clearPending = useCallback(() => {
|
|
76
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
77
|
+
timerRef.current = null;
|
|
78
|
+
queuedRef.current = null;
|
|
79
|
+
}, []);
|
|
80
|
+
useEffect(() => clearPending, [clearPending]);
|
|
81
|
+
|
|
64
82
|
const openExecutionDetail = useCallback(
|
|
65
83
|
(entryId: string): void => {
|
|
84
|
+
clearPending();
|
|
85
|
+
requestIdRef.current = null;
|
|
86
|
+
entryIdRef.current = entryId;
|
|
87
|
+
modeRef.current = 'live';
|
|
88
|
+
setMode('live');
|
|
66
89
|
setOpenEntryId(entryId);
|
|
67
90
|
setRecords([]);
|
|
91
|
+
setUnreadable(undefined);
|
|
68
92
|
setComplete(false);
|
|
69
93
|
nextCursorRef.current = undefined;
|
|
70
94
|
readPage(entryId);
|
|
71
95
|
},
|
|
72
|
-
[readPage],
|
|
96
|
+
[clearPending, readPage],
|
|
73
97
|
);
|
|
74
98
|
|
|
75
99
|
const loadMoreExecutionDetail = useCallback((): void => {
|
|
@@ -77,32 +101,103 @@ export function useExecutionDetailState(
|
|
|
77
101
|
readPage(openEntryId, nextCursorRef.current);
|
|
78
102
|
}, [openEntryId, status, complete, readPage]);
|
|
79
103
|
|
|
104
|
+
const changeMode = useCallback(
|
|
105
|
+
(next: TDetailMode): void => {
|
|
106
|
+
const entryId = entryIdRef.current;
|
|
107
|
+
if (!entryId) return;
|
|
108
|
+
clearPending();
|
|
109
|
+
modeRef.current = next;
|
|
110
|
+
setMode(next);
|
|
111
|
+
setRecords([]);
|
|
112
|
+
setUnreadable(undefined);
|
|
113
|
+
setComplete(false);
|
|
114
|
+
setStatus('loading');
|
|
115
|
+
nextCursorRef.current = undefined;
|
|
116
|
+
readPage(entryId, next === 'transcript' ? { offset: 0 } : undefined);
|
|
117
|
+
},
|
|
118
|
+
[clearPending, readPage],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const showEarlierExecutionDetail = useCallback(() => changeMode('transcript'), [changeMode]);
|
|
122
|
+
const showLiveExecutionDetail = useCallback(() => changeMode('live'), [changeMode]);
|
|
123
|
+
|
|
80
124
|
const closeExecutionDetail = useCallback((): void => {
|
|
125
|
+
clearPending();
|
|
81
126
|
setOpenEntryId(null);
|
|
82
127
|
requestIdRef.current = null;
|
|
83
128
|
entryIdRef.current = null;
|
|
84
129
|
setRecords([]);
|
|
130
|
+
setUnreadable(undefined);
|
|
85
131
|
setStatus('idle');
|
|
86
132
|
setError(null);
|
|
87
133
|
setComplete(false);
|
|
88
|
-
}, []);
|
|
134
|
+
}, [clearPending]);
|
|
135
|
+
|
|
136
|
+
const scheduleRefresh = useCallback(
|
|
137
|
+
(entryId: string): void => {
|
|
138
|
+
if (timerRef.current !== null) return;
|
|
139
|
+
const delay = Math.max(0, 250 - (Date.now() - lastReadAtRef.current));
|
|
140
|
+
timerRef.current = setTimeout(() => {
|
|
141
|
+
timerRef.current = null;
|
|
142
|
+
if (entryIdRef.current === entryId && modeRef.current === 'live') {
|
|
143
|
+
readPage(entryId, undefined, true);
|
|
144
|
+
}
|
|
145
|
+
}, delay);
|
|
146
|
+
},
|
|
147
|
+
[readPage],
|
|
148
|
+
);
|
|
89
149
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
150
|
+
const handleExecutionWorkspace = useCallback(
|
|
151
|
+
(snapshot: IExecutionWorkspaceSnapshot): void => {
|
|
152
|
+
const entryId = entryIdRef.current;
|
|
153
|
+
if (!entryId || modeRef.current !== 'live') return;
|
|
154
|
+
const entry = snapshot.entries.find((item) => item.id === entryId);
|
|
155
|
+
if (entry?.resumeSessionId && entry.controls.includes('attach')) scheduleRefresh(entryId);
|
|
156
|
+
},
|
|
157
|
+
[scheduleRefresh],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handleExecutionDetailMessage = useCallback(
|
|
161
|
+
(msg: TServerMessage): boolean => {
|
|
162
|
+
if (msg.type !== 'execution_detail' && msg.type !== 'execution_detail_error') return false;
|
|
163
|
+
if (requestIdRef.current !== msg.requestId) return true;
|
|
164
|
+
requestIdRef.current = null;
|
|
165
|
+
const queued = queuedRef.current;
|
|
166
|
+
queuedRef.current = null;
|
|
167
|
+
if (!queued || queued.refresh) {
|
|
168
|
+
if (msg.type === 'execution_detail_error') {
|
|
169
|
+
setStatus('error');
|
|
170
|
+
setError(msg.message);
|
|
171
|
+
} else {
|
|
172
|
+
setRecords((previous) =>
|
|
173
|
+
msg.page.cursor === undefined || msg.page.cursor.offset === 0
|
|
174
|
+
? msg.page.records
|
|
175
|
+
: [...previous, ...msg.page.records],
|
|
176
|
+
);
|
|
177
|
+
setUnreadable((previous) => {
|
|
178
|
+
const current = msg.page.unreadable;
|
|
179
|
+
if (msg.page.cursor === undefined || msg.page.cursor.offset === 0) return current;
|
|
180
|
+
if (!previous) return current;
|
|
181
|
+
if (!current) return previous;
|
|
182
|
+
return {
|
|
183
|
+
locked: previous.locked + current.locked,
|
|
184
|
+
noKeys: previous.noKeys + current.noKeys,
|
|
185
|
+
doesNotOpen: previous.doesNotOpen + current.doesNotOpen,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
nextCursorRef.current = msg.page.nextCursor;
|
|
189
|
+
setComplete(msg.page.nextCursor === undefined);
|
|
190
|
+
setStatus('ready');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (queued && entryIdRef.current) {
|
|
194
|
+
if (queued.refresh) scheduleRefresh(entryIdRef.current);
|
|
195
|
+
else readPage(entryIdRef.current, queued.cursor);
|
|
196
|
+
}
|
|
96
197
|
return true;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
101
|
-
nextCursorRef.current = msg.page.nextCursor;
|
|
102
|
-
setComplete(msg.page.nextCursor === undefined);
|
|
103
|
-
setStatus('ready');
|
|
104
|
-
return true;
|
|
105
|
-
}, []);
|
|
198
|
+
},
|
|
199
|
+
[readPage, scheduleRefresh],
|
|
200
|
+
);
|
|
106
201
|
|
|
107
202
|
return {
|
|
108
203
|
openEntryId,
|
|
@@ -110,9 +205,14 @@ export function useExecutionDetailState(
|
|
|
110
205
|
executionDetailRecords: records,
|
|
111
206
|
executionDetailError: error,
|
|
112
207
|
executionDetailComplete: complete,
|
|
208
|
+
executionDetailMode: mode,
|
|
209
|
+
executionDetailUnreadable: unreadable,
|
|
113
210
|
openExecutionDetail,
|
|
114
211
|
loadMoreExecutionDetail,
|
|
212
|
+
showEarlierExecutionDetail,
|
|
213
|
+
showLiveExecutionDetail,
|
|
115
214
|
closeExecutionDetail,
|
|
116
215
|
handleExecutionDetailMessage,
|
|
216
|
+
handleExecutionWorkspace,
|
|
117
217
|
};
|
|
118
218
|
}
|
|
@@ -368,8 +368,18 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
368
368
|
// Their request ids, for this connection: a refusal names the request it answers. A switch
|
|
369
369
|
// answers with no id, so an id that succeeded stays here until the connection is replaced.
|
|
370
370
|
const sessionChangeRequestIdsRef = useRef(new Set<string>());
|
|
371
|
+
const [executionDetailInput, setExecutionDetailInput] = useState<{
|
|
372
|
+
taskId: string;
|
|
373
|
+
status: 'pending' | 'sent' | 'failed';
|
|
374
|
+
}>();
|
|
375
|
+
const executionDetailInputRef = useRef<string | null>(null);
|
|
371
376
|
const send = useCallback(
|
|
372
377
|
(msg: TClientMessage): void => {
|
|
378
|
+
if (msg.type === 'send-background-task') {
|
|
379
|
+
if (executionDetailInputRef.current !== null) return;
|
|
380
|
+
executionDetailInputRef.current = msg.taskId;
|
|
381
|
+
setExecutionDetailInput({ taskId: msg.taskId, status: 'pending' });
|
|
382
|
+
}
|
|
373
383
|
if (msg.type === 'command') {
|
|
374
384
|
// #3282 §4e: `/help` is the GUI's own Help sheet — never sent to the session.
|
|
375
385
|
if (msg.name === 'help') {
|
|
@@ -433,8 +443,12 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
433
443
|
[send],
|
|
434
444
|
);
|
|
435
445
|
const { handleUsageMessage, ...personalUsageState } = usePersonalUsageState(send);
|
|
436
|
-
const {
|
|
437
|
-
|
|
446
|
+
const {
|
|
447
|
+
handleExecutionDetailMessage,
|
|
448
|
+
handleExecutionWorkspace,
|
|
449
|
+
closeExecutionDetail,
|
|
450
|
+
...executionDetailState
|
|
451
|
+
} = useExecutionDetailState(send);
|
|
438
452
|
const { handleModelListMessage, ...modelListState } = useModelListState(send);
|
|
439
453
|
const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
|
|
440
454
|
const isConnected = useCallback((): boolean => statusRef.current === 'connected', []);
|
|
@@ -465,7 +479,9 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
465
479
|
const { handleSchedulesMessage, requestSchedules, ...schedulesState } = useSchedulesState(send);
|
|
466
480
|
|
|
467
481
|
const clearCompactionNotices = useCallback((): void => {
|
|
468
|
-
setSessionNotices((previous) =>
|
|
482
|
+
setSessionNotices((previous) =>
|
|
483
|
+
previous.filter((notice) => notice.kind !== 'context-compacted'),
|
|
484
|
+
);
|
|
469
485
|
}, []);
|
|
470
486
|
|
|
471
487
|
const handleMessage = useCallback(
|
|
@@ -552,19 +568,29 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
552
568
|
break;
|
|
553
569
|
}
|
|
554
570
|
case 'execution_workspace_event': {
|
|
571
|
+
handleExecutionWorkspace(msg.snapshot);
|
|
555
572
|
setExecutionWorkspace(msg.snapshot);
|
|
556
573
|
break;
|
|
557
574
|
}
|
|
558
575
|
// #3288 §1: a Stop that did not take effect (e.g. the task/loop had already finished) is
|
|
559
576
|
// worth telling the operator; a success shows itself through the snapshot update above.
|
|
560
577
|
case 'background_task_control_result': {
|
|
578
|
+
if (msg.action === 'send' && executionDetailInputRef.current === msg.taskId) {
|
|
579
|
+
executionDetailInputRef.current = null;
|
|
580
|
+
setExecutionDetailInput({
|
|
581
|
+
taskId: msg.taskId,
|
|
582
|
+
status: msg.success ? 'sent' : 'failed',
|
|
583
|
+
});
|
|
584
|
+
}
|
|
561
585
|
if (!msg.success) {
|
|
562
586
|
setSessionNotices((previous) => [
|
|
563
587
|
...previous,
|
|
564
588
|
{
|
|
565
589
|
id: nextId(),
|
|
566
590
|
kind: 'background-task-control-failed',
|
|
567
|
-
message:
|
|
591
|
+
message:
|
|
592
|
+
msg.message ??
|
|
593
|
+
`Could not ${msg.action === 'send' ? 'send to' : 'stop'} ${msg.taskId}.`,
|
|
568
594
|
},
|
|
569
595
|
]);
|
|
570
596
|
}
|
|
@@ -765,6 +791,13 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
765
791
|
break;
|
|
766
792
|
}
|
|
767
793
|
case 'protocol_error': {
|
|
794
|
+
if (
|
|
795
|
+
executionDetailInputRef.current !== null &&
|
|
796
|
+
msg.message === 'Not permitted for an observer: send-background-task'
|
|
797
|
+
) {
|
|
798
|
+
setExecutionDetailInput({ taskId: executionDetailInputRef.current, status: 'failed' });
|
|
799
|
+
executionDetailInputRef.current = null;
|
|
800
|
+
}
|
|
768
801
|
if (sessionChangesInFlightRef.current > 0) {
|
|
769
802
|
// A host older than `session_change_failed` refuses a session change with this.
|
|
770
803
|
settleSessionChange();
|
|
@@ -911,6 +944,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
911
944
|
handleAgentDefinitionsMessage,
|
|
912
945
|
handleAppViewMessage,
|
|
913
946
|
handleExecutionDetailMessage,
|
|
947
|
+
handleExecutionWorkspace,
|
|
914
948
|
handleModelListMessage,
|
|
915
949
|
handleProjectMessage,
|
|
916
950
|
handleSchedulesMessage,
|
|
@@ -954,6 +988,11 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
954
988
|
if (next !== 'connected') {
|
|
955
989
|
rejectPendingAppRequests();
|
|
956
990
|
clearCompactionNotices();
|
|
991
|
+
closeExecutionDetail();
|
|
992
|
+
if (executionDetailInputRef.current !== null) {
|
|
993
|
+
setExecutionDetailInput({ taskId: executionDetailInputRef.current, status: 'failed' });
|
|
994
|
+
executionDetailInputRef.current = null;
|
|
995
|
+
}
|
|
957
996
|
}
|
|
958
997
|
// Every client learns what it can offer and what is in effect as soon as it is attached.
|
|
959
998
|
if (next === 'connected') {
|
|
@@ -988,6 +1027,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
988
1027
|
makeClient,
|
|
989
1028
|
handleMessage,
|
|
990
1029
|
clearCompactionNotices,
|
|
1030
|
+
closeExecutionDetail,
|
|
991
1031
|
requestSessions,
|
|
992
1032
|
requestSchedules,
|
|
993
1033
|
armRestore,
|
|
@@ -1013,6 +1053,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
1013
1053
|
answerAsk,
|
|
1014
1054
|
...personalUsageState,
|
|
1015
1055
|
...executionDetailState,
|
|
1056
|
+
executionDetailInput,
|
|
1016
1057
|
closeExecutionDetail,
|
|
1017
1058
|
...modelListState,
|
|
1018
1059
|
...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 {
|