@ouispec/agent-react 0.1.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/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/annotations/AnnotationRegistry.d.ts +20 -0
- package/dist/annotations/AnnotationRegistry.d.ts.map +1 -0
- package/dist/annotations/AnnotationRegistry.js +100 -0
- package/dist/annotations/AnnotationRegistry.js.map +1 -0
- package/dist/annotations/singleton.d.ts +7 -0
- package/dist/annotations/singleton.d.ts.map +1 -0
- package/dist/annotations/singleton.js +7 -0
- package/dist/annotations/singleton.js.map +1 -0
- package/dist/annotations/types.d.ts +22 -0
- package/dist/annotations/types.d.ts.map +1 -0
- package/dist/annotations/types.js +2 -0
- package/dist/annotations/types.js.map +1 -0
- package/dist/annotations/useViewAnnotation.d.ts +15 -0
- package/dist/annotations/useViewAnnotation.d.ts.map +1 -0
- package/dist/annotations/useViewAnnotation.js +42 -0
- package/dist/annotations/useViewAnnotation.js.map +1 -0
- package/dist/approvals/ApprovalCard.d.ts +54 -0
- package/dist/approvals/ApprovalCard.d.ts.map +1 -0
- package/dist/approvals/ApprovalCard.js +65 -0
- package/dist/approvals/ApprovalCard.js.map +1 -0
- package/dist/approvals/decision.d.ts +17 -0
- package/dist/approvals/decision.d.ts.map +1 -0
- package/dist/approvals/decision.js +25 -0
- package/dist/approvals/decision.js.map +1 -0
- package/dist/entity/AgentEntity.d.ts +58 -0
- package/dist/entity/AgentEntity.d.ts.map +1 -0
- package/dist/entity/AgentEntity.js +97 -0
- package/dist/entity/AgentEntity.js.map +1 -0
- package/dist/entity/AgentEntityComponent.d.ts +73 -0
- package/dist/entity/AgentEntityComponent.d.ts.map +1 -0
- package/dist/entity/AgentEntityComponent.js +134 -0
- package/dist/entity/AgentEntityComponent.js.map +1 -0
- package/dist/entity/asAgentEntity.d.ts +45 -0
- package/dist/entity/asAgentEntity.d.ts.map +1 -0
- package/dist/entity/asAgentEntity.js +51 -0
- package/dist/entity/asAgentEntity.js.map +1 -0
- package/dist/entity/defineAgentEntity.d.ts +54 -0
- package/dist/entity/defineAgentEntity.d.ts.map +1 -0
- package/dist/entity/defineAgentEntity.js +133 -0
- package/dist/entity/defineAgentEntity.js.map +1 -0
- package/dist/entity/index.d.ts +10 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/index.js +6 -0
- package/dist/entity/index.js.map +1 -0
- package/dist/entity/useViewAnnotation.d.ts +64 -0
- package/dist/entity/useViewAnnotation.d.ts.map +1 -0
- package/dist/entity/useViewAnnotation.js +126 -0
- package/dist/entity/useViewAnnotation.js.map +1 -0
- package/dist/hooks/useFeedback.d.ts +7 -0
- package/dist/hooks/useFeedback.d.ts.map +1 -0
- package/dist/hooks/useFeedback.js +38 -0
- package/dist/hooks/useFeedback.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/provider/AgentProvider.d.ts +25 -0
- package/dist/provider/AgentProvider.d.ts.map +1 -0
- package/dist/provider/AgentProvider.js +800 -0
- package/dist/provider/AgentProvider.js.map +1 -0
- package/dist/provider/socketio.d.ts +8 -0
- package/dist/provider/socketio.d.ts.map +1 -0
- package/dist/provider/socketio.js +19 -0
- package/dist/provider/socketio.js.map +1 -0
- package/dist/provider/stored-messages.d.ts +16 -0
- package/dist/provider/stored-messages.d.ts.map +1 -0
- package/dist/provider/stored-messages.js +57 -0
- package/dist/provider/stored-messages.js.map +1 -0
- package/dist/provider/types.d.ts +158 -0
- package/dist/provider/types.d.ts.map +1 -0
- package/dist/provider/types.js +2 -0
- package/dist/provider/types.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { APPROVAL_DECIDE_EVENT } from '@ouispec/agent-core';
|
|
3
|
+
import { ALL_AGENT_SOCKET_EVENTS, parseSocketEvent } from '@ouispec/agent-core';
|
|
4
|
+
import { createSocketIOSocket } from './socketio.js';
|
|
5
|
+
import { ApprovalDecisionContext, approvalRefusalText } from '../approvals/decision.js';
|
|
6
|
+
import { annotationRegistry } from '../annotations/singleton.js';
|
|
7
|
+
import { storedToAgentMessages } from './stored-messages.js';
|
|
8
|
+
const AgentContext = createContext(null);
|
|
9
|
+
const MAX_DEBUG_LOGS = 500;
|
|
10
|
+
const DEFAULT_ACTIVE_CONVERSATION_KEY = 'agent-sdk.activeConversation';
|
|
11
|
+
const HISTORY_PAGE_SIZE = 20;
|
|
12
|
+
/** How long the card waits for the approval store to answer a click. */
|
|
13
|
+
const APPROVAL_DECIDE_TIMEOUT_MS = 10_000;
|
|
14
|
+
/**
|
|
15
|
+
* The tab's remembered active conversation, in `sessionStorage` so a reload
|
|
16
|
+
* returns to it and a new tab starts fresh. Storage can be missing or throw
|
|
17
|
+
* (private windows, blocked site data): then nothing is remembered.
|
|
18
|
+
*/
|
|
19
|
+
function sessionKeyOf(config) {
|
|
20
|
+
if (!config.getConversation || config.activeConversationKey === false)
|
|
21
|
+
return null;
|
|
22
|
+
return config.activeConversationKey ?? DEFAULT_ACTIVE_CONVERSATION_KEY;
|
|
23
|
+
}
|
|
24
|
+
function readRemembered(key) {
|
|
25
|
+
if (!key)
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
return window.sessionStorage.getItem(key);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function writeRemembered(key, conversationId) {
|
|
35
|
+
if (!key)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
if (conversationId)
|
|
39
|
+
window.sessionStorage.setItem(key, conversationId);
|
|
40
|
+
else
|
|
41
|
+
window.sessionStorage.removeItem(key);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Nothing is remembered; the conversation still works.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether a conversation still belongs in a list with this filter after a
|
|
49
|
+
* change. Only what the SDK can see is checked: a search is the platform's.
|
|
50
|
+
*/
|
|
51
|
+
function passesFilter(summary, filter) {
|
|
52
|
+
const status = filter.status ?? 'active';
|
|
53
|
+
if (status === 'active' && summary.archivedAt)
|
|
54
|
+
return false;
|
|
55
|
+
if (status === 'archived' && !summary.archivedAt)
|
|
56
|
+
return false;
|
|
57
|
+
if (filter.scopeId !== undefined && (summary.scope?.id ?? null) !== filter.scopeId)
|
|
58
|
+
return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
function reasonOf(err) {
|
|
62
|
+
return err instanceof Error ? err.message : String(err);
|
|
63
|
+
}
|
|
64
|
+
const ALL_DEBUG_NAMESPACES = ['agent:socket', 'agent:protocol', 'agent:state', 'agent:api'];
|
|
65
|
+
let debugLogCounter = 0;
|
|
66
|
+
function createDebugEntry(level, ns, message, data) {
|
|
67
|
+
return {
|
|
68
|
+
id: `dbg_${++debugLogCounter}`,
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
level,
|
|
71
|
+
ns,
|
|
72
|
+
message,
|
|
73
|
+
data,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* AgentProvider — turnkey provider that handles the complete agent lifecycle.
|
|
78
|
+
*
|
|
79
|
+
* The platform provides:
|
|
80
|
+
* - createConversation() / sendMessage() — API callbacks
|
|
81
|
+
* - realtime: { url, getToken } — where and how to connect
|
|
82
|
+
*
|
|
83
|
+
* The SDK owns:
|
|
84
|
+
* - Socket.IO connection lifecycle (connect, reconnect, auth)
|
|
85
|
+
* - Room subscription tied to active turns, kept across reconnects
|
|
86
|
+
* - All event parsing (agent:* protocol)
|
|
87
|
+
* - Streaming state, message history, keyboard shortcuts
|
|
88
|
+
*
|
|
89
|
+
* UI actions are not dispatched here: the client's OUI surface runtime answers
|
|
90
|
+
* them on the same socket (ADR-0209).
|
|
91
|
+
*/
|
|
92
|
+
export function AgentProvider({ config, children }) {
|
|
93
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
94
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
95
|
+
const [currentTurnId, setCurrentTurnId] = useState(null);
|
|
96
|
+
const [conversationId, setConversationId] = useState(null);
|
|
97
|
+
// The same id, read by callbacks that must not wait for a re-render (a send
|
|
98
|
+
// that follows a restore, a switch during a restore).
|
|
99
|
+
const conversationIdRef = useRef(null);
|
|
100
|
+
const [isLoadingConversation, setIsLoadingConversation] = useState(false);
|
|
101
|
+
const pendingLoadRef = useRef(null);
|
|
102
|
+
const loadSeqRef = useRef(0);
|
|
103
|
+
const [historyConversations, setHistoryConversations] = useState([]);
|
|
104
|
+
const [historyTotal, setHistoryTotal] = useState(0);
|
|
105
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
106
|
+
const [historyError, setHistoryError] = useState(null);
|
|
107
|
+
// The listed conversations, read by changes that must not wait for a re-render.
|
|
108
|
+
const historyConversationsRef = useRef([]);
|
|
109
|
+
const [historyFilter, setHistoryFilter] = useState({});
|
|
110
|
+
const historyFilterRef = useRef({});
|
|
111
|
+
const [historyRevision, setHistoryRevision] = useState(0);
|
|
112
|
+
const isStreamingRef = useRef(false);
|
|
113
|
+
const [messages, setMessages] = useState([]);
|
|
114
|
+
const [connected, setConnected] = useState(false);
|
|
115
|
+
const [debugEnabled, setDebugEnabled] = useState(false);
|
|
116
|
+
const [debugLogs, setDebugLogs] = useState([]);
|
|
117
|
+
const [presentedOptions, setPresentedOptions] = useState(null);
|
|
118
|
+
// The call a turn stopped at for the user's approval (ADR-0228), until they decide.
|
|
119
|
+
const [pendingApproval, setPendingApproval] = useState(null);
|
|
120
|
+
const [approvalDeciding, setApprovalDeciding] = useState(false);
|
|
121
|
+
const [approvalError, setApprovalError] = useState(null);
|
|
122
|
+
const socketRef = useRef(null);
|
|
123
|
+
const unsubscribeRef = useRef(null);
|
|
124
|
+
// The room of the turn in progress and its token. A reconnect gives the
|
|
125
|
+
// server a new socket that is in no room, so the join is repeated on every
|
|
126
|
+
// connect while a turn is active; without it the turn streams to nobody and
|
|
127
|
+
// the panel waits forever.
|
|
128
|
+
const activeRoomRef = useRef(null);
|
|
129
|
+
const streamBufferRef = useRef('');
|
|
130
|
+
const roundPrefixRef = useRef('');
|
|
131
|
+
const configRef = useRef(config);
|
|
132
|
+
const debugEnabledRef = useRef(debugEnabled);
|
|
133
|
+
configRef.current = config;
|
|
134
|
+
debugEnabledRef.current = debugEnabled;
|
|
135
|
+
isStreamingRef.current = isStreaming;
|
|
136
|
+
const shortcutKey = config.shortcutKey ?? 'k';
|
|
137
|
+
const setActiveConversation = useCallback((id) => {
|
|
138
|
+
conversationIdRef.current = id;
|
|
139
|
+
setConversationId(id);
|
|
140
|
+
writeRemembered(sessionKeyOf(configRef.current), id);
|
|
141
|
+
}, []);
|
|
142
|
+
const addDebugLog = useCallback((level, ns, message, data) => {
|
|
143
|
+
if (!debugEnabledRef.current)
|
|
144
|
+
return;
|
|
145
|
+
const entry = createDebugEntry(level, ns, message, data);
|
|
146
|
+
setDebugLogs(prev => {
|
|
147
|
+
const next = [...prev, entry];
|
|
148
|
+
return next.length > MAX_DEBUG_LOGS ? next.slice(-MAX_DEBUG_LOGS) : next;
|
|
149
|
+
});
|
|
150
|
+
}, []);
|
|
151
|
+
const clearDebugLogs = useCallback(() => setDebugLogs([]), []);
|
|
152
|
+
// --- Socket connection lifecycle (SDK-owned; the socket is a seam) ---
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const { url, getToken, createSocket = createSocketIOSocket } = config.realtime;
|
|
155
|
+
if (!url)
|
|
156
|
+
return;
|
|
157
|
+
const socket = createSocket({ url, getToken });
|
|
158
|
+
socket.on('connect', () => {
|
|
159
|
+
setConnected(true);
|
|
160
|
+
addDebugLog('info', 'agent:socket', 'Connected', { socketId: socket.id, url });
|
|
161
|
+
const active = activeRoomRef.current;
|
|
162
|
+
if (active) {
|
|
163
|
+
addDebugLog('info', 'agent:socket', `Rejoining turn room after reconnect: ${active.room}`, { room: active.room });
|
|
164
|
+
joinRoom(socket, active.room, active.token);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
socket.on('disconnect', (reason) => {
|
|
168
|
+
setConnected(false);
|
|
169
|
+
addDebugLog('warn', 'agent:socket', `Disconnected: ${reason}`, { reason });
|
|
170
|
+
});
|
|
171
|
+
socket.on('connect_error', (err) => {
|
|
172
|
+
addDebugLog('error', 'agent:socket', `Connection error: ${err.message}`, { error: err.message });
|
|
173
|
+
});
|
|
174
|
+
socket.on('reconnect_attempt', (attempt) => {
|
|
175
|
+
addDebugLog('info', 'agent:socket', `Reconnecting (attempt ${attempt})`, { attempt });
|
|
176
|
+
});
|
|
177
|
+
socket.connect();
|
|
178
|
+
addDebugLog('info', 'agent:socket', 'Connecting...', { url });
|
|
179
|
+
socketRef.current = socket;
|
|
180
|
+
return () => {
|
|
181
|
+
socket.disconnect();
|
|
182
|
+
socketRef.current = null;
|
|
183
|
+
setConnected(false);
|
|
184
|
+
};
|
|
185
|
+
}, [config.realtime.url]);
|
|
186
|
+
// Keyboard shortcut
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const handler = (e) => {
|
|
189
|
+
if ((e.metaKey || e.ctrlKey) && e.key === shortcutKey) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
setIsOpen(prev => !prev);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
document.addEventListener('keydown', handler);
|
|
195
|
+
return () => document.removeEventListener('keydown', handler);
|
|
196
|
+
}, [shortcutKey]);
|
|
197
|
+
// --- Protocol event handler ---
|
|
198
|
+
const handleProtocolEvent = useCallback((event) => {
|
|
199
|
+
addDebugLog('event', 'agent:protocol', `${event.type}`, event);
|
|
200
|
+
configRef.current.onEvent?.(event);
|
|
201
|
+
switch (event.type) {
|
|
202
|
+
case 'token': {
|
|
203
|
+
if (!event.content)
|
|
204
|
+
break;
|
|
205
|
+
streamBufferRef.current += event.content;
|
|
206
|
+
// Snapshot the accumulated content BEFORE enqueueing the state update.
|
|
207
|
+
// React batches updates and runs the setMessages callback later; if the
|
|
208
|
+
// 'done' event arrives and clears streamBufferRef.current before the
|
|
209
|
+
// batch flushes, the deferred callback would otherwise read '' and wipe
|
|
210
|
+
// the message. Capturing into a local makes the update self-contained.
|
|
211
|
+
const snapshot = streamBufferRef.current;
|
|
212
|
+
setMessages(prev => {
|
|
213
|
+
const msgId = `msg_${event.turnId}`;
|
|
214
|
+
const existingIdx = prev.findIndex(m => m.id === msgId);
|
|
215
|
+
if (existingIdx !== -1) {
|
|
216
|
+
// Update in place — never create a duplicate with the same id.
|
|
217
|
+
const updated = [...prev];
|
|
218
|
+
updated[existingIdx] = { ...updated[existingIdx], content: snapshot, isStreaming: true };
|
|
219
|
+
return updated;
|
|
220
|
+
}
|
|
221
|
+
// Defensive: drop any stale streaming messages for this turnId before adding a fresh one.
|
|
222
|
+
const cleaned = prev.filter(m => m.id !== msgId);
|
|
223
|
+
return [...cleaned, {
|
|
224
|
+
id: msgId,
|
|
225
|
+
role: 'assistant',
|
|
226
|
+
content: snapshot,
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
isStreaming: true,
|
|
229
|
+
}];
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'token_clear':
|
|
234
|
+
// A tool_use round ended. Preserve text from previous rounds so the
|
|
235
|
+
// user sees the full multi-round response. Only truly clear if no text
|
|
236
|
+
// has been accumulated yet (first round).
|
|
237
|
+
if (streamBufferRef.current) {
|
|
238
|
+
// Commit the current buffer as a completed round prefix.
|
|
239
|
+
roundPrefixRef.current = streamBufferRef.current;
|
|
240
|
+
// Add a separator — the next round's tokens will append after this.
|
|
241
|
+
streamBufferRef.current = roundPrefixRef.current + '\n\n';
|
|
242
|
+
}
|
|
243
|
+
// Don't update message content — leave the existing text visible.
|
|
244
|
+
break;
|
|
245
|
+
case 'intent_call':
|
|
246
|
+
setMessages(prev => [...prev, {
|
|
247
|
+
id: `tool_${event.id}`,
|
|
248
|
+
role: 'tool',
|
|
249
|
+
content: null,
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
toolCall: { id: event.id, name: event.intentId, status: 'running' },
|
|
252
|
+
}]);
|
|
253
|
+
break;
|
|
254
|
+
case 'intent_result':
|
|
255
|
+
// Detect present_options tool results and surface them as interactive UI state.
|
|
256
|
+
if (event.result && typeof event.result === 'object' && event.result.__present_options) {
|
|
257
|
+
const payload = event.result;
|
|
258
|
+
setPresentedOptions({
|
|
259
|
+
prompt: payload.prompt ?? '',
|
|
260
|
+
options: payload.options ?? [],
|
|
261
|
+
style: payload.style ?? undefined,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
setMessages(prev => prev.map(m => m.toolCall?.id === event.callId
|
|
265
|
+
? { ...m, toolCall: { ...m.toolCall, status: event.success ? 'complete' : 'error', result: event.result } }
|
|
266
|
+
: m));
|
|
267
|
+
break;
|
|
268
|
+
case 'approval_required': {
|
|
269
|
+
// The card renders it; only the user's click on it decides.
|
|
270
|
+
const { type: _type, ...request } = event;
|
|
271
|
+
setPendingApproval(request);
|
|
272
|
+
setApprovalError(null);
|
|
273
|
+
setApprovalDeciding(false);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case 'done':
|
|
277
|
+
// Finalize all streaming messages. Drop any assistant streaming bubble
|
|
278
|
+
// that ended up truly empty (e.g. token_clear wiped it and no text was
|
|
279
|
+
// re-emitted). Preserve tool messages (which legitimately have null content).
|
|
280
|
+
streamBufferRef.current = '';
|
|
281
|
+
roundPrefixRef.current = '';
|
|
282
|
+
setMessages(prev => {
|
|
283
|
+
const finalized = prev.map(m => m.isStreaming ? { ...m, isStreaming: false } : m);
|
|
284
|
+
return finalized.filter(m => {
|
|
285
|
+
// Only drop assistant bubbles that are empty (null or empty string).
|
|
286
|
+
// Tool messages have null content by design — keep them.
|
|
287
|
+
if (m.role === 'assistant' && (m.content === null || m.content === '')) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
setIsStreaming(false);
|
|
294
|
+
setCurrentTurnId(null);
|
|
295
|
+
activeRoomRef.current = null;
|
|
296
|
+
if (unsubscribeRef.current) {
|
|
297
|
+
unsubscribeRef.current();
|
|
298
|
+
unsubscribeRef.current = null;
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
case 'error':
|
|
302
|
+
streamBufferRef.current = '';
|
|
303
|
+
roundPrefixRef.current = '';
|
|
304
|
+
setMessages(prev => {
|
|
305
|
+
const last = prev[prev.length - 1];
|
|
306
|
+
if (last?.isStreaming) {
|
|
307
|
+
return [...prev.slice(0, -1), { ...last, isStreaming: false, content: last.content || 'An error occurred.' }];
|
|
308
|
+
}
|
|
309
|
+
return [...prev, {
|
|
310
|
+
id: `err_${Date.now()}`,
|
|
311
|
+
role: 'assistant',
|
|
312
|
+
content: event.message || 'An error occurred.',
|
|
313
|
+
timestamp: Date.now(),
|
|
314
|
+
}];
|
|
315
|
+
});
|
|
316
|
+
setIsStreaming(false);
|
|
317
|
+
setCurrentTurnId(null);
|
|
318
|
+
activeRoomRef.current = null;
|
|
319
|
+
if (unsubscribeRef.current) {
|
|
320
|
+
unsubscribeRef.current();
|
|
321
|
+
unsubscribeRef.current = null;
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}, []);
|
|
326
|
+
// --- Join a turn room ---
|
|
327
|
+
// The realtime server requires a signed room token for a turn room and
|
|
328
|
+
// denies the join without one — the socket stays connected, the
|
|
329
|
+
// turn completes server-side, and no event ever arrives. Send the
|
|
330
|
+
// token-bearing form when we have a token, and ACK the result so a denial
|
|
331
|
+
// is visible instead of looking like the agent simply never replied.
|
|
332
|
+
const joinRoom = useCallback((socket, room, roomToken) => {
|
|
333
|
+
if (roomToken) {
|
|
334
|
+
socket.emit('subscribe', { rooms: [room], tokens: { [room]: roomToken } }, (res) => {
|
|
335
|
+
if (res && (!res.ok || res.denied?.includes(room))) {
|
|
336
|
+
addDebugLog('error', 'agent:socket', `Room subscription DENIED: ${room}`, {
|
|
337
|
+
room,
|
|
338
|
+
denied: res.denied,
|
|
339
|
+
error: res.error,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
addDebugLog('info', 'agent:socket', `Room joined: ${room}`, { room, joined: res?.joined });
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
addDebugLog('warn', 'agent:socket', `Subscribing to ${room} WITHOUT a room token`, {
|
|
349
|
+
room,
|
|
350
|
+
hint: 'agent:* rooms require a signed token; the server will deny this join',
|
|
351
|
+
});
|
|
352
|
+
socket.emit('subscribe', [room]);
|
|
353
|
+
}
|
|
354
|
+
}, [addDebugLog]);
|
|
355
|
+
// --- Subscribe to a socket room for agent events ---
|
|
356
|
+
const subscribeToRoom = useCallback((room, turnId, roomToken) => {
|
|
357
|
+
const socket = socketRef.current;
|
|
358
|
+
if (!socket) {
|
|
359
|
+
addDebugLog('error', 'agent:socket', 'Cannot subscribe — no socket (realtime URL not configured)', { room });
|
|
360
|
+
return () => { };
|
|
361
|
+
}
|
|
362
|
+
addDebugLog('info', 'agent:socket', `Subscribing to room: ${room}`, {
|
|
363
|
+
room,
|
|
364
|
+
turnId,
|
|
365
|
+
hasRoomToken: !!roomToken,
|
|
366
|
+
connected: socket.connected,
|
|
367
|
+
});
|
|
368
|
+
// Joined now if connected; otherwise on the next connect, which the
|
|
369
|
+
// connect handler does for the active room.
|
|
370
|
+
activeRoomRef.current = { room, token: roomToken };
|
|
371
|
+
if (socket.connected)
|
|
372
|
+
joinRoom(socket, room, roomToken);
|
|
373
|
+
const handlers = new Map();
|
|
374
|
+
for (const eventName of ALL_AGENT_SOCKET_EVENTS) {
|
|
375
|
+
const handler = (data) => {
|
|
376
|
+
addDebugLog('socket', 'agent:socket', `← ${eventName}`, { eventName, data });
|
|
377
|
+
const parsed = parseSocketEvent(eventName, data, turnId);
|
|
378
|
+
if (parsed)
|
|
379
|
+
handleProtocolEvent(parsed);
|
|
380
|
+
};
|
|
381
|
+
handlers.set(eventName, handler);
|
|
382
|
+
socket.on(eventName, handler);
|
|
383
|
+
}
|
|
384
|
+
return () => {
|
|
385
|
+
for (const [eventName, handler] of handlers) {
|
|
386
|
+
socket.off(eventName, handler);
|
|
387
|
+
}
|
|
388
|
+
socket.emit('unsubscribe', [room]);
|
|
389
|
+
addDebugLog('info', 'agent:socket', `Unsubscribed from room: ${room}`, { room });
|
|
390
|
+
};
|
|
391
|
+
}, [handleProtocolEvent, addDebugLog, joinRoom]);
|
|
392
|
+
// Collect context from visible annotations and current route
|
|
393
|
+
const collectContext = useCallback(() => {
|
|
394
|
+
const annotations = annotationRegistry.getVisibleAnnotations();
|
|
395
|
+
return {
|
|
396
|
+
currentPath: typeof window !== 'undefined' ? window.location.pathname : undefined,
|
|
397
|
+
visibleAnnotations: annotations,
|
|
398
|
+
};
|
|
399
|
+
}, []);
|
|
400
|
+
// --- Start a turn: the user's message, or the continuation after an approval decision ---
|
|
401
|
+
const startTurn = useCallback(async (content, attachments, approval) => {
|
|
402
|
+
// A message sent while a conversation loads belongs to that conversation.
|
|
403
|
+
if (pendingLoadRef.current)
|
|
404
|
+
await pendingLoadRef.current;
|
|
405
|
+
let convId = conversationIdRef.current;
|
|
406
|
+
if (!convId) {
|
|
407
|
+
addDebugLog('info', 'agent:api', 'Creating conversation...', {});
|
|
408
|
+
const result = await configRef.current.createConversation();
|
|
409
|
+
convId = result.conversationId;
|
|
410
|
+
setActiveConversation(convId);
|
|
411
|
+
addDebugLog('info', 'agent:api', `Conversation created: ${convId}`, { conversationId: convId });
|
|
412
|
+
}
|
|
413
|
+
// A click on the approval card continues the turn, but it is not a message.
|
|
414
|
+
if (!approval) {
|
|
415
|
+
const userMsg = {
|
|
416
|
+
id: `msg_user_${Date.now()}`,
|
|
417
|
+
role: 'user',
|
|
418
|
+
content,
|
|
419
|
+
timestamp: Date.now(),
|
|
420
|
+
};
|
|
421
|
+
setMessages(prev => [...prev, userMsg]);
|
|
422
|
+
// A new message moves on: a card still waiting belongs to the previous turn.
|
|
423
|
+
setPendingApproval(null);
|
|
424
|
+
}
|
|
425
|
+
// Any choice still on screen belongs to the previous turn.
|
|
426
|
+
setPresentedOptions(null);
|
|
427
|
+
setIsStreaming(true);
|
|
428
|
+
streamBufferRef.current = '';
|
|
429
|
+
try {
|
|
430
|
+
const context = collectContext();
|
|
431
|
+
addDebugLog('info', 'agent:api', approval ? 'Continuing after an approval decision...' : 'Sending message...', {
|
|
432
|
+
conversationId: convId,
|
|
433
|
+
contentLength: content.length,
|
|
434
|
+
context,
|
|
435
|
+
...(approval ? { approvalId: approval.approvalId, decision: approval.decision } : {}),
|
|
436
|
+
});
|
|
437
|
+
const { turnId, socketRoom, roomToken } = await configRef.current.sendMessage({
|
|
438
|
+
conversationId: convId,
|
|
439
|
+
content,
|
|
440
|
+
context,
|
|
441
|
+
attachments,
|
|
442
|
+
...(approval ? { approval } : {}),
|
|
443
|
+
});
|
|
444
|
+
addDebugLog('info', 'agent:api', `Turn started: ${turnId}`, {
|
|
445
|
+
turnId,
|
|
446
|
+
socketRoom,
|
|
447
|
+
hasRoomToken: !!roomToken,
|
|
448
|
+
});
|
|
449
|
+
addDebugLog('info', 'agent:state', 'Streaming started', { turnId });
|
|
450
|
+
setCurrentTurnId(turnId);
|
|
451
|
+
if (unsubscribeRef.current)
|
|
452
|
+
unsubscribeRef.current();
|
|
453
|
+
unsubscribeRef.current = subscribeToRoom(socketRoom, turnId, roomToken);
|
|
454
|
+
return { turnId };
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
addDebugLog('error', 'agent:api', `Send failed: ${err instanceof Error ? err.message : String(err)}`, { error: err });
|
|
458
|
+
setIsStreaming(false);
|
|
459
|
+
setMessages(prev => [...prev, {
|
|
460
|
+
id: `err_${Date.now()}`,
|
|
461
|
+
role: 'assistant',
|
|
462
|
+
content: err instanceof Error ? err.message : 'Failed to send message.',
|
|
463
|
+
timestamp: Date.now(),
|
|
464
|
+
}]);
|
|
465
|
+
return { turnId: '' };
|
|
466
|
+
}
|
|
467
|
+
}, [collectContext, subscribeToRoom, addDebugLog, setActiveConversation]);
|
|
468
|
+
const sendMessage = useCallback((content, attachments) => startTurn(content, attachments), [startTurn]);
|
|
469
|
+
// --- The user's decision on an approval card (ADR-0228 §2.2.3) ---
|
|
470
|
+
// Only the card calls this: it is not on the context `useAgent` returns, so
|
|
471
|
+
// nothing an app binds for the assistant can reach it.
|
|
472
|
+
const pendingApprovalRef = useRef(null);
|
|
473
|
+
pendingApprovalRef.current = pendingApproval;
|
|
474
|
+
const decideApproval = useCallback(async (decision) => {
|
|
475
|
+
const request = pendingApprovalRef.current;
|
|
476
|
+
const socket = socketRef.current;
|
|
477
|
+
if (!request)
|
|
478
|
+
return;
|
|
479
|
+
if (!socket) {
|
|
480
|
+
setApprovalError('Not connected: try again once the assistant reconnects.');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
setApprovalDeciding(true);
|
|
484
|
+
setApprovalError(null);
|
|
485
|
+
addDebugLog('info', 'agent:api', `Approval ${decision}`, { approvalId: request.approvalId, tool: request.tool });
|
|
486
|
+
const result = await new Promise(resolve => {
|
|
487
|
+
const timer = setTimeout(() => resolve({ ok: false, reason: 'invalid', error: 'the server did not answer' }), APPROVAL_DECIDE_TIMEOUT_MS);
|
|
488
|
+
socket.emit(APPROVAL_DECIDE_EVENT, { approvalId: request.approvalId, decision }, (answer) => {
|
|
489
|
+
clearTimeout(timer);
|
|
490
|
+
resolve(answer);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
if (!result.ok) {
|
|
494
|
+
addDebugLog('warn', 'agent:api', `Approval refused: ${result.reason}`, { approvalId: request.approvalId, reason: result.reason });
|
|
495
|
+
setApprovalError(approvalRefusalText(result));
|
|
496
|
+
setApprovalDeciding(false);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
// The grant goes to this tab's OUI runtime before the turn that dispatches the call starts.
|
|
500
|
+
if (result.decision === 'approve') {
|
|
501
|
+
configRef.current.grantApproval?.({ approvalId: result.approvalId, argsHash: result.argsHash, expiresAt: result.expiresAt });
|
|
502
|
+
}
|
|
503
|
+
setPendingApproval(null);
|
|
504
|
+
setApprovalDeciding(false);
|
|
505
|
+
await startTurn('', undefined, result.decision === 'approve'
|
|
506
|
+
? { approvalId: result.approvalId, decision: 'approve', token: result.token }
|
|
507
|
+
: { approvalId: result.approvalId, decision: 'decline' });
|
|
508
|
+
}, [startTurn, addDebugLog]);
|
|
509
|
+
const dismissApproval = useCallback(() => {
|
|
510
|
+
setPendingApproval(null);
|
|
511
|
+
setApprovalError(null);
|
|
512
|
+
setApprovalDeciding(false);
|
|
513
|
+
}, []);
|
|
514
|
+
// --- Conversation history ---
|
|
515
|
+
/** Stop listening to the turn in progress, if any. */
|
|
516
|
+
const leaveTurn = useCallback(() => {
|
|
517
|
+
if (unsubscribeRef.current) {
|
|
518
|
+
unsubscribeRef.current();
|
|
519
|
+
unsubscribeRef.current = null;
|
|
520
|
+
}
|
|
521
|
+
activeRoomRef.current = null;
|
|
522
|
+
streamBufferRef.current = '';
|
|
523
|
+
roundPrefixRef.current = '';
|
|
524
|
+
setCurrentTurnId(null);
|
|
525
|
+
}, []);
|
|
526
|
+
/**
|
|
527
|
+
* Show a stored conversation. The latest request wins: a load that finishes
|
|
528
|
+
* after another began is discarded. A restore that fails (the conversation
|
|
529
|
+
* was deleted, or belongs to someone else) forgets it and starts fresh.
|
|
530
|
+
*/
|
|
531
|
+
const loadConversation = useCallback((id, { restoring }) => {
|
|
532
|
+
const getConversation = configRef.current.getConversation;
|
|
533
|
+
if (!getConversation)
|
|
534
|
+
return Promise.resolve();
|
|
535
|
+
const seq = ++loadSeqRef.current;
|
|
536
|
+
setIsLoadingConversation(true);
|
|
537
|
+
addDebugLog('info', 'agent:api', `${restoring ? 'Restoring' : 'Loading'} conversation ${id}`, { conversationId: id });
|
|
538
|
+
const load = (async () => {
|
|
539
|
+
try {
|
|
540
|
+
const stored = await getConversation(id);
|
|
541
|
+
if (seq !== loadSeqRef.current)
|
|
542
|
+
return;
|
|
543
|
+
setActiveConversation(stored.conversationId);
|
|
544
|
+
setMessages(storedToAgentMessages(stored.messages));
|
|
545
|
+
setPresentedOptions(null);
|
|
546
|
+
setPendingApproval(null);
|
|
547
|
+
setHistoryError(null);
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
if (seq !== loadSeqRef.current)
|
|
551
|
+
return;
|
|
552
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
553
|
+
addDebugLog('warn', 'agent:api', `Could not load conversation ${id}: ${message}`, { conversationId: id });
|
|
554
|
+
if (restoring) {
|
|
555
|
+
setActiveConversation(null);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
setHistoryError('That conversation could not be opened.');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
finally {
|
|
562
|
+
if (seq === loadSeqRef.current) {
|
|
563
|
+
setIsLoadingConversation(false);
|
|
564
|
+
pendingLoadRef.current = null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
})();
|
|
568
|
+
pendingLoadRef.current = load;
|
|
569
|
+
return load;
|
|
570
|
+
}, [addDebugLog, setActiveConversation]);
|
|
571
|
+
// Return to the tab's conversation after a reload.
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
const remembered = readRemembered(sessionKeyOf(configRef.current));
|
|
574
|
+
if (remembered)
|
|
575
|
+
void loadConversation(remembered, { restoring: true });
|
|
576
|
+
}, [loadConversation]);
|
|
577
|
+
const refreshHistory = useCallback(async (filter) => {
|
|
578
|
+
const listConversations = configRef.current.listConversations;
|
|
579
|
+
if (!listConversations)
|
|
580
|
+
return;
|
|
581
|
+
if (filter) {
|
|
582
|
+
historyFilterRef.current = filter;
|
|
583
|
+
setHistoryFilter(filter);
|
|
584
|
+
}
|
|
585
|
+
const asked = historyFilterRef.current;
|
|
586
|
+
setHistoryLoading(true);
|
|
587
|
+
try {
|
|
588
|
+
const page = await listConversations({ limit: HISTORY_PAGE_SIZE, offset: 0, ...asked });
|
|
589
|
+
// A refresh with another filter started since: its answer wins.
|
|
590
|
+
if (asked !== historyFilterRef.current)
|
|
591
|
+
return;
|
|
592
|
+
historyConversationsRef.current = page.conversations;
|
|
593
|
+
setHistoryConversations(page.conversations);
|
|
594
|
+
setHistoryTotal(page.total);
|
|
595
|
+
setHistoryError(null);
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
addDebugLog('warn', 'agent:api', `Could not list conversations: ${reasonOf(err)}`);
|
|
599
|
+
setHistoryError('Your conversations could not be loaded.');
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
setHistoryLoading(false);
|
|
603
|
+
}
|
|
604
|
+
}, [addDebugLog]);
|
|
605
|
+
const queryConversations = useCallback(async (params) => {
|
|
606
|
+
const listConversations = configRef.current.listConversations;
|
|
607
|
+
if (!listConversations)
|
|
608
|
+
throw new Error('This app cannot list conversations.');
|
|
609
|
+
return listConversations(params);
|
|
610
|
+
}, []);
|
|
611
|
+
/** Replace the listed conversations after a change, and count the change. */
|
|
612
|
+
const setListed = useCallback((next) => {
|
|
613
|
+
const dropped = historyConversationsRef.current.length - next.length;
|
|
614
|
+
historyConversationsRef.current = next;
|
|
615
|
+
setHistoryConversations(next);
|
|
616
|
+
if (dropped > 0)
|
|
617
|
+
setHistoryTotal(total => Math.max(0, total - dropped));
|
|
618
|
+
setHistoryRevision(r => r + 1);
|
|
619
|
+
}, []);
|
|
620
|
+
/** Put changed conversations into the listed ones, dropping those the filter no longer lets in. */
|
|
621
|
+
const applyToHistory = useCallback((changed) => {
|
|
622
|
+
const byId = new Map(changed.map(c => [c.conversationId, c]));
|
|
623
|
+
const filter = historyFilterRef.current;
|
|
624
|
+
setListed(historyConversationsRef.current.flatMap(c => {
|
|
625
|
+
const updated = byId.get(c.conversationId);
|
|
626
|
+
if (!updated)
|
|
627
|
+
return [c];
|
|
628
|
+
return passesFilter(updated, filter) ? [updated] : [];
|
|
629
|
+
}));
|
|
630
|
+
}, [setListed]);
|
|
631
|
+
const renameConversation = useCallback(async (id, title) => {
|
|
632
|
+
const rename = configRef.current.renameConversation;
|
|
633
|
+
if (!rename)
|
|
634
|
+
throw new Error('This app cannot rename conversations.');
|
|
635
|
+
addDebugLog('info', 'agent:api', `Renaming conversation ${id}`, { conversationId: id, automatic: title === null });
|
|
636
|
+
const updated = await rename(id, title);
|
|
637
|
+
applyToHistory([updated]);
|
|
638
|
+
return updated;
|
|
639
|
+
}, [addDebugLog, applyToHistory]);
|
|
640
|
+
const updateConversations = useCallback(async (ids, changes) => {
|
|
641
|
+
const update = configRef.current.updateConversations;
|
|
642
|
+
if (!update)
|
|
643
|
+
throw new Error('This app cannot file or archive conversations.');
|
|
644
|
+
addDebugLog('info', 'agent:api', `Updating ${ids.length} conversation(s)`, { conversationIds: ids, changes });
|
|
645
|
+
const updated = await update(ids, changes);
|
|
646
|
+
applyToHistory(updated);
|
|
647
|
+
return updated;
|
|
648
|
+
}, [addDebugLog, applyToHistory]);
|
|
649
|
+
const switchConversation = useCallback(async (id) => {
|
|
650
|
+
if (isStreaming || id === conversationIdRef.current)
|
|
651
|
+
return;
|
|
652
|
+
leaveTurn();
|
|
653
|
+
await loadConversation(id, { restoring: false });
|
|
654
|
+
}, [isStreaming, leaveTurn, loadConversation]);
|
|
655
|
+
/** Leave the open conversation for a new one. Callers check that no turn is answering. */
|
|
656
|
+
const resetToNewConversation = useCallback(() => {
|
|
657
|
+
loadSeqRef.current++;
|
|
658
|
+
pendingLoadRef.current = null;
|
|
659
|
+
setIsLoadingConversation(false);
|
|
660
|
+
leaveTurn();
|
|
661
|
+
setActiveConversation(null);
|
|
662
|
+
setMessages([]);
|
|
663
|
+
setPresentedOptions(null);
|
|
664
|
+
setPendingApproval(null);
|
|
665
|
+
addDebugLog('info', 'agent:state', 'New conversation');
|
|
666
|
+
}, [leaveTurn, setActiveConversation, addDebugLog]);
|
|
667
|
+
const startNewConversation = useCallback(() => {
|
|
668
|
+
if (isStreaming)
|
|
669
|
+
return;
|
|
670
|
+
resetToNewConversation();
|
|
671
|
+
}, [isStreaming, resetToNewConversation]);
|
|
672
|
+
const removeConversations = useCallback(async (ids) => {
|
|
673
|
+
const remove = configRef.current.deleteConversations;
|
|
674
|
+
if (!remove)
|
|
675
|
+
throw new Error('This app cannot delete conversations.');
|
|
676
|
+
const open = conversationIdRef.current;
|
|
677
|
+
const removingOpen = open !== null && ids.includes(open);
|
|
678
|
+
if (removingOpen && isStreamingRef.current) {
|
|
679
|
+
throw new Error('That conversation is answering. Wait for it to finish, then delete it.');
|
|
680
|
+
}
|
|
681
|
+
addDebugLog('info', 'agent:api', `Deleting ${ids.length} conversation(s)`, { conversationIds: ids });
|
|
682
|
+
await remove(ids);
|
|
683
|
+
const gone = new Set(ids);
|
|
684
|
+
setListed(historyConversationsRef.current.filter(c => !gone.has(c.conversationId)));
|
|
685
|
+
if (removingOpen && conversationIdRef.current === open)
|
|
686
|
+
resetToNewConversation();
|
|
687
|
+
}, [addDebugLog, resetToNewConversation, setListed]);
|
|
688
|
+
const exportSession = useCallback(() => {
|
|
689
|
+
const session = {
|
|
690
|
+
exportedAt: new Date().toISOString(),
|
|
691
|
+
sdkVersion: '0.2.14',
|
|
692
|
+
session: {
|
|
693
|
+
conversationId,
|
|
694
|
+
currentTurnId,
|
|
695
|
+
connected,
|
|
696
|
+
isStreaming,
|
|
697
|
+
messageCount: messages.length,
|
|
698
|
+
messages: messages.map(m => ({
|
|
699
|
+
id: m.id,
|
|
700
|
+
role: m.role,
|
|
701
|
+
content: m.content,
|
|
702
|
+
timestamp: new Date(m.timestamp).toISOString(),
|
|
703
|
+
isStreaming: m.isStreaming,
|
|
704
|
+
toolCall: m.toolCall,
|
|
705
|
+
})),
|
|
706
|
+
},
|
|
707
|
+
debug: {
|
|
708
|
+
enabled: debugEnabled,
|
|
709
|
+
logCount: debugLogs.length,
|
|
710
|
+
logs: debugLogs,
|
|
711
|
+
},
|
|
712
|
+
environment: {
|
|
713
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
|
|
714
|
+
url: typeof window !== 'undefined' ? window.location.href : 'unknown',
|
|
715
|
+
realtimeUrl: config.realtime.url,
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
const blob = new Blob([JSON.stringify(session, null, 2)], { type: 'application/json' });
|
|
719
|
+
const url = URL.createObjectURL(blob);
|
|
720
|
+
const a = document.createElement('a');
|
|
721
|
+
a.href = url;
|
|
722
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
723
|
+
a.download = `agent-session-${conversationId ?? 'no-conv'}-${stamp}.json`;
|
|
724
|
+
document.body.appendChild(a);
|
|
725
|
+
a.click();
|
|
726
|
+
document.body.removeChild(a);
|
|
727
|
+
URL.revokeObjectURL(url);
|
|
728
|
+
}, [conversationId, currentTurnId, connected, isStreaming, messages, debugEnabled, debugLogs, config.realtime.url]);
|
|
729
|
+
const debugState = {
|
|
730
|
+
enabled: debugEnabled,
|
|
731
|
+
setEnabled: setDebugEnabled,
|
|
732
|
+
logs: debugLogs,
|
|
733
|
+
clearLogs: clearDebugLogs,
|
|
734
|
+
namespaces: ALL_DEBUG_NAMESPACES,
|
|
735
|
+
exportSession,
|
|
736
|
+
};
|
|
737
|
+
const selectOption = useCallback((value) => {
|
|
738
|
+
setPresentedOptions(null);
|
|
739
|
+
sendMessage(value);
|
|
740
|
+
}, [sendMessage]);
|
|
741
|
+
const dismissOptions = useCallback(() => {
|
|
742
|
+
setPresentedOptions(null);
|
|
743
|
+
}, []);
|
|
744
|
+
const contextValue = {
|
|
745
|
+
isOpen,
|
|
746
|
+
toggle: useCallback(() => setIsOpen(p => !p), []),
|
|
747
|
+
open: useCallback(() => setIsOpen(true), []),
|
|
748
|
+
close: useCallback(() => setIsOpen(false), []),
|
|
749
|
+
sendMessage,
|
|
750
|
+
messages,
|
|
751
|
+
isStreaming,
|
|
752
|
+
isProcessing: isStreaming,
|
|
753
|
+
currentTurnId,
|
|
754
|
+
conversationId,
|
|
755
|
+
connected,
|
|
756
|
+
debug: debugState,
|
|
757
|
+
presentedOptions,
|
|
758
|
+
selectOption,
|
|
759
|
+
dismissOptions,
|
|
760
|
+
pendingApproval,
|
|
761
|
+
socket: socketRef.current,
|
|
762
|
+
history: {
|
|
763
|
+
available: Boolean(config.listConversations && config.getConversation),
|
|
764
|
+
manage: {
|
|
765
|
+
rename: Boolean(config.renameConversation),
|
|
766
|
+
update: Boolean(config.updateConversations),
|
|
767
|
+
remove: Boolean(config.deleteConversations),
|
|
768
|
+
},
|
|
769
|
+
conversations: historyConversations,
|
|
770
|
+
total: historyTotal,
|
|
771
|
+
filter: historyFilter,
|
|
772
|
+
loading: historyLoading,
|
|
773
|
+
error: historyError,
|
|
774
|
+
refresh: refreshHistory,
|
|
775
|
+
query: queryConversations,
|
|
776
|
+
rename: renameConversation,
|
|
777
|
+
update: updateConversations,
|
|
778
|
+
remove: removeConversations,
|
|
779
|
+
revision: historyRevision,
|
|
780
|
+
},
|
|
781
|
+
isLoadingConversation,
|
|
782
|
+
switchConversation,
|
|
783
|
+
startNewConversation,
|
|
784
|
+
};
|
|
785
|
+
const approvalDecision = {
|
|
786
|
+
pending: pendingApproval,
|
|
787
|
+
deciding: approvalDeciding,
|
|
788
|
+
error: approvalError,
|
|
789
|
+
decide: decideApproval,
|
|
790
|
+
dismiss: dismissApproval,
|
|
791
|
+
};
|
|
792
|
+
return React.createElement(AgentContext.Provider, { value: contextValue }, React.createElement(ApprovalDecisionContext.Provider, { value: approvalDecision }, children));
|
|
793
|
+
}
|
|
794
|
+
export function useAgent() {
|
|
795
|
+
const ctx = useContext(AgentContext);
|
|
796
|
+
if (!ctx)
|
|
797
|
+
throw new Error('useAgent must be used within an AgentProvider');
|
|
798
|
+
return ctx;
|
|
799
|
+
}
|
|
800
|
+
//# sourceMappingURL=AgentProvider.js.map
|