@robota-sdk/agent-ui-web 3.0.0-beta.95 → 3.0.0-beta.99
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 +45 -0
- package/dist/browser/index.d.ts +19 -4
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +587 -60
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +586 -59
- package/dist/node/index.d.cts +19 -4
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +19 -4
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +587 -60
- 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/components/SessionSurfaceChrome.tsx +6 -3
- package/src/hooks/server-message-handling.ts +7 -8
- package/src/hooks/session-client-types.ts +16 -1
- package/src/hooks/use-app-view-requests.ts +178 -0
- package/src/hooks/useSessionClient.ts +62 -2
- package/src/index.ts +7 -1
|
@@ -268,7 +268,7 @@ function noticeSentence(notice: ISessionNotice): string | null {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
/**
|
|
271
|
-
* Session and protocol failures, as dismissible toasts over the top-right corner: they stay until
|
|
271
|
+
* Session outcomes and protocol failures, as dismissible toasts over the top-right corner: they stay until
|
|
272
272
|
* dismissed, and never push the conversation or the composer out of view. Command output is not
|
|
273
273
|
* here — it belongs to the conversation. A provider error classified at the wire boundary shows one
|
|
274
274
|
* plain sentence with the next step; its raw text sits behind "Details" rather than as the headline.
|
|
@@ -280,13 +280,16 @@ export function SessionNotices({ state }: { state: IWsSessionState }): React.Rea
|
|
|
280
280
|
<div className="pointer-events-none absolute right-4 top-16 z-40 flex w-[min(400px,calc(100%-32px))] flex-col gap-2">
|
|
281
281
|
{notices.slice(-3).map((notice) => {
|
|
282
282
|
const sentence = noticeSentence(notice);
|
|
283
|
+
const compacted = notice.kind === 'context-compacted';
|
|
283
284
|
return (
|
|
284
285
|
<div
|
|
285
286
|
key={notice.id}
|
|
286
|
-
role=
|
|
287
|
+
role={compacted ? 'status' : 'alert'}
|
|
287
288
|
className="gui-rise pointer-events-auto flex items-start gap-3 rounded-xl bg-popover px-4 py-3 text-[14px] leading-snug text-popover-foreground shadow-xl shadow-black/30"
|
|
288
289
|
>
|
|
289
|
-
<span
|
|
290
|
+
<span
|
|
291
|
+
className={`mt-[7px] h-2 w-2 flex-shrink-0 rounded-full ${compacted ? 'bg-primary' : 'bg-destructive'}`}
|
|
292
|
+
/>
|
|
290
293
|
<span className="max-h-40 flex-1 overflow-y-auto whitespace-pre-wrap break-words">
|
|
291
294
|
{sentence ?? notice.message}
|
|
292
295
|
{sentence && (
|
|
@@ -20,11 +20,11 @@ export const SERVER_MESSAGE_HANDLING = {
|
|
|
20
20
|
error: 'visible-notice',
|
|
21
21
|
command_result: 'reducer-state',
|
|
22
22
|
messages: 'reducer-state',
|
|
23
|
-
//
|
|
24
|
-
//
|
|
23
|
+
// The full system history serves the TUI; this transcript uses `messages` and streamed turns.
|
|
24
|
+
// A history change carries optional compaction metadata for a notice without replacing the turn.
|
|
25
25
|
history: 'intentionally-not-rendered',
|
|
26
|
-
history_changed: '
|
|
27
|
-
context: '
|
|
26
|
+
history_changed: 'visible-notice',
|
|
27
|
+
context: 'reducer-state',
|
|
28
28
|
turn_source: 'intentionally-not-rendered',
|
|
29
29
|
commands: 'reducer-state',
|
|
30
30
|
session_status: 'reducer-state',
|
|
@@ -62,10 +62,9 @@ export const SERVER_MESSAGE_HANDLING = {
|
|
|
62
62
|
project_status: 'reducer-state',
|
|
63
63
|
project_diff: 'reducer-state',
|
|
64
64
|
project_memory: 'reducer-state',
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
app_tool_result: 'intentionally-not-rendered',
|
|
65
|
+
|
|
66
|
+
app_view: 'reducer-state',
|
|
67
|
+
app_tool_result: 'reducer-state',
|
|
69
68
|
background_task_event: 'intentionally-not-rendered',
|
|
70
69
|
background_job_group_event: 'intentionally-not-rendered',
|
|
71
70
|
plan_event: 'intentionally-not-rendered',
|
|
@@ -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
|
/**
|
|
@@ -151,7 +157,8 @@ export interface ISessionNotice {
|
|
|
151
157
|
| 'protocol-error'
|
|
152
158
|
| 'session-change-refused'
|
|
153
159
|
| 'background-task-control-failed'
|
|
154
|
-
| 'command-failed'
|
|
160
|
+
| 'command-failed'
|
|
161
|
+
| 'context-compacted';
|
|
155
162
|
message: string;
|
|
156
163
|
/**
|
|
157
164
|
* A `session-error` classified at the wire boundary (#3289 §3) — present only when the host could
|
|
@@ -326,4 +333,12 @@ export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
|
326
333
|
* session notice) — no separate error state here.
|
|
327
334
|
*/
|
|
328
335
|
deleteSchedule: (taskId: string) => void;
|
|
336
|
+
|
|
337
|
+
readAppView: (viewKey: string, signal?: AbortSignal) => Promise<TAppView>;
|
|
338
|
+
callAppTool: (
|
|
339
|
+
viewKey: string,
|
|
340
|
+
name: string,
|
|
341
|
+
args: TAppToolArguments,
|
|
342
|
+
signal?: AbortSignal,
|
|
343
|
+
) => Promise<TAppToolOutcome>;
|
|
329
344
|
}
|
|
@@ -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,
|
|
@@ -457,6 +464,10 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
457
464
|
} = useAgentSwitcherState(send);
|
|
458
465
|
const { handleSchedulesMessage, requestSchedules, ...schedulesState } = useSchedulesState(send);
|
|
459
466
|
|
|
467
|
+
const clearCompactionNotices = useCallback((): void => {
|
|
468
|
+
setSessionNotices((previous) => previous.filter((notice) => notice.kind !== 'context-compacted'));
|
|
469
|
+
}, []);
|
|
470
|
+
|
|
460
471
|
const handleMessage = useCallback(
|
|
461
472
|
(msg: TServerMessage): void => {
|
|
462
473
|
// ARCH-2164: touching the exhaustive registry here keeps every decoded variant tied to an
|
|
@@ -466,6 +477,8 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
466
477
|
if (handleExecutionDetailMessage(msg)) return;
|
|
467
478
|
if (handleModelListMessage(msg)) return;
|
|
468
479
|
if (handleProjectMessage(msg)) return;
|
|
480
|
+
// A reply to a request an app view's host made (the view's document, or a tool it called).
|
|
481
|
+
if (handleAppViewMessage(msg)) return;
|
|
469
482
|
if (handleSessionsMessage(msg)) return;
|
|
470
483
|
if (handleSettingsMessage(msg)) return;
|
|
471
484
|
if (handleAgentDefinitionsMessage(msg)) return;
|
|
@@ -649,10 +662,12 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
649
662
|
}
|
|
650
663
|
// #3189: the host made another session current — drop what this one showed, re-read it all.
|
|
651
664
|
case 'session_switched': {
|
|
665
|
+
rejectPendingAppRequests('The active session changed.');
|
|
652
666
|
settleSessionChange();
|
|
653
667
|
// Nothing of the old session stays on screen until the new one's answers arrive.
|
|
654
668
|
sessionStatusRef.current = null;
|
|
655
669
|
setSessionStatus(null);
|
|
670
|
+
clearCompactionNotices();
|
|
656
671
|
streamingTextRef.current = '';
|
|
657
672
|
turnSegmentsRef.current = [];
|
|
658
673
|
setStreamingText('');
|
|
@@ -675,6 +690,8 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
675
690
|
break;
|
|
676
691
|
}
|
|
677
692
|
case 'history_cleared': {
|
|
693
|
+
clearCompactionNotices();
|
|
694
|
+
rejectPendingAppRequests('The session history was cleared.');
|
|
678
695
|
streamingTextRef.current = '';
|
|
679
696
|
turnSegmentsRef.current = [];
|
|
680
697
|
setStreamingText('');
|
|
@@ -782,6 +799,29 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
782
799
|
setCommandCatalog(catalog);
|
|
783
800
|
break;
|
|
784
801
|
}
|
|
802
|
+
case 'context': {
|
|
803
|
+
// A later status snapshot supplies identity and the latest context when none is known yet.
|
|
804
|
+
if (sessionStatusRef.current !== null) {
|
|
805
|
+
const next = { ...sessionStatusRef.current, context: msg.state };
|
|
806
|
+
sessionStatusRef.current = next;
|
|
807
|
+
setSessionStatus(next);
|
|
808
|
+
}
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case 'history_changed': {
|
|
812
|
+
const compact = msg.compact;
|
|
813
|
+
if (compact?.trigger === 'auto') {
|
|
814
|
+
setSessionNotices((previous) => [
|
|
815
|
+
...previous,
|
|
816
|
+
{
|
|
817
|
+
id: nextId(),
|
|
818
|
+
kind: 'context-compacted',
|
|
819
|
+
message: `Auto compacted context: ${Math.round(compact.beforeUsedPercentage)}% → ${Math.round(compact.afterUsedPercentage)}%`,
|
|
820
|
+
},
|
|
821
|
+
]);
|
|
822
|
+
}
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
785
825
|
case 'session_status': {
|
|
786
826
|
sessionStatusRef.current = msg.status;
|
|
787
827
|
setSessionStatus(msg.status);
|
|
@@ -866,8 +906,10 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
866
906
|
appendEntry,
|
|
867
907
|
canListSessions,
|
|
868
908
|
closeExecutionDetail,
|
|
909
|
+
clearCompactionNotices,
|
|
869
910
|
finishTurn,
|
|
870
911
|
handleAgentDefinitionsMessage,
|
|
912
|
+
handleAppViewMessage,
|
|
871
913
|
handleExecutionDetailMessage,
|
|
872
914
|
handleModelListMessage,
|
|
873
915
|
handleProjectMessage,
|
|
@@ -882,6 +924,7 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
882
924
|
requestSchedules,
|
|
883
925
|
requestSessions,
|
|
884
926
|
resolveSwitchResult,
|
|
927
|
+
rejectPendingAppRequests,
|
|
885
928
|
send,
|
|
886
929
|
setSessionSidebarOpen,
|
|
887
930
|
settleSessionChange,
|
|
@@ -905,7 +948,13 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
905
948
|
|
|
906
949
|
useEffect(() => {
|
|
907
950
|
const onStatusChange = (next: TStatus): void => {
|
|
951
|
+
statusRef.current = next;
|
|
908
952
|
setStatus(next);
|
|
953
|
+
// A reply lost with the connection never arrives; the views waiting on one learn that now.
|
|
954
|
+
if (next !== 'connected') {
|
|
955
|
+
rejectPendingAppRequests();
|
|
956
|
+
clearCompactionNotices();
|
|
957
|
+
}
|
|
909
958
|
// Every client learns what it can offer and what is in effect as soon as it is attached.
|
|
910
959
|
if (next === 'connected') {
|
|
911
960
|
// A reply lost with the old connection never arrives; stop waiting for it.
|
|
@@ -933,8 +982,17 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
933
982
|
return () => {
|
|
934
983
|
client.disconnect();
|
|
935
984
|
clientRef.current = null;
|
|
985
|
+
rejectPendingAppRequests();
|
|
936
986
|
};
|
|
937
|
-
}, [
|
|
987
|
+
}, [
|
|
988
|
+
makeClient,
|
|
989
|
+
handleMessage,
|
|
990
|
+
clearCompactionNotices,
|
|
991
|
+
requestSessions,
|
|
992
|
+
requestSchedules,
|
|
993
|
+
armRestore,
|
|
994
|
+
rejectPendingAppRequests,
|
|
995
|
+
]);
|
|
938
996
|
|
|
939
997
|
return {
|
|
940
998
|
status,
|
|
@@ -966,6 +1024,8 @@ export function useSessionClient<TStatus extends string = TConnectionStatus>(
|
|
|
966
1024
|
openAgentSwitcher,
|
|
967
1025
|
...agentSwitcherState,
|
|
968
1026
|
...schedulesState,
|
|
1027
|
+
readAppView,
|
|
1028
|
+
callAppTool,
|
|
969
1029
|
helpOpen,
|
|
970
1030
|
openHelp,
|
|
971
1031
|
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';
|