@robota-sdk/agent-ui-web 3.0.0-beta.95 → 3.0.0-beta.96
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 +15 -0
- package/dist/browser/index.d.ts +18 -3
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +552 -55
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +551 -54
- package/dist/node/index.d.cts +18 -3
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +18 -3
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +552 -55
- package/dist/node/index.js.map +1 -1
- package/docs/SPEC.md +7 -2
- package/package.json +6 -6
- package/src/app-view/bridge.ts +217 -0
- package/src/app-view/call-tool-result.ts +58 -0
- package/src/app-view/host-context.ts +37 -0
- package/src/app-view/types.ts +12 -0
- package/src/components/AppView.tsx +147 -0
- package/src/components/ConversationView.tsx +18 -1
- package/src/components/SessionSurface.tsx +39 -12
- package/src/hooks/server-message-handling.ts +3 -4
- package/src/hooks/session-client-types.ts +14 -0
- package/src/hooks/use-app-view-requests.ts +178 -0
- package/src/hooks/useSessionClient.ts +28 -2
- package/src/index.ts +7 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { TAppToolArguments, TAppToolOutcome, TAppView } from '../app-view/types.js';
|
|
1
2
|
import type { TConnectionStatus, TClientMessage } from '../client/ws-session-client.js';
|
|
2
3
|
import type { TPendingPrompt } from './prompt-state.js';
|
|
3
4
|
import type { TActionResponse } from '@robota-sdk/agent-interface-transport';
|
|
4
5
|
import type {
|
|
5
6
|
IDiffLine,
|
|
7
|
+
IToolAppViewRef,
|
|
6
8
|
IToolState,
|
|
7
9
|
ISettingsSnapshot,
|
|
8
10
|
TDriverId,
|
|
@@ -16,6 +18,8 @@ import type {
|
|
|
16
18
|
} from '@robota-sdk/agent-interface-execution';
|
|
17
19
|
import type { IWireAgentDefinitionSummary, TServerMessage } from '@robota-sdk/agent-transport';
|
|
18
20
|
|
|
21
|
+
export type { TAppToolArguments, TAppToolOutcome, TAppView } from '../app-view/types.js';
|
|
22
|
+
|
|
19
23
|
export type TPersonalUsageReport = Extract<
|
|
20
24
|
TServerMessage,
|
|
21
25
|
{ type: 'personal_usage_report' }
|
|
@@ -125,6 +129,8 @@ export interface IActiveTool {
|
|
|
125
129
|
commandName?: string;
|
|
126
130
|
/** #3288: true for an internal signal tool (e.g. goal-status) — never rendered as a call. */
|
|
127
131
|
internal?: boolean;
|
|
132
|
+
|
|
133
|
+
appView?: IToolAppViewRef;
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
/**
|
|
@@ -326,4 +332,12 @@ export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
|
326
332
|
* session notice) — no separate error state here.
|
|
327
333
|
*/
|
|
328
334
|
deleteSchedule: (taskId: string) => void;
|
|
335
|
+
|
|
336
|
+
readAppView: (viewKey: string, signal?: AbortSignal) => Promise<TAppView>;
|
|
337
|
+
callAppTool: (
|
|
338
|
+
viewKey: string,
|
|
339
|
+
name: string,
|
|
340
|
+
args: TAppToolArguments,
|
|
341
|
+
signal?: AbortSignal,
|
|
342
|
+
) => Promise<TAppToolOutcome>;
|
|
329
343
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { TClientMessage } from '../client/ws-session-client.js';
|
|
4
|
+
import type { TAppToolArguments, TAppToolOutcome, TAppView } from '../app-view/types.js';
|
|
5
|
+
import type { IActiveTool } from './session-client-types.js';
|
|
6
|
+
import type { TServerMessage } from '@robota-sdk/agent-transport';
|
|
7
|
+
|
|
8
|
+
type TPendingRequest = {
|
|
9
|
+
reject: (error: Error) => void;
|
|
10
|
+
dispose: () => void;
|
|
11
|
+
} & (
|
|
12
|
+
| { answer: 'app_view'; resolve: (value: TAppView) => void }
|
|
13
|
+
| { answer: 'app_tool_result'; resolve: (value: TAppToolOutcome) => void }
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
let requestCounter = 0;
|
|
17
|
+
function nextRequestId(prefix: string): string {
|
|
18
|
+
requestCounter += 1;
|
|
19
|
+
return `${prefix}_${requestCounter}_${Date.now()}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CONNECTION_CLOSED = 'The connection to the session closed.';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The key a finished call's app view is read and called by, taken from the tool row's `appView`
|
|
26
|
+
* reference; `undefined` when the row gives none. Everything outside this module treats it as an
|
|
27
|
+
* opaque string — it is read here and put on the two wire messages below, nowhere else.
|
|
28
|
+
*/
|
|
29
|
+
export function appViewKey(tool: IActiveTool): string | undefined {
|
|
30
|
+
return tool.appView?.viewId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readAppViewMessage(requestId: string, viewKey: string): TClientMessage {
|
|
34
|
+
return { type: 'read-app-view', requestId, viewId: viewKey };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function callAppToolMessage(
|
|
38
|
+
requestId: string,
|
|
39
|
+
viewKey: string,
|
|
40
|
+
name: string,
|
|
41
|
+
args: TAppToolArguments,
|
|
42
|
+
): TClientMessage {
|
|
43
|
+
return { type: 'call-app-tool', requestId, viewId: viewKey, name, arguments: args };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The app-view host's two requests to the session, each a promise settled by the reply that echoes
|
|
48
|
+
* its `requestId` (`app_view` / `app_tool_result`, or a `protocol_error` naming it). Unlike the
|
|
49
|
+
* panels' latest-wins state, several can be outstanding at once (one per frame on screen), so each
|
|
50
|
+
* keeps its own entry. A reply that lost its connection never arrives: the owner of the connection
|
|
51
|
+
* rejects every outstanding request when it drops, is replaced or goes away (`rejectPendingAppRequests`).
|
|
52
|
+
*/
|
|
53
|
+
export function useAppViewRequests(
|
|
54
|
+
send: (msg: TClientMessage) => void,
|
|
55
|
+
isConnected: () => boolean,
|
|
56
|
+
): {
|
|
57
|
+
readAppView: (viewKey: string, signal?: AbortSignal) => Promise<TAppView>;
|
|
58
|
+
callAppTool: (
|
|
59
|
+
viewKey: string,
|
|
60
|
+
name: string,
|
|
61
|
+
args: TAppToolArguments,
|
|
62
|
+
signal?: AbortSignal,
|
|
63
|
+
) => Promise<TAppToolOutcome>;
|
|
64
|
+
handleAppViewMessage: (msg: TServerMessage) => boolean;
|
|
65
|
+
rejectPendingAppRequests: (reason?: string) => void;
|
|
66
|
+
} {
|
|
67
|
+
const pendingRef = useRef(new Map<string, TPendingRequest>());
|
|
68
|
+
const [requestOwner] = useState(() => nextRequestId('app_request'));
|
|
69
|
+
|
|
70
|
+
const rejectPendingAppRequests = useCallback((reason: string = CONNECTION_CLOSED): void => {
|
|
71
|
+
const pending = [...pendingRef.current.values()];
|
|
72
|
+
pendingRef.current.clear();
|
|
73
|
+
for (const request of pending) {
|
|
74
|
+
request.dispose();
|
|
75
|
+
request.reject(new Error(reason));
|
|
76
|
+
}
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const request = useCallback(
|
|
80
|
+
<T extends TAppView | TAppToolOutcome>(
|
|
81
|
+
prefix: string,
|
|
82
|
+
message: (requestId: string) => TClientMessage,
|
|
83
|
+
pending: (
|
|
84
|
+
resolve: (value: T) => void,
|
|
85
|
+
reject: (error: Error) => void,
|
|
86
|
+
dispose: () => void,
|
|
87
|
+
) => TPendingRequest,
|
|
88
|
+
signal?: AbortSignal,
|
|
89
|
+
): Promise<T> =>
|
|
90
|
+
new Promise<T>((resolve, reject) => {
|
|
91
|
+
if (!isConnected() || signal?.aborted) {
|
|
92
|
+
reject(new Error(signal?.aborted ? 'The app view closed.' : CONNECTION_CLOSED));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const requestId = nextRequestId(`${requestOwner}_${prefix}`);
|
|
96
|
+
const dispose = (): void => signal?.removeEventListener('abort', abort);
|
|
97
|
+
const abort = (): void => {
|
|
98
|
+
pendingRef.current.delete(requestId);
|
|
99
|
+
dispose();
|
|
100
|
+
reject(new Error('The app view closed.'));
|
|
101
|
+
};
|
|
102
|
+
pendingRef.current.set(requestId, pending(resolve, reject, dispose));
|
|
103
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
104
|
+
try {
|
|
105
|
+
send(message(requestId));
|
|
106
|
+
} catch (error: unknown) {
|
|
107
|
+
pendingRef.current.delete(requestId);
|
|
108
|
+
dispose();
|
|
109
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
[isConnected, requestOwner, send],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const readAppView = useCallback(
|
|
116
|
+
(viewKey: string, signal?: AbortSignal): Promise<TAppView> =>
|
|
117
|
+
request<TAppView>(
|
|
118
|
+
'app_view',
|
|
119
|
+
(requestId) => readAppViewMessage(requestId, viewKey),
|
|
120
|
+
(resolve, reject, dispose) => ({ answer: 'app_view', resolve, reject, dispose }),
|
|
121
|
+
signal,
|
|
122
|
+
),
|
|
123
|
+
[request],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const callAppTool = useCallback(
|
|
127
|
+
(
|
|
128
|
+
viewKey: string,
|
|
129
|
+
name: string,
|
|
130
|
+
args: TAppToolArguments,
|
|
131
|
+
signal?: AbortSignal,
|
|
132
|
+
): Promise<TAppToolOutcome> =>
|
|
133
|
+
request<TAppToolOutcome>(
|
|
134
|
+
'app_tool',
|
|
135
|
+
(requestId) => callAppToolMessage(requestId, viewKey, name, args),
|
|
136
|
+
(resolve, reject, dispose) => ({ answer: 'app_tool_result', resolve, reject, dispose }),
|
|
137
|
+
signal,
|
|
138
|
+
),
|
|
139
|
+
[request],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
/** True when the message answered one of this hook's requests; every other message is not its own. */
|
|
143
|
+
const handleAppViewMessage = useCallback(
|
|
144
|
+
(msg: TServerMessage): boolean => {
|
|
145
|
+
if (msg.type === 'app_view' || msg.type === 'app_tool_result') {
|
|
146
|
+
const pending = pendingRef.current.get(msg.requestId);
|
|
147
|
+
// A reply to a request that already settled (or never was ours) matches nothing.
|
|
148
|
+
if (pending === undefined || pending.answer !== msg.type) return true;
|
|
149
|
+
pendingRef.current.delete(msg.requestId);
|
|
150
|
+
pending.dispose();
|
|
151
|
+
if (msg.type === 'app_view' && pending.answer === 'app_view') {
|
|
152
|
+
pending.resolve(msg.view);
|
|
153
|
+
} else if (msg.type === 'app_tool_result' && pending.answer === 'app_tool_result') {
|
|
154
|
+
const { success, data, parts, error } = msg;
|
|
155
|
+
pending.resolve({
|
|
156
|
+
success,
|
|
157
|
+
...(data !== undefined ? { data } : {}),
|
|
158
|
+
...(parts !== undefined ? { parts } : {}),
|
|
159
|
+
...(error !== undefined ? { error } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (msg.type === 'protocol_error' && msg.requestId !== undefined) {
|
|
165
|
+
const pending = pendingRef.current.get(msg.requestId);
|
|
166
|
+
if (pending === undefined) return msg.requestId.startsWith(`${requestOwner}_`);
|
|
167
|
+
pendingRef.current.delete(msg.requestId);
|
|
168
|
+
pending.dispose();
|
|
169
|
+
pending.reject(new Error(msg.message));
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
},
|
|
174
|
+
[requestOwner],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return { readAppView, callAppTool, handleAppViewMessage, rejectPendingAppRequests };
|
|
178
|
+
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { createWsSessionClient } from '../client/ws-session-client.js';
|
|
24
24
|
import { SERVER_MESSAGE_HANDLING } from './server-message-handling.js';
|
|
25
25
|
import { useAgentSwitcherState } from './use-agent-switcher-state.js';
|
|
26
|
+
import { useAppViewRequests } from './use-app-view-requests.js';
|
|
26
27
|
import { useExecutionDetailState } from './use-execution-detail.js';
|
|
27
28
|
import { useModelListState } from './use-model-list.js';
|
|
28
29
|
import { usePersonalUsageState } from './use-personal-usage.js';
|
|
@@ -153,7 +154,7 @@ function toolEndFields(
|
|
|
153
154
|
state: IToolState,
|
|
154
155
|
): Pick<
|
|
155
156
|
IActiveTool,
|
|
156
|
-
'status' | 'result' | 'diffLines' | 'diffFile' | 'toolResultData' | 'toolResultParts'
|
|
157
|
+
'status' | 'result' | 'diffLines' | 'diffFile' | 'toolResultData' | 'toolResultParts' | 'appView'
|
|
157
158
|
> {
|
|
158
159
|
return {
|
|
159
160
|
// A tool that finished with an error result renders failed (ToolCard/ToolGroup key off
|
|
@@ -168,6 +169,7 @@ function toolEndFields(
|
|
|
168
169
|
...(state.diffFile ? { diffFile: state.diffFile } : {}),
|
|
169
170
|
...(state.toolResultData !== undefined ? { toolResultData: state.toolResultData } : {}),
|
|
170
171
|
...(state.toolResultParts ? { toolResultParts: state.toolResultParts } : {}),
|
|
172
|
+
...(state.appView ? { appView: state.appView } : {}),
|
|
171
173
|
};
|
|
172
174
|
}
|
|
173
175
|
|
|
@@ -268,6 +270,8 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
268
270
|
makeClient: TMakeSessionClient<TStatus>,
|
|
269
271
|
): IWsSessionState<TStatus> {
|
|
270
272
|
const [status, setStatus] = useState<TStatus>('disconnected' as TStatus);
|
|
273
|
+
// The app-view requests read this to refuse at once when no connection exists to answer them.
|
|
274
|
+
const statusRef = useRef<TStatus>('disconnected' as TStatus);
|
|
271
275
|
const [messages, setMessages] = useState<TConversationEntry[]>([]);
|
|
272
276
|
const [activeTools, setActiveTools] = useState<IActiveTool[]>([]);
|
|
273
277
|
const [streamingText, setStreamingText] = useState('');
|
|
@@ -433,6 +437,9 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
433
437
|
useExecutionDetailState(send);
|
|
434
438
|
const { handleModelListMessage, ...modelListState } = useModelListState(send);
|
|
435
439
|
const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
|
|
440
|
+
const isConnected = useCallback((): boolean => statusRef.current === 'connected', []);
|
|
441
|
+
const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } =
|
|
442
|
+
useAppViewRequests(send, isConnected);
|
|
436
443
|
const {
|
|
437
444
|
handleSessionsMessage,
|
|
438
445
|
canListSessions,
|
|
@@ -466,6 +473,8 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
466
473
|
if (handleExecutionDetailMessage(msg)) return;
|
|
467
474
|
if (handleModelListMessage(msg)) return;
|
|
468
475
|
if (handleProjectMessage(msg)) return;
|
|
476
|
+
// A reply to a request an app view's host made (the view's document, or a tool it called).
|
|
477
|
+
if (handleAppViewMessage(msg)) return;
|
|
469
478
|
if (handleSessionsMessage(msg)) return;
|
|
470
479
|
if (handleSettingsMessage(msg)) return;
|
|
471
480
|
if (handleAgentDefinitionsMessage(msg)) return;
|
|
@@ -649,6 +658,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
649
658
|
}
|
|
650
659
|
// #3189: the host made another session current — drop what this one showed, re-read it all.
|
|
651
660
|
case 'session_switched': {
|
|
661
|
+
rejectPendingAppRequests('The active session changed.');
|
|
652
662
|
settleSessionChange();
|
|
653
663
|
// Nothing of the old session stays on screen until the new one's answers arrive.
|
|
654
664
|
sessionStatusRef.current = null;
|
|
@@ -675,6 +685,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
675
685
|
break;
|
|
676
686
|
}
|
|
677
687
|
case 'history_cleared': {
|
|
688
|
+
rejectPendingAppRequests('The session history was cleared.');
|
|
678
689
|
streamingTextRef.current = '';
|
|
679
690
|
turnSegmentsRef.current = [];
|
|
680
691
|
setStreamingText('');
|
|
@@ -868,6 +879,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
868
879
|
closeExecutionDetail,
|
|
869
880
|
finishTurn,
|
|
870
881
|
handleAgentDefinitionsMessage,
|
|
882
|
+
handleAppViewMessage,
|
|
871
883
|
handleExecutionDetailMessage,
|
|
872
884
|
handleModelListMessage,
|
|
873
885
|
handleProjectMessage,
|
|
@@ -882,6 +894,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
882
894
|
requestSchedules,
|
|
883
895
|
requestSessions,
|
|
884
896
|
resolveSwitchResult,
|
|
897
|
+
rejectPendingAppRequests,
|
|
885
898
|
send,
|
|
886
899
|
setSessionSidebarOpen,
|
|
887
900
|
settleSessionChange,
|
|
@@ -905,7 +918,10 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
905
918
|
|
|
906
919
|
useEffect(() => {
|
|
907
920
|
const onStatusChange = (next: TStatus): void => {
|
|
921
|
+
statusRef.current = next;
|
|
908
922
|
setStatus(next);
|
|
923
|
+
// A reply lost with the connection never arrives; the views waiting on one learn that now.
|
|
924
|
+
if (next !== 'connected') rejectPendingAppRequests();
|
|
909
925
|
// Every client learns what it can offer and what is in effect as soon as it is attached.
|
|
910
926
|
if (next === 'connected') {
|
|
911
927
|
// A reply lost with the old connection never arrives; stop waiting for it.
|
|
@@ -933,8 +949,16 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
933
949
|
return () => {
|
|
934
950
|
client.disconnect();
|
|
935
951
|
clientRef.current = null;
|
|
952
|
+
rejectPendingAppRequests();
|
|
936
953
|
};
|
|
937
|
-
}, [
|
|
954
|
+
}, [
|
|
955
|
+
makeClient,
|
|
956
|
+
handleMessage,
|
|
957
|
+
requestSessions,
|
|
958
|
+
requestSchedules,
|
|
959
|
+
armRestore,
|
|
960
|
+
rejectPendingAppRequests,
|
|
961
|
+
]);
|
|
938
962
|
|
|
939
963
|
return {
|
|
940
964
|
status,
|
|
@@ -966,6 +990,8 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
966
990
|
openAgentSwitcher,
|
|
967
991
|
...agentSwitcherState,
|
|
968
992
|
...schedulesState,
|
|
993
|
+
readAppView,
|
|
994
|
+
callAppTool,
|
|
969
995
|
helpOpen,
|
|
970
996
|
openHelp,
|
|
971
997
|
closeHelp,
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,13 @@ export type {
|
|
|
16
16
|
ISessionClientHandle,
|
|
17
17
|
TMakeSessionClient,
|
|
18
18
|
} from './hooks/useSessionClient.js';
|
|
19
|
-
export type {
|
|
19
|
+
export type {
|
|
20
|
+
TAppToolArguments,
|
|
21
|
+
TAppToolOutcome,
|
|
22
|
+
TAppView,
|
|
23
|
+
TSessionListing,
|
|
24
|
+
TSessionsError,
|
|
25
|
+
} from './hooks/session-client-types.js';
|
|
20
26
|
// Issue #3280 §5: a host that can restart the runtime calls this before reloading the page, so the
|
|
21
27
|
// first session listing after reconnecting can switch back to the session the person was in.
|
|
22
28
|
export { rememberSessionForRestore } from './hooks/use-session-directory.js';
|