@chrysb/alphaclaw 0.8.2 → 0.8.3-beta.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/README.md +2 -1
- package/lib/public/css/chat.css +426 -0
- package/lib/public/css/explorer.css +101 -0
- package/lib/public/dist/app.bundle.js +2307 -2115
- package/lib/public/js/app.js +34 -0
- package/lib/public/js/components/nodes-tab/connected-nodes/index.js +16 -14
- package/lib/public/js/components/routes/chat-route.js +1094 -0
- package/lib/public/js/components/routes/index.js +1 -0
- package/lib/public/js/components/sidebar.js +52 -0
- package/lib/public/js/hooks/use-browse-navigation.js +16 -5
- package/lib/public/js/lib/app-navigation.js +1 -0
- package/lib/public/js/lib/storage-keys.js +3 -0
- package/lib/public/setup.html +1 -0
- package/lib/server/auth-profiles.js +1 -1
- package/lib/server/chat-ws.js +872 -0
- package/lib/server/constants.js +4 -0
- package/lib/server/onboarding/validation.js +15 -10
- package/lib/server/openclaw-version.js +2 -1
- package/lib/server/routes/proxy.js +2 -2
- package/lib/server/slack-api.js +211 -2
- package/lib/server/watchdog-notify.js +52 -2
- package/lib/server/watchdog-terminal-ws.js +14 -1
- package/lib/server/watchdog.js +9 -4
- package/lib/server.js +36 -0
- package/package.json +6 -4
- /package/lib/public/dist/chunks/{addon-fit-W4YZGRNV.js → addon-fit-4LH2IIZ4.js} +0 -0
- /package/lib/public/dist/chunks/{xterm-KOX4YMOF.js → xterm-DK3X7FZB.js} +0 -0
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
import { h } from "preact";
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "preact/hooks";
|
|
9
|
+
import htm from "htm";
|
|
10
|
+
import { marked } from "marked";
|
|
11
|
+
import { authFetch } from "../../lib/api.js";
|
|
12
|
+
import { kChatSessionDraftsStorageKey } from "../../lib/storage-keys.js";
|
|
13
|
+
import { showToast } from "../toast.js";
|
|
14
|
+
|
|
15
|
+
const html = htm.bind(h);
|
|
16
|
+
const kNewChatEventName = "alphaclaw:chat-new";
|
|
17
|
+
const kWsReconnectMaxAttempts = 8;
|
|
18
|
+
const kAutoscrollBottomThresholdPx = 40;
|
|
19
|
+
const kChatDebugQueryFlag = "chatDebug";
|
|
20
|
+
|
|
21
|
+
const buildMessage = ({
|
|
22
|
+
role = "assistant",
|
|
23
|
+
content = "",
|
|
24
|
+
createdAt = Date.now(),
|
|
25
|
+
debugPayload = null,
|
|
26
|
+
} = {}) => ({
|
|
27
|
+
id: crypto.randomUUID(),
|
|
28
|
+
role,
|
|
29
|
+
content: String(content || ""),
|
|
30
|
+
createdAt: Number(createdAt) || Date.now(),
|
|
31
|
+
debugPayload,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const formatChatTime = (createdAt) => {
|
|
35
|
+
const value = Number(createdAt || 0);
|
|
36
|
+
if (!value) return "";
|
|
37
|
+
try {
|
|
38
|
+
return new Date(value).toLocaleTimeString([], {
|
|
39
|
+
hour: "2-digit",
|
|
40
|
+
minute: "2-digit",
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const escapeHtmlForMarkdown = (value = "") =>
|
|
48
|
+
String(value || "")
|
|
49
|
+
.replaceAll("&", "&")
|
|
50
|
+
.replaceAll("<", "<")
|
|
51
|
+
.replaceAll(">", ">");
|
|
52
|
+
|
|
53
|
+
const normalizeMarkdownInput = (value = "") => {
|
|
54
|
+
const source = String(value || "").replace(/\r\n/g, "\n");
|
|
55
|
+
if (source.includes("\n")) return source;
|
|
56
|
+
// Some runtimes persist escaped sequences in history payloads.
|
|
57
|
+
return source.includes("\\n") ? source.replace(/\\n/g, "\n") : source;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const normalizeListMarkers = (value = "") =>
|
|
61
|
+
String(value || "").replace(/^(\s*)\d+\.\s+/gm, "$1- ");
|
|
62
|
+
|
|
63
|
+
const parseJsonMessage = (value = "") => {
|
|
64
|
+
const source = String(value || "").trim();
|
|
65
|
+
if (!source) return null;
|
|
66
|
+
if (!(source.startsWith("{") || source.startsWith("["))) return null;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(source);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const extractToolCallsFromPayload = (payload = null) => {
|
|
75
|
+
const normalizedPayload =
|
|
76
|
+
payload && typeof payload === "object" ? payload : {};
|
|
77
|
+
if (
|
|
78
|
+
Array.isArray(normalizedPayload?.toolCalls) &&
|
|
79
|
+
normalizedPayload.toolCalls.length > 0
|
|
80
|
+
) {
|
|
81
|
+
return normalizedPayload.toolCalls;
|
|
82
|
+
}
|
|
83
|
+
const rawParts = Array.isArray(normalizedPayload?.rawMessage?.content)
|
|
84
|
+
? normalizedPayload.rawMessage.content
|
|
85
|
+
: [];
|
|
86
|
+
return rawParts
|
|
87
|
+
.filter((part) => String(part?.type || "").toLowerCase() === "toolcall")
|
|
88
|
+
.map((part) => ({
|
|
89
|
+
id: String(part?.id || ""),
|
|
90
|
+
name: String(part?.name || ""),
|
|
91
|
+
arguments: part?.arguments || null,
|
|
92
|
+
partialJson: String(part?.partialJson || ""),
|
|
93
|
+
}))
|
|
94
|
+
.filter((toolCall) => toolCall.name || toolCall.id);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const normalizeToolResult = (toolResult = null) => {
|
|
98
|
+
if (!toolResult || typeof toolResult !== "object") return null;
|
|
99
|
+
const rawMessage = toolResult?.rawMessage || toolResult;
|
|
100
|
+
if (!rawMessage || typeof rawMessage !== "object") return null;
|
|
101
|
+
const contentParts = Array.isArray(rawMessage?.content) ? rawMessage.content : [];
|
|
102
|
+
const text = contentParts
|
|
103
|
+
.map((part) => String(part?.text || ""))
|
|
104
|
+
.filter((value) => value.length > 0)
|
|
105
|
+
.join("\n")
|
|
106
|
+
.trim();
|
|
107
|
+
return {
|
|
108
|
+
toolCallId: String(rawMessage?.toolCallId || toolResult?.toolCallId || ""),
|
|
109
|
+
toolName: String(rawMessage?.toolName || toolResult?.toolName || ""),
|
|
110
|
+
text,
|
|
111
|
+
isError: Boolean(
|
|
112
|
+
rawMessage?.isError === true ||
|
|
113
|
+
toolResult?.isError === true ||
|
|
114
|
+
String(rawMessage?.status || "").toLowerCase() === "error",
|
|
115
|
+
),
|
|
116
|
+
rawMessage,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const buildToolMessage = ({
|
|
121
|
+
toolCall = null,
|
|
122
|
+
toolResult = null,
|
|
123
|
+
createdAt = Date.now(),
|
|
124
|
+
debugPayload = null,
|
|
125
|
+
} = {}) => {
|
|
126
|
+
const normalizedToolCall =
|
|
127
|
+
toolCall && typeof toolCall === "object" ? toolCall : {};
|
|
128
|
+
const name = String(
|
|
129
|
+
normalizedToolCall?.name || toolResult?.toolName || "unknown",
|
|
130
|
+
);
|
|
131
|
+
return buildMessage({
|
|
132
|
+
role: "tool",
|
|
133
|
+
content: `Tool call: ${name}`,
|
|
134
|
+
createdAt,
|
|
135
|
+
debugPayload:
|
|
136
|
+
debugPayload ||
|
|
137
|
+
({
|
|
138
|
+
timestamp: createdAt,
|
|
139
|
+
metadata: null,
|
|
140
|
+
rawMessage: null,
|
|
141
|
+
toolCalls: normalizedToolCall?.name || normalizedToolCall?.id ? [normalizedToolCall] : [],
|
|
142
|
+
toolResult: toolResult || null,
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const renderMarkdownHtml = (value = "") =>
|
|
148
|
+
marked.parse(
|
|
149
|
+
escapeHtmlForMarkdown(normalizeListMarkers(normalizeMarkdownInput(value))),
|
|
150
|
+
{
|
|
151
|
+
gfm: true,
|
|
152
|
+
breaks: true,
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
157
|
+
const [messagesBySession, setMessagesBySession] = useState({});
|
|
158
|
+
const [draft, setDraft] = useState("");
|
|
159
|
+
const [draftBySession, setDraftBySession] = useState(() => {
|
|
160
|
+
try {
|
|
161
|
+
const rawValue = localStorage.getItem(kChatSessionDraftsStorageKey);
|
|
162
|
+
if (!rawValue) return {};
|
|
163
|
+
const parsed = JSON.parse(rawValue);
|
|
164
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
165
|
+
} catch {
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const [sending, setSending] = useState(false);
|
|
170
|
+
const [streaming, setStreaming] = useState(false);
|
|
171
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
172
|
+
const [rawHistoryBySession, setRawHistoryBySession] = useState({});
|
|
173
|
+
const [debugEventsBySession, setDebugEventsBySession] = useState({});
|
|
174
|
+
const [activeRunBySession, setActiveRunBySession] = useState({});
|
|
175
|
+
const [connectionError, setConnectionError] = useState("");
|
|
176
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
177
|
+
const wsRef = useRef(null);
|
|
178
|
+
const threadRef = useRef(null);
|
|
179
|
+
const reconnectTimerRef = useRef(null);
|
|
180
|
+
const reconnectAttemptsRef = useRef(0);
|
|
181
|
+
const selectedSessionKeyRef = useRef(selectedSessionKey);
|
|
182
|
+
const realtimeDisabledRef = useRef(false);
|
|
183
|
+
const shouldAutoScrollRef = useRef(true);
|
|
184
|
+
const appendDebugEvent = useCallback((sessionKey, label, payload) => {
|
|
185
|
+
const normalizedSessionKey = String(
|
|
186
|
+
sessionKey || selectedSessionKeyRef.current || "",
|
|
187
|
+
);
|
|
188
|
+
if (!normalizedSessionKey) return;
|
|
189
|
+
const nextEvent = {
|
|
190
|
+
id: crypto.randomUUID(),
|
|
191
|
+
at: Date.now(),
|
|
192
|
+
label: String(label || ""),
|
|
193
|
+
payload: payload ?? null,
|
|
194
|
+
};
|
|
195
|
+
setDebugEventsBySession((currentMap) => {
|
|
196
|
+
const existing = currentMap[normalizedSessionKey] || [];
|
|
197
|
+
const nextList = [...existing, nextEvent].slice(-30);
|
|
198
|
+
return {
|
|
199
|
+
...currentMap,
|
|
200
|
+
[normalizedSessionKey]: nextList,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}, []);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
selectedSessionKeyRef.current = selectedSessionKey;
|
|
207
|
+
}, [selectedSessionKey]);
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!selectedSessionKey) return;
|
|
211
|
+
setDraft(String(draftBySession[selectedSessionKey] || ""));
|
|
212
|
+
}, [draftBySession, selectedSessionKey]);
|
|
213
|
+
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
try {
|
|
216
|
+
localStorage.setItem(
|
|
217
|
+
kChatSessionDraftsStorageKey,
|
|
218
|
+
JSON.stringify(draftBySession),
|
|
219
|
+
);
|
|
220
|
+
} catch {}
|
|
221
|
+
}, [draftBySession]);
|
|
222
|
+
|
|
223
|
+
const selectedSession = useMemo(
|
|
224
|
+
() =>
|
|
225
|
+
sessions.find(
|
|
226
|
+
(sessionRow) =>
|
|
227
|
+
String(sessionRow?.key || "") === String(selectedSessionKey || ""),
|
|
228
|
+
) || null,
|
|
229
|
+
[selectedSessionKey, sessions],
|
|
230
|
+
);
|
|
231
|
+
const chatDebugEnabled = useMemo(() => {
|
|
232
|
+
try {
|
|
233
|
+
const params = new URLSearchParams(window.location.search || "");
|
|
234
|
+
return params.get(kChatDebugQueryFlag) === "1";
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
const messages = useMemo(
|
|
241
|
+
() => messagesBySession[selectedSessionKey] || [],
|
|
242
|
+
[messagesBySession, selectedSessionKey],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
const handleNewChat = () => {
|
|
247
|
+
if (!selectedSessionKey) return;
|
|
248
|
+
setMessagesBySession((currentMap) => ({
|
|
249
|
+
...currentMap,
|
|
250
|
+
[selectedSessionKey]: [],
|
|
251
|
+
}));
|
|
252
|
+
setDraft("");
|
|
253
|
+
setDraftBySession((currentMap) => ({
|
|
254
|
+
...currentMap,
|
|
255
|
+
[selectedSessionKey]: "",
|
|
256
|
+
}));
|
|
257
|
+
};
|
|
258
|
+
window.addEventListener(kNewChatEventName, handleNewChat);
|
|
259
|
+
return () => {
|
|
260
|
+
window.removeEventListener(kNewChatEventName, handleNewChat);
|
|
261
|
+
};
|
|
262
|
+
}, [selectedSessionKey]);
|
|
263
|
+
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
let mounted = true;
|
|
266
|
+
|
|
267
|
+
const connect = () => {
|
|
268
|
+
if (realtimeDisabledRef.current) return;
|
|
269
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
270
|
+
const ws = new WebSocket(
|
|
271
|
+
`${protocol}//${window.location.host}/api/ws/chat`,
|
|
272
|
+
);
|
|
273
|
+
wsRef.current = ws;
|
|
274
|
+
|
|
275
|
+
ws.onopen = () => {
|
|
276
|
+
if (!mounted) return;
|
|
277
|
+
setIsConnected(true);
|
|
278
|
+
setConnectionError("");
|
|
279
|
+
reconnectAttemptsRef.current = 0;
|
|
280
|
+
const currentSessionKey = String(selectedSessionKeyRef.current || "");
|
|
281
|
+
if (currentSessionKey) {
|
|
282
|
+
setHistoryLoading(true);
|
|
283
|
+
ws.send(
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
type: "history",
|
|
286
|
+
sessionKey: currentSessionKey,
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
ws.onclose = () => {
|
|
293
|
+
if (!mounted) return;
|
|
294
|
+
setIsConnected(false);
|
|
295
|
+
setStreaming(false);
|
|
296
|
+
setSending(false);
|
|
297
|
+
setHistoryLoading(false);
|
|
298
|
+
if (realtimeDisabledRef.current) return;
|
|
299
|
+
if (reconnectAttemptsRef.current >= kWsReconnectMaxAttempts) return;
|
|
300
|
+
const delayMs = Math.min(
|
|
301
|
+
1000 * 2 ** reconnectAttemptsRef.current,
|
|
302
|
+
5000,
|
|
303
|
+
);
|
|
304
|
+
reconnectAttemptsRef.current += 1;
|
|
305
|
+
setConnectionError("Realtime chat socket disconnected.");
|
|
306
|
+
reconnectTimerRef.current = setTimeout(connect, delayMs);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
ws.onerror = () => {
|
|
310
|
+
if (!mounted) return;
|
|
311
|
+
setIsConnected(false);
|
|
312
|
+
setHistoryLoading(false);
|
|
313
|
+
setConnectionError("Realtime chat socket failed to connect.");
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
ws.onmessage = (event) => {
|
|
317
|
+
let payload = null;
|
|
318
|
+
try {
|
|
319
|
+
payload = JSON.parse(String(event?.data || ""));
|
|
320
|
+
} catch {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!payload || typeof payload !== "object") return;
|
|
324
|
+
appendDebugEvent(
|
|
325
|
+
String(payload.sessionKey || selectedSessionKeyRef.current || ""),
|
|
326
|
+
`ws:${String(payload.type || "unknown")}`,
|
|
327
|
+
payload,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (payload.type === "history") {
|
|
331
|
+
const historySessionKey = String(payload.sessionKey || "");
|
|
332
|
+
if (!historySessionKey) return;
|
|
333
|
+
const historyMessages = (
|
|
334
|
+
Array.isArray(payload.messages) ? payload.messages : []
|
|
335
|
+
)
|
|
336
|
+
.map((messageRow) =>
|
|
337
|
+
buildMessage({
|
|
338
|
+
role: String(messageRow?.role || "assistant"),
|
|
339
|
+
content: String(messageRow?.content || ""),
|
|
340
|
+
createdAt: Number(messageRow?.timestamp) || Date.now(),
|
|
341
|
+
debugPayload: messageRow || null,
|
|
342
|
+
}),
|
|
343
|
+
)
|
|
344
|
+
.filter(
|
|
345
|
+
(messageRow) =>
|
|
346
|
+
String(messageRow.content || "").trim() ||
|
|
347
|
+
extractToolCallsFromPayload(messageRow?.debugPayload).length >
|
|
348
|
+
0,
|
|
349
|
+
);
|
|
350
|
+
setMessagesBySession((currentMap) => ({
|
|
351
|
+
...currentMap,
|
|
352
|
+
[historySessionKey]: historyMessages,
|
|
353
|
+
}));
|
|
354
|
+
setRawHistoryBySession((currentMap) => ({
|
|
355
|
+
...currentMap,
|
|
356
|
+
[historySessionKey]: payload.rawHistory || null,
|
|
357
|
+
}));
|
|
358
|
+
setHistoryLoading(false);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (payload.type === "chunk") {
|
|
363
|
+
const chunkSessionKey = String(
|
|
364
|
+
payload.sessionKey || selectedSessionKeyRef.current || "",
|
|
365
|
+
);
|
|
366
|
+
const messageId = String(payload.messageId || "");
|
|
367
|
+
const chunkText = String(payload.content || "");
|
|
368
|
+
if (!chunkSessionKey || !messageId) return;
|
|
369
|
+
setSending(false);
|
|
370
|
+
setStreaming(true);
|
|
371
|
+
setMessagesBySession((currentMap) => {
|
|
372
|
+
const currentMessages = currentMap[chunkSessionKey] || [];
|
|
373
|
+
const lastMessage = currentMessages[currentMessages.length - 1];
|
|
374
|
+
if (
|
|
375
|
+
lastMessage &&
|
|
376
|
+
lastMessage.role === "assistant" &&
|
|
377
|
+
String(lastMessage.id || "") === messageId
|
|
378
|
+
) {
|
|
379
|
+
return {
|
|
380
|
+
...currentMap,
|
|
381
|
+
[chunkSessionKey]: [
|
|
382
|
+
...currentMessages.slice(0, -1),
|
|
383
|
+
{
|
|
384
|
+
...lastMessage,
|
|
385
|
+
content: `${String(lastMessage.content || "")}${chunkText}`,
|
|
386
|
+
debugPayload: {
|
|
387
|
+
...(lastMessage?.debugPayload || {}),
|
|
388
|
+
source: "stream",
|
|
389
|
+
messageId,
|
|
390
|
+
sessionKey: chunkSessionKey,
|
|
391
|
+
chunkCount:
|
|
392
|
+
Number(lastMessage?.debugPayload?.chunkCount || 1) + 1,
|
|
393
|
+
lastChunk: chunkText,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
...currentMap,
|
|
401
|
+
[chunkSessionKey]: [
|
|
402
|
+
...currentMessages,
|
|
403
|
+
{
|
|
404
|
+
id: messageId,
|
|
405
|
+
role: "assistant",
|
|
406
|
+
content: chunkText,
|
|
407
|
+
createdAt: Date.now(),
|
|
408
|
+
debugPayload: {
|
|
409
|
+
source: "stream",
|
|
410
|
+
messageId,
|
|
411
|
+
sessionKey: chunkSessionKey,
|
|
412
|
+
chunkCount: 1,
|
|
413
|
+
lastChunk: chunkText,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (payload.type === "tool") {
|
|
423
|
+
const toolSessionKey = String(
|
|
424
|
+
payload.sessionKey || selectedSessionKeyRef.current || "",
|
|
425
|
+
);
|
|
426
|
+
if (!toolSessionKey) return;
|
|
427
|
+
const toolPhase = String(payload.phase || "").toLowerCase();
|
|
428
|
+
const toolCall =
|
|
429
|
+
payload?.toolCall && typeof payload.toolCall === "object"
|
|
430
|
+
? payload.toolCall
|
|
431
|
+
: null;
|
|
432
|
+
const toolResult =
|
|
433
|
+
payload?.toolResult && typeof payload.toolResult === "object"
|
|
434
|
+
? payload.toolResult
|
|
435
|
+
: null;
|
|
436
|
+
const toolCallId = String(
|
|
437
|
+
toolCall?.id || toolResult?.toolCallId || payload?.toolCallId || "",
|
|
438
|
+
);
|
|
439
|
+
const toolTimestamp = Number(payload.timestamp) || Date.now();
|
|
440
|
+
setMessagesBySession((currentMap) => {
|
|
441
|
+
const currentMessages = currentMap[toolSessionKey] || [];
|
|
442
|
+
if (toolPhase === "result") {
|
|
443
|
+
let matched = false;
|
|
444
|
+
const nextMessages = currentMessages.map((messageRow) => {
|
|
445
|
+
if (matched || messageRow.role !== "tool") return messageRow;
|
|
446
|
+
const messageToolCalls = extractToolCallsFromPayload(
|
|
447
|
+
messageRow.debugPayload,
|
|
448
|
+
);
|
|
449
|
+
const messageToolCallId = String(messageToolCalls?.[0]?.id || "");
|
|
450
|
+
const messageToolName = String(messageToolCalls?.[0]?.name || "");
|
|
451
|
+
const resultToolName = String(toolResult?.toolName || "");
|
|
452
|
+
const hasResultAlready = Boolean(
|
|
453
|
+
normalizeToolResult(messageRow?.debugPayload?.toolResult),
|
|
454
|
+
);
|
|
455
|
+
const shouldMatchById =
|
|
456
|
+
toolCallId && messageToolCallId && messageToolCallId === toolCallId;
|
|
457
|
+
const shouldMatchByNameFallback =
|
|
458
|
+
!toolCallId &&
|
|
459
|
+
!messageToolCallId &&
|
|
460
|
+
resultToolName &&
|
|
461
|
+
messageToolName === resultToolName &&
|
|
462
|
+
!hasResultAlready;
|
|
463
|
+
if (!shouldMatchById && !shouldMatchByNameFallback) {
|
|
464
|
+
return messageRow;
|
|
465
|
+
}
|
|
466
|
+
matched = true;
|
|
467
|
+
return {
|
|
468
|
+
...messageRow,
|
|
469
|
+
debugPayload: {
|
|
470
|
+
...(messageRow.debugPayload || {}),
|
|
471
|
+
toolResult: toolResult || null,
|
|
472
|
+
rawEvent: payload?.rawEvent || null,
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
if (matched) {
|
|
477
|
+
return {
|
|
478
|
+
...currentMap,
|
|
479
|
+
[toolSessionKey]: nextMessages,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (toolPhase === "call" && toolCall) {
|
|
484
|
+
const duplicateCall = currentMessages.some((messageRow) => {
|
|
485
|
+
if (messageRow.role !== "tool") return false;
|
|
486
|
+
const existingCall = extractToolCallsFromPayload(
|
|
487
|
+
messageRow.debugPayload,
|
|
488
|
+
)[0];
|
|
489
|
+
if (!existingCall) return false;
|
|
490
|
+
const existingId = String(existingCall?.id || "");
|
|
491
|
+
if (toolCallId && existingId && existingId === toolCallId) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
const existingName = String(existingCall?.name || "");
|
|
495
|
+
const incomingName = String(toolCall?.name || "");
|
|
496
|
+
return !toolCallId && existingName && incomingName && existingName === incomingName;
|
|
497
|
+
});
|
|
498
|
+
if (duplicateCall) return currentMap;
|
|
499
|
+
return {
|
|
500
|
+
...currentMap,
|
|
501
|
+
[toolSessionKey]: [
|
|
502
|
+
...currentMessages,
|
|
503
|
+
buildToolMessage({
|
|
504
|
+
toolCall,
|
|
505
|
+
createdAt: toolTimestamp,
|
|
506
|
+
debugPayload: {
|
|
507
|
+
timestamp: toolTimestamp,
|
|
508
|
+
metadata: null,
|
|
509
|
+
rawMessage: null,
|
|
510
|
+
toolCalls: [toolCall],
|
|
511
|
+
toolResult: null,
|
|
512
|
+
rawEvent: payload?.rawEvent || null,
|
|
513
|
+
},
|
|
514
|
+
}),
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
if (toolPhase === "result" && toolResult) {
|
|
519
|
+
return {
|
|
520
|
+
...currentMap,
|
|
521
|
+
[toolSessionKey]: [
|
|
522
|
+
...currentMessages,
|
|
523
|
+
buildToolMessage({
|
|
524
|
+
toolCall: toolCall || {
|
|
525
|
+
id: String(toolResult?.toolCallId || ""),
|
|
526
|
+
name: String(toolResult?.toolName || "unknown"),
|
|
527
|
+
arguments: null,
|
|
528
|
+
partialJson: "",
|
|
529
|
+
},
|
|
530
|
+
toolResult,
|
|
531
|
+
createdAt: toolTimestamp,
|
|
532
|
+
debugPayload: {
|
|
533
|
+
timestamp: toolTimestamp,
|
|
534
|
+
metadata: null,
|
|
535
|
+
rawMessage: null,
|
|
536
|
+
toolCalls: toolCall ? [toolCall] : [],
|
|
537
|
+
toolResult,
|
|
538
|
+
rawEvent: payload?.rawEvent || null,
|
|
539
|
+
},
|
|
540
|
+
}),
|
|
541
|
+
],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return currentMap;
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (payload.type === "started") {
|
|
550
|
+
const nextSessionKey = String(
|
|
551
|
+
payload.sessionKey || selectedSessionKeyRef.current || "",
|
|
552
|
+
);
|
|
553
|
+
const runId = String(payload.runId || "");
|
|
554
|
+
if (!nextSessionKey || !runId) return;
|
|
555
|
+
setActiveRunBySession((currentMap) => ({
|
|
556
|
+
...currentMap,
|
|
557
|
+
[nextSessionKey]: runId,
|
|
558
|
+
}));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (payload.type === "done") {
|
|
563
|
+
const doneSessionKey = String(
|
|
564
|
+
payload.sessionKey || selectedSessionKeyRef.current || "",
|
|
565
|
+
);
|
|
566
|
+
if (doneSessionKey) {
|
|
567
|
+
setActiveRunBySession((currentMap) => {
|
|
568
|
+
const nextMap = { ...currentMap };
|
|
569
|
+
delete nextMap[doneSessionKey];
|
|
570
|
+
return nextMap;
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
setSending(false);
|
|
574
|
+
setStreaming(false);
|
|
575
|
+
setHistoryLoading(false);
|
|
576
|
+
if (doneSessionKey && ws && ws.readyState === 1) {
|
|
577
|
+
setHistoryLoading(true);
|
|
578
|
+
appendDebugEvent(doneSessionKey, "ws:history-request-after-done", {
|
|
579
|
+
type: "history",
|
|
580
|
+
sessionKey: doneSessionKey,
|
|
581
|
+
});
|
|
582
|
+
ws.send(
|
|
583
|
+
JSON.stringify({
|
|
584
|
+
type: "history",
|
|
585
|
+
sessionKey: doneSessionKey,
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (payload.type === "error") {
|
|
593
|
+
setSending(false);
|
|
594
|
+
setStreaming(false);
|
|
595
|
+
setHistoryLoading(false);
|
|
596
|
+
const errorSessionKey = String(
|
|
597
|
+
payload.sessionKey || selectedSessionKeyRef.current || "",
|
|
598
|
+
);
|
|
599
|
+
if (errorSessionKey) {
|
|
600
|
+
setActiveRunBySession((currentMap) => {
|
|
601
|
+
const nextMap = { ...currentMap };
|
|
602
|
+
delete nextMap[errorSessionKey];
|
|
603
|
+
return nextMap;
|
|
604
|
+
});
|
|
605
|
+
setMessagesBySession((currentMap) => ({
|
|
606
|
+
...currentMap,
|
|
607
|
+
[errorSessionKey]: [
|
|
608
|
+
...(currentMap[errorSessionKey] || []),
|
|
609
|
+
buildMessage({
|
|
610
|
+
role: "assistant",
|
|
611
|
+
content:
|
|
612
|
+
String(payload.message || "").trim() ||
|
|
613
|
+
"Something went wrong.",
|
|
614
|
+
}),
|
|
615
|
+
],
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
if (payload.message) showToast(String(payload.message), "error");
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
connect();
|
|
624
|
+
|
|
625
|
+
return () => {
|
|
626
|
+
mounted = false;
|
|
627
|
+
if (reconnectTimerRef.current) {
|
|
628
|
+
clearTimeout(reconnectTimerRef.current);
|
|
629
|
+
}
|
|
630
|
+
const ws = wsRef.current;
|
|
631
|
+
wsRef.current = null;
|
|
632
|
+
if (ws) ws.close();
|
|
633
|
+
};
|
|
634
|
+
}, []);
|
|
635
|
+
|
|
636
|
+
useEffect(() => {
|
|
637
|
+
if (!selectedSessionKey) return;
|
|
638
|
+
const ws = wsRef.current;
|
|
639
|
+
if (ws && ws.readyState === 1) {
|
|
640
|
+
setHistoryLoading(true);
|
|
641
|
+
appendDebugEvent(selectedSessionKey, "ws:history-request", {
|
|
642
|
+
type: "history",
|
|
643
|
+
sessionKey: selectedSessionKey,
|
|
644
|
+
});
|
|
645
|
+
ws.send(
|
|
646
|
+
JSON.stringify({
|
|
647
|
+
type: "history",
|
|
648
|
+
sessionKey: selectedSessionKey,
|
|
649
|
+
}),
|
|
650
|
+
);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
// Fallback for environments where websocket upgrade is unavailable:
|
|
654
|
+
// load history over HTTP so the UI can still show prior messages.
|
|
655
|
+
let cancelled = false;
|
|
656
|
+
const loadHistory = async () => {
|
|
657
|
+
try {
|
|
658
|
+
setHistoryLoading(true);
|
|
659
|
+
const response = await authFetch(
|
|
660
|
+
`/api/chat/history?sessionKey=${encodeURIComponent(selectedSessionKey)}`,
|
|
661
|
+
);
|
|
662
|
+
const payload = await response.json();
|
|
663
|
+
if (cancelled) return;
|
|
664
|
+
if (!response.ok || payload?.ok === false) {
|
|
665
|
+
throw new Error(payload?.error || "Could not load chat history");
|
|
666
|
+
}
|
|
667
|
+
appendDebugEvent(selectedSessionKey, "http:history-response", payload);
|
|
668
|
+
const historyMessages = (
|
|
669
|
+
Array.isArray(payload.messages) ? payload.messages : []
|
|
670
|
+
)
|
|
671
|
+
.map((messageRow) =>
|
|
672
|
+
buildMessage({
|
|
673
|
+
role: String(messageRow?.role || "assistant"),
|
|
674
|
+
content: String(messageRow?.content || ""),
|
|
675
|
+
createdAt: Number(messageRow?.timestamp) || Date.now(),
|
|
676
|
+
debugPayload: messageRow || null,
|
|
677
|
+
}),
|
|
678
|
+
)
|
|
679
|
+
.filter(
|
|
680
|
+
(messageRow) =>
|
|
681
|
+
String(messageRow.content || "").trim() ||
|
|
682
|
+
extractToolCallsFromPayload(messageRow?.debugPayload).length > 0,
|
|
683
|
+
);
|
|
684
|
+
setMessagesBySession((currentMap) => ({
|
|
685
|
+
...currentMap,
|
|
686
|
+
[selectedSessionKey]: historyMessages,
|
|
687
|
+
}));
|
|
688
|
+
setRawHistoryBySession((currentMap) => ({
|
|
689
|
+
...currentMap,
|
|
690
|
+
[selectedSessionKey]: payload.rawHistory || null,
|
|
691
|
+
}));
|
|
692
|
+
if (!isConnected) {
|
|
693
|
+
// If HTTP history works while WS is down, stop noisy reconnect loops.
|
|
694
|
+
realtimeDisabledRef.current = true;
|
|
695
|
+
if (reconnectTimerRef.current) {
|
|
696
|
+
clearTimeout(reconnectTimerRef.current);
|
|
697
|
+
reconnectTimerRef.current = null;
|
|
698
|
+
}
|
|
699
|
+
const ws = wsRef.current;
|
|
700
|
+
if (ws) ws.close();
|
|
701
|
+
setConnectionError("Realtime unavailable; using HTTP fallback.");
|
|
702
|
+
}
|
|
703
|
+
} catch (err) {
|
|
704
|
+
if (cancelled) return;
|
|
705
|
+
const errorMessage = err.message || "Could not load chat history.";
|
|
706
|
+
appendDebugEvent(selectedSessionKey, "http:history-error", {
|
|
707
|
+
error: errorMessage,
|
|
708
|
+
});
|
|
709
|
+
if (
|
|
710
|
+
errorMessage.toLowerCase().includes("runtime unavailable") ||
|
|
711
|
+
errorMessage.toLowerCase().includes("websocket unavailable")
|
|
712
|
+
) {
|
|
713
|
+
realtimeDisabledRef.current = true;
|
|
714
|
+
if (reconnectTimerRef.current) {
|
|
715
|
+
clearTimeout(reconnectTimerRef.current);
|
|
716
|
+
reconnectTimerRef.current = null;
|
|
717
|
+
}
|
|
718
|
+
const ws = wsRef.current;
|
|
719
|
+
if (ws) ws.close();
|
|
720
|
+
setConnectionError(
|
|
721
|
+
"Chat runtime unavailable (missing server dependency).",
|
|
722
|
+
);
|
|
723
|
+
} else {
|
|
724
|
+
setConnectionError(errorMessage);
|
|
725
|
+
}
|
|
726
|
+
} finally {
|
|
727
|
+
if (!cancelled) setHistoryLoading(false);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
loadHistory();
|
|
731
|
+
return () => {
|
|
732
|
+
cancelled = true;
|
|
733
|
+
};
|
|
734
|
+
}, [isConnected, selectedSessionKey]);
|
|
735
|
+
|
|
736
|
+
const handleThreadScroll = useCallback(() => {
|
|
737
|
+
const threadElement = threadRef.current;
|
|
738
|
+
if (!threadElement) return;
|
|
739
|
+
const distanceFromBottom =
|
|
740
|
+
threadElement.scrollHeight -
|
|
741
|
+
threadElement.scrollTop -
|
|
742
|
+
threadElement.clientHeight;
|
|
743
|
+
shouldAutoScrollRef.current =
|
|
744
|
+
distanceFromBottom <= kAutoscrollBottomThresholdPx;
|
|
745
|
+
}, []);
|
|
746
|
+
|
|
747
|
+
useEffect(() => {
|
|
748
|
+
const threadElement = threadRef.current;
|
|
749
|
+
if (!threadElement) return;
|
|
750
|
+
if (!shouldAutoScrollRef.current) return;
|
|
751
|
+
threadElement.scrollTop = threadElement.scrollHeight;
|
|
752
|
+
}, [messages, historyLoading, streaming]);
|
|
753
|
+
|
|
754
|
+
const handleDraftInput = useCallback(
|
|
755
|
+
(event) => {
|
|
756
|
+
const nextValue = String(event?.target?.value || "");
|
|
757
|
+
setDraft(nextValue);
|
|
758
|
+
if (!selectedSessionKey) return;
|
|
759
|
+
setDraftBySession((currentMap) => ({
|
|
760
|
+
...currentMap,
|
|
761
|
+
[selectedSessionKey]: nextValue,
|
|
762
|
+
}));
|
|
763
|
+
},
|
|
764
|
+
[selectedSessionKey],
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
const handleSend = useCallback(() => {
|
|
768
|
+
const messageText = String(draft || "").trim();
|
|
769
|
+
const ws = wsRef.current;
|
|
770
|
+
if (!messageText || !selectedSessionKey || sending || streaming) return;
|
|
771
|
+
if (!ws || ws.readyState !== 1) {
|
|
772
|
+
showToast(
|
|
773
|
+
"Chat websocket is unavailable in this environment.",
|
|
774
|
+
"warning",
|
|
775
|
+
);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const userMessage = buildMessage({
|
|
780
|
+
role: "user",
|
|
781
|
+
content: messageText,
|
|
782
|
+
debugPayload: {
|
|
783
|
+
source: "composer",
|
|
784
|
+
type: "message",
|
|
785
|
+
content: messageText,
|
|
786
|
+
sessionKey: selectedSessionKey,
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
setDraft("");
|
|
790
|
+
setDraftBySession((currentMap) => ({
|
|
791
|
+
...currentMap,
|
|
792
|
+
[selectedSessionKey]: "",
|
|
793
|
+
}));
|
|
794
|
+
setSending(true);
|
|
795
|
+
setMessagesBySession((currentMap) => ({
|
|
796
|
+
...currentMap,
|
|
797
|
+
[selectedSessionKey]: [
|
|
798
|
+
...(currentMap[selectedSessionKey] || []),
|
|
799
|
+
userMessage,
|
|
800
|
+
],
|
|
801
|
+
}));
|
|
802
|
+
setStreaming(true);
|
|
803
|
+
ws.send(
|
|
804
|
+
JSON.stringify({
|
|
805
|
+
type: "message",
|
|
806
|
+
content: messageText,
|
|
807
|
+
sessionKey: selectedSessionKey,
|
|
808
|
+
}),
|
|
809
|
+
);
|
|
810
|
+
appendDebugEvent(selectedSessionKey, "ws:message-request", {
|
|
811
|
+
type: "message",
|
|
812
|
+
content: messageText,
|
|
813
|
+
sessionKey: selectedSessionKey,
|
|
814
|
+
});
|
|
815
|
+
}, [appendDebugEvent, draft, selectedSessionKey, sending, streaming]);
|
|
816
|
+
|
|
817
|
+
const handleStop = useCallback(() => {
|
|
818
|
+
const ws = wsRef.current;
|
|
819
|
+
if (!ws || ws.readyState !== 1 || !selectedSessionKey) return;
|
|
820
|
+
ws.send(
|
|
821
|
+
JSON.stringify({
|
|
822
|
+
type: "stop",
|
|
823
|
+
sessionKey: selectedSessionKey,
|
|
824
|
+
}),
|
|
825
|
+
);
|
|
826
|
+
appendDebugEvent(selectedSessionKey, "ws:stop-request", {
|
|
827
|
+
type: "stop",
|
|
828
|
+
sessionKey: selectedSessionKey,
|
|
829
|
+
});
|
|
830
|
+
setStreaming(false);
|
|
831
|
+
setSending(false);
|
|
832
|
+
}, [appendDebugEvent, selectedSessionKey]);
|
|
833
|
+
|
|
834
|
+
const rawHistory = selectedSessionKey
|
|
835
|
+
? rawHistoryBySession[selectedSessionKey]
|
|
836
|
+
: null;
|
|
837
|
+
const debugEvents = selectedSessionKey
|
|
838
|
+
? debugEventsBySession[selectedSessionKey] || []
|
|
839
|
+
: [];
|
|
840
|
+
|
|
841
|
+
return html`
|
|
842
|
+
<div class="chat-route-shell">
|
|
843
|
+
<div class="chat-route-header">
|
|
844
|
+
<div>
|
|
845
|
+
<div class="chat-route-title">Chat</div>
|
|
846
|
+
<div class="chat-route-subtitle">
|
|
847
|
+
${selectedSession?.label || "Pick a session in the sidebar"}
|
|
848
|
+
</div>
|
|
849
|
+
${connectionError
|
|
850
|
+
? html`<div class="chat-route-warning">${connectionError}</div>`
|
|
851
|
+
: null}
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
<div class="chat-thread" ref=${threadRef} onscroll=${handleThreadScroll}>
|
|
855
|
+
${!selectedSessionKey
|
|
856
|
+
? html`<div class="chat-empty-state">
|
|
857
|
+
Select a session to begin chatting.
|
|
858
|
+
</div>`
|
|
859
|
+
: historyLoading
|
|
860
|
+
? html`<div class="chat-empty-state">Loading history...</div>`
|
|
861
|
+
: messages.length === 0
|
|
862
|
+
? html`<div class="chat-empty-state">
|
|
863
|
+
Start a message in this session.
|
|
864
|
+
</div>`
|
|
865
|
+
: messages.map(
|
|
866
|
+
(message) => html`
|
|
867
|
+
${(() => {
|
|
868
|
+
const toolCalls = extractToolCallsFromPayload(
|
|
869
|
+
message.debugPayload,
|
|
870
|
+
);
|
|
871
|
+
const hasVisibleContent =
|
|
872
|
+
String(message.content || "").trim().length > 0;
|
|
873
|
+
const isToolMessage = message.role === "tool";
|
|
874
|
+
const shouldRenderContent =
|
|
875
|
+
hasVisibleContent &&
|
|
876
|
+
!isToolMessage &&
|
|
877
|
+
!(
|
|
878
|
+
toolCalls.length > 0 &&
|
|
879
|
+
String(message.content || "").startsWith(
|
|
880
|
+
"Tool calls:",
|
|
881
|
+
)
|
|
882
|
+
);
|
|
883
|
+
const primaryToolCall = toolCalls[0] || null;
|
|
884
|
+
const matchedResult = normalizeToolResult(
|
|
885
|
+
message?.debugPayload?.toolResult || null,
|
|
886
|
+
);
|
|
887
|
+
return html`
|
|
888
|
+
<div
|
|
889
|
+
key=${message.id}
|
|
890
|
+
class=${`chat-bubble ${message.role === "user" ? "is-user" : "is-assistant"}`}
|
|
891
|
+
>
|
|
892
|
+
${!isToolMessage
|
|
893
|
+
? html`
|
|
894
|
+
<div class="chat-bubble-meta">
|
|
895
|
+
<span
|
|
896
|
+
>${message.role === "user"
|
|
897
|
+
? "You"
|
|
898
|
+
: "Agent"}</span
|
|
899
|
+
>
|
|
900
|
+
<span
|
|
901
|
+
>${formatChatTime(message.createdAt)}</span
|
|
902
|
+
>
|
|
903
|
+
</div>
|
|
904
|
+
`
|
|
905
|
+
: null}
|
|
906
|
+
${isToolMessage && primaryToolCall
|
|
907
|
+
? html`
|
|
908
|
+
<details class="chat-tool-inline-message">
|
|
909
|
+
<summary>
|
|
910
|
+
<span class="chat-tool-inline-icon"
|
|
911
|
+
>🛠️</span
|
|
912
|
+
>
|
|
913
|
+
<span class="chat-tool-inline-title"
|
|
914
|
+
>${String(
|
|
915
|
+
primaryToolCall?.name || "unknown",
|
|
916
|
+
)}</span
|
|
917
|
+
>
|
|
918
|
+
<span class="chat-tool-inline-time"
|
|
919
|
+
>${formatChatTime(
|
|
920
|
+
message.createdAt,
|
|
921
|
+
)}</span
|
|
922
|
+
>
|
|
923
|
+
</summary>
|
|
924
|
+
<div class="chat-tool-inline-body">
|
|
925
|
+
<div class="chat-tool-inline-label">
|
|
926
|
+
Payload
|
|
927
|
+
</div>
|
|
928
|
+
<pre>
|
|
929
|
+
${JSON.stringify(
|
|
930
|
+
{
|
|
931
|
+
id:
|
|
932
|
+
String(primaryToolCall?.id || "") ||
|
|
933
|
+
null,
|
|
934
|
+
name:
|
|
935
|
+
String(
|
|
936
|
+
primaryToolCall?.name || "",
|
|
937
|
+
) || null,
|
|
938
|
+
arguments:
|
|
939
|
+
primaryToolCall?.arguments || null,
|
|
940
|
+
partialJson:
|
|
941
|
+
String(
|
|
942
|
+
primaryToolCall?.partialJson ||
|
|
943
|
+
"",
|
|
944
|
+
) || null,
|
|
945
|
+
},
|
|
946
|
+
null,
|
|
947
|
+
2,
|
|
948
|
+
)}</pre
|
|
949
|
+
>
|
|
950
|
+
${matchedResult
|
|
951
|
+
? html`
|
|
952
|
+
<div class="chat-tool-inline-label">
|
|
953
|
+
Result${matchedResult.isError
|
|
954
|
+
? " (error)"
|
|
955
|
+
: ""}
|
|
956
|
+
</div>
|
|
957
|
+
<pre>
|
|
958
|
+
${JSON.stringify(
|
|
959
|
+
{
|
|
960
|
+
toolCallId:
|
|
961
|
+
matchedResult.toolCallId,
|
|
962
|
+
toolName:
|
|
963
|
+
matchedResult.toolName,
|
|
964
|
+
text: matchedResult.text || "",
|
|
965
|
+
isError: matchedResult.isError,
|
|
966
|
+
rawMessage:
|
|
967
|
+
matchedResult.rawMessage ||
|
|
968
|
+
null,
|
|
969
|
+
},
|
|
970
|
+
null,
|
|
971
|
+
2,
|
|
972
|
+
)}</pre
|
|
973
|
+
>
|
|
974
|
+
`
|
|
975
|
+
: null}
|
|
976
|
+
</div>
|
|
977
|
+
</details>
|
|
978
|
+
`
|
|
979
|
+
: null}
|
|
980
|
+
${shouldRenderContent
|
|
981
|
+
? (() => {
|
|
982
|
+
const parsedJson = parseJsonMessage(
|
|
983
|
+
message.content,
|
|
984
|
+
);
|
|
985
|
+
if (parsedJson) {
|
|
986
|
+
return html`<pre
|
|
987
|
+
class="chat-bubble-content chat-bubble-json"
|
|
988
|
+
>
|
|
989
|
+
${JSON.stringify(parsedJson, null, 2)}</pre
|
|
990
|
+
>`;
|
|
991
|
+
}
|
|
992
|
+
return html`
|
|
993
|
+
<div
|
|
994
|
+
class="chat-bubble-content chat-bubble-markdown"
|
|
995
|
+
dangerouslySetInnerHTML=${{
|
|
996
|
+
__html: renderMarkdownHtml(
|
|
997
|
+
message.content,
|
|
998
|
+
),
|
|
999
|
+
}}
|
|
1000
|
+
></div>
|
|
1001
|
+
`;
|
|
1002
|
+
})()
|
|
1003
|
+
: null}
|
|
1004
|
+
${!isToolMessage
|
|
1005
|
+
? html`
|
|
1006
|
+
<details class="chat-message-json">
|
|
1007
|
+
<summary>JSON</summary>
|
|
1008
|
+
<pre>
|
|
1009
|
+
${JSON.stringify(
|
|
1010
|
+
message.debugPayload || {
|
|
1011
|
+
role: message.role,
|
|
1012
|
+
content: message.content,
|
|
1013
|
+
createdAt: message.createdAt,
|
|
1014
|
+
},
|
|
1015
|
+
null,
|
|
1016
|
+
2,
|
|
1017
|
+
)}</pre
|
|
1018
|
+
>
|
|
1019
|
+
</details>
|
|
1020
|
+
`
|
|
1021
|
+
: null}
|
|
1022
|
+
</div>
|
|
1023
|
+
`;
|
|
1024
|
+
})()}
|
|
1025
|
+
`,
|
|
1026
|
+
)}
|
|
1027
|
+
${selectedSessionKey && (sending || streaming)
|
|
1028
|
+
? html`
|
|
1029
|
+
<div class="chat-bubble is-assistant chat-typing-indicator">
|
|
1030
|
+
<div class="chat-bubble-meta">
|
|
1031
|
+
<span>Agent</span>
|
|
1032
|
+
<span>${isConnected ? "typing..." : "reconnecting..."}</span>
|
|
1033
|
+
</div>
|
|
1034
|
+
<div class="chat-typing-dots">
|
|
1035
|
+
<span></span><span></span><span></span>
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
`
|
|
1039
|
+
: null}
|
|
1040
|
+
${selectedSessionKey
|
|
1041
|
+
? chatDebugEnabled
|
|
1042
|
+
? html`
|
|
1043
|
+
<details class="chat-raw-debug">
|
|
1044
|
+
<summary>Raw history JSON</summary>
|
|
1045
|
+
<pre>${JSON.stringify(rawHistory || null, null, 2)}</pre>
|
|
1046
|
+
</details>
|
|
1047
|
+
<details class="chat-raw-debug">
|
|
1048
|
+
<summary>Inbound event log</summary>
|
|
1049
|
+
<pre>${JSON.stringify(debugEvents, null, 2)}</pre>
|
|
1050
|
+
</details>
|
|
1051
|
+
`
|
|
1052
|
+
: null
|
|
1053
|
+
: null}
|
|
1054
|
+
</div>
|
|
1055
|
+
<div class="chat-composer">
|
|
1056
|
+
<textarea
|
|
1057
|
+
class="chat-composer-input"
|
|
1058
|
+
placeholder=${selectedSessionKey
|
|
1059
|
+
? "Type a message..."
|
|
1060
|
+
: "Select a session to start"}
|
|
1061
|
+
value=${draft}
|
|
1062
|
+
disabled=${!selectedSessionKey || sending || !isConnected}
|
|
1063
|
+
oninput=${handleDraftInput}
|
|
1064
|
+
></textarea>
|
|
1065
|
+
<div class="chat-composer-actions">
|
|
1066
|
+
${streaming
|
|
1067
|
+
? html`
|
|
1068
|
+
<button
|
|
1069
|
+
type="button"
|
|
1070
|
+
class="ac-btn-secondary chat-composer-stop"
|
|
1071
|
+
disabled=${!isConnected}
|
|
1072
|
+
onclick=${handleStop}
|
|
1073
|
+
>
|
|
1074
|
+
Stop
|
|
1075
|
+
</button>
|
|
1076
|
+
`
|
|
1077
|
+
: null}
|
|
1078
|
+
<button
|
|
1079
|
+
type="button"
|
|
1080
|
+
class="ac-btn-cyan chat-composer-send"
|
|
1081
|
+
disabled=${!selectedSessionKey ||
|
|
1082
|
+
sending ||
|
|
1083
|
+
streaming ||
|
|
1084
|
+
!isConnected ||
|
|
1085
|
+
!String(draft || "").trim()}
|
|
1086
|
+
onclick=${handleSend}
|
|
1087
|
+
>
|
|
1088
|
+
${sending || streaming ? "Sending..." : "Send"}
|
|
1089
|
+
</button>
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
`;
|
|
1094
|
+
};
|