@robota-sdk/agent-web-ui 3.0.0-beta.64 → 3.0.0-beta.66
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 +90 -0
- package/dist/browser/index.js +336 -336
- package/dist/node/index.cjs +304 -304
- package/dist/node/index.js +336 -336
- package/package.json +2 -2
package/dist/node/index.js
CHANGED
|
@@ -1,195 +1,126 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import ReactMarkdown from "react-markdown";
|
|
3
4
|
import remarkGfm from "remark-gfm";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
setStatus("connected");
|
|
31
|
-
send({ type: "get-messages" });
|
|
32
|
-
};
|
|
33
|
-
ws.onmessage = (event) => {
|
|
34
|
-
const data = event.data;
|
|
35
|
-
if (typeof data !== "string") return;
|
|
36
|
-
const msg = JSON.parse(data);
|
|
37
|
-
callbacks.onMessage(msg);
|
|
38
|
-
};
|
|
39
|
-
ws.onclose = () => {
|
|
40
|
-
ws = null;
|
|
41
|
-
if (!intentionalDisconnect) {
|
|
42
|
-
setStatus("disconnected");
|
|
43
|
-
scheduleReconnect();
|
|
44
|
-
} else setStatus("disconnected");
|
|
45
|
-
};
|
|
46
|
-
ws.onerror = () => {
|
|
47
|
-
setStatus("error");
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function connect() {
|
|
51
|
-
intentionalDisconnect = false;
|
|
52
|
-
if (reconnectTimer !== null) {
|
|
53
|
-
clearTimeout(reconnectTimer);
|
|
54
|
-
reconnectTimer = null;
|
|
55
|
-
}
|
|
56
|
-
doConnect();
|
|
57
|
-
}
|
|
58
|
-
function disconnect() {
|
|
59
|
-
intentionalDisconnect = true;
|
|
60
|
-
if (reconnectTimer !== null) {
|
|
61
|
-
clearTimeout(reconnectTimer);
|
|
62
|
-
reconnectTimer = null;
|
|
63
|
-
}
|
|
64
|
-
ws?.close();
|
|
65
|
-
ws = null;
|
|
66
|
-
setStatus("disconnected");
|
|
67
|
-
}
|
|
68
|
-
function send(msg) {
|
|
69
|
-
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
connect,
|
|
73
|
-
disconnect,
|
|
74
|
-
send,
|
|
75
|
-
status: () => currentStatus
|
|
76
|
-
};
|
|
5
|
+
//#region src/components/AgentActivityPanel.tsx
|
|
6
|
+
function AgentActivityPanel({ tasks, className }) {
|
|
7
|
+
const runningCount = tasks.filter((t) => t.status === "running").length;
|
|
8
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9
|
+
className: `flex flex-col overflow-hidden bg-card/10 ${className ?? ""}`,
|
|
10
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
11
|
+
className: "px-3 py-2 border-b border-border/50 flex items-center gap-2 flex-shrink-0",
|
|
12
|
+
children: [
|
|
13
|
+
runningCount > 0 ? /* @__PURE__ */ jsxs("span", {
|
|
14
|
+
className: "relative flex h-1.5 w-1.5 flex-shrink-0",
|
|
15
|
+
children: [/* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-60" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-amber-500" })]
|
|
16
|
+
}) : /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-zinc-600 flex-shrink-0" }),
|
|
17
|
+
/* @__PURE__ */ jsx("span", {
|
|
18
|
+
className: "text-[10px] font-mono font-semibold tracking-[0.14em] uppercase text-muted-foreground",
|
|
19
|
+
children: "Agents"
|
|
20
|
+
}),
|
|
21
|
+
runningCount > 0 && /* @__PURE__ */ jsxs("span", {
|
|
22
|
+
className: "ml-auto text-[10px] font-mono text-amber-400/70 tabular-nums",
|
|
23
|
+
children: [runningCount, " running"]
|
|
24
|
+
})
|
|
25
|
+
]
|
|
26
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
27
|
+
className: "flex-1 overflow-y-auto px-2 py-2 space-y-1.5",
|
|
28
|
+
children: tasks.map((entry) => /* @__PURE__ */ jsx(AgentCard, { entry }, entry.id))
|
|
29
|
+
})]
|
|
30
|
+
});
|
|
77
31
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
32
|
+
function AgentCard({ entry }) {
|
|
33
|
+
const tokens = getStatusTokens(entry.status, entry.attention);
|
|
34
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
35
|
+
className: `relative rounded-md overflow-hidden border transition-opacity duration-500 ${tokens.cardCls} ${entry.status === "completed" ? "opacity-40" : ""}`,
|
|
36
|
+
children: [/* @__PURE__ */ jsx("div", { className: `absolute left-0 top-0 bottom-0 w-0.5 ${tokens.barCls}` }), /* @__PURE__ */ jsxs("div", {
|
|
37
|
+
className: "pl-3.5 pr-3 py-2",
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ jsxs("div", {
|
|
40
|
+
className: "flex items-center gap-2",
|
|
41
|
+
children: [
|
|
42
|
+
/* @__PURE__ */ jsx(StatusDot, {
|
|
43
|
+
status: entry.status,
|
|
44
|
+
attention: entry.attention
|
|
45
|
+
}),
|
|
46
|
+
/* @__PURE__ */ jsx("span", {
|
|
47
|
+
className: "text-[11px] font-mono font-medium text-foreground/90 truncate flex-1 leading-none",
|
|
48
|
+
children: entry.title
|
|
49
|
+
}),
|
|
50
|
+
/* @__PURE__ */ jsx(AttentionTag, {
|
|
51
|
+
attention: entry.attention,
|
|
52
|
+
status: entry.status
|
|
53
|
+
})
|
|
54
|
+
]
|
|
55
|
+
}),
|
|
56
|
+
entry.currentAction && /* @__PURE__ */ jsx("p", {
|
|
57
|
+
className: "text-[10px] text-foreground/50 leading-snug mt-1.5 truncate",
|
|
58
|
+
children: entry.currentAction
|
|
59
|
+
}),
|
|
60
|
+
entry.preview && /* @__PURE__ */ jsxs("p", {
|
|
61
|
+
className: "text-[10px] font-mono text-muted-foreground/40 leading-snug mt-0.5 truncate",
|
|
62
|
+
children: [
|
|
63
|
+
"“",
|
|
64
|
+
entry.preview,
|
|
65
|
+
"”"
|
|
66
|
+
]
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
})]
|
|
70
|
+
});
|
|
87
71
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}]);
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
case "tool_end": {
|
|
141
|
-
const { state } = msg;
|
|
142
|
-
setActiveTools((prev) => prev.map((t) => t.name === state.toolName && t.status === "running" ? {
|
|
143
|
-
...t,
|
|
144
|
-
status: state.isRunning ? "running" : "done",
|
|
145
|
-
result: state.result
|
|
146
|
-
} : t));
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
case "execution_workspace_event":
|
|
150
|
-
setExecutionWorkspace(msg.snapshot);
|
|
151
|
-
break;
|
|
152
|
-
case "complete":
|
|
153
|
-
case "interrupted": {
|
|
154
|
-
const finalText = streamingTextRef.current;
|
|
155
|
-
const sid = streamingIdRef.current;
|
|
156
|
-
streamingTextRef.current = "";
|
|
157
|
-
streamingIdRef.current = null;
|
|
158
|
-
setStreamingText("");
|
|
159
|
-
setIsThinking(false);
|
|
160
|
-
setActiveTools([]);
|
|
161
|
-
if (finalText) setMessages((prev) => [...prev, {
|
|
162
|
-
id: sid ?? nextId(),
|
|
163
|
-
role: "assistant",
|
|
164
|
-
content: finalText
|
|
165
|
-
}]);
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}, []);
|
|
170
|
-
const send = useCallback((msg) => {
|
|
171
|
-
clientRef.current?.send(msg);
|
|
172
|
-
}, []);
|
|
173
|
-
useEffect(() => {
|
|
174
|
-
const client = createWsSessionClient(url, {
|
|
175
|
-
onMessage: handleMessage,
|
|
176
|
-
onStatusChange: setStatus
|
|
177
|
-
});
|
|
178
|
-
clientRef.current = client;
|
|
179
|
-
client.connect();
|
|
180
|
-
return () => {
|
|
181
|
-
client.disconnect();
|
|
182
|
-
clientRef.current = null;
|
|
183
|
-
};
|
|
184
|
-
}, [url, handleMessage]);
|
|
72
|
+
function StatusDot({ status, attention }) {
|
|
73
|
+
if (attention === "permission" || status === "waiting_permission") return /* @__PURE__ */ jsxs("span", {
|
|
74
|
+
className: "relative flex h-1.5 w-1.5 flex-shrink-0",
|
|
75
|
+
children: [/* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-70" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-rose-500" })]
|
|
76
|
+
});
|
|
77
|
+
if (attention === "failed" || status === "failed") return /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-rose-500 flex-shrink-0" });
|
|
78
|
+
if (status === "running") return /* @__PURE__ */ jsxs("span", {
|
|
79
|
+
className: "relative flex h-1.5 w-1.5 flex-shrink-0",
|
|
80
|
+
children: [/* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-55" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-amber-400" })]
|
|
81
|
+
});
|
|
82
|
+
if (status === "completed") return /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400/60 flex-shrink-0" });
|
|
83
|
+
return /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-zinc-600 flex-shrink-0" });
|
|
84
|
+
}
|
|
85
|
+
function AttentionTag({ attention, status }) {
|
|
86
|
+
if (attention === "permission" || status === "waiting_permission") return /* @__PURE__ */ jsx("span", {
|
|
87
|
+
className: "text-[9px] font-mono text-rose-400 bg-rose-500/10 border border-rose-500/25 px-1.5 py-px rounded flex-shrink-0",
|
|
88
|
+
children: "perm"
|
|
89
|
+
});
|
|
90
|
+
if (attention === "failed" || status === "failed") return /* @__PURE__ */ jsx("span", {
|
|
91
|
+
className: "text-[9px] font-mono text-rose-400 bg-rose-500/10 border border-rose-500/25 px-1.5 py-px rounded flex-shrink-0",
|
|
92
|
+
children: "err"
|
|
93
|
+
});
|
|
94
|
+
if (status === "completed") return /* @__PURE__ */ jsx("span", {
|
|
95
|
+
className: "text-[9px] font-mono text-emerald-400/50 flex-shrink-0",
|
|
96
|
+
children: "done"
|
|
97
|
+
});
|
|
98
|
+
if (status === "queued") return /* @__PURE__ */ jsx("span", {
|
|
99
|
+
className: "text-[9px] font-mono text-zinc-500 flex-shrink-0",
|
|
100
|
+
children: "queued"
|
|
101
|
+
});
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function getStatusTokens(status, attention) {
|
|
105
|
+
if (attention === "permission" || status === "waiting_permission") return {
|
|
106
|
+
cardCls: "border-rose-500/40 bg-rose-950/10",
|
|
107
|
+
barCls: "bg-rose-400"
|
|
108
|
+
};
|
|
109
|
+
if (attention === "failed" || status === "failed") return {
|
|
110
|
+
cardCls: "border-rose-500/30 bg-rose-950/15",
|
|
111
|
+
barCls: "bg-rose-500"
|
|
112
|
+
};
|
|
113
|
+
if (status === "running") return {
|
|
114
|
+
cardCls: "border-amber-500/20 bg-card/30",
|
|
115
|
+
barCls: "bg-amber-400"
|
|
116
|
+
};
|
|
117
|
+
if (status === "completed") return {
|
|
118
|
+
cardCls: "border-border/20 bg-card/20",
|
|
119
|
+
barCls: "bg-emerald-400/50"
|
|
120
|
+
};
|
|
185
121
|
return {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
activeTools,
|
|
189
|
-
streamingText,
|
|
190
|
-
isThinking,
|
|
191
|
-
executionWorkspace,
|
|
192
|
-
send
|
|
122
|
+
cardCls: "border-border/30 bg-card/25",
|
|
123
|
+
barCls: "bg-zinc-600/50"
|
|
193
124
|
};
|
|
194
125
|
}
|
|
195
126
|
//#endregion
|
|
@@ -345,162 +276,231 @@ function ThinkingIndicator() {
|
|
|
345
276
|
2
|
|
346
277
|
].map((i) => /* @__PURE__ */ jsx("span", {
|
|
347
278
|
className: "w-1.5 h-1.5 rounded-full bg-emerald-400/60 animate-bounce",
|
|
348
|
-
style: { animationDelay: `${i * 150}ms` }
|
|
349
|
-
}, i))
|
|
350
|
-
})]
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
function ConversationView({ messages, activeTools, streamingText, isThinking }) {
|
|
354
|
-
const bottomRef = useRef(null);
|
|
355
|
-
useEffect(() => {
|
|
356
|
-
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
357
|
-
}, [
|
|
358
|
-
messages.length,
|
|
359
|
-
streamingText,
|
|
360
|
-
isThinking,
|
|
361
|
-
activeTools.length
|
|
362
|
-
]);
|
|
363
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
364
|
-
className: "flex flex-col gap-3 p-4 overflow-y-auto h-full",
|
|
365
|
-
children: [
|
|
366
|
-
messages.length === 0 && !isThinking && activeTools.length === 0 && !streamingText && /* @__PURE__ */ jsx("div", {
|
|
367
|
-
className: "flex h-full items-center justify-center",
|
|
368
|
-
children: /* @__PURE__ */ jsx("p", {
|
|
369
|
-
className: "text-xs font-mono text-muted-foreground/50 tracking-widest uppercase",
|
|
370
|
-
children: "No messages yet"
|
|
371
|
-
})
|
|
372
|
-
}),
|
|
373
|
-
messages.map((msg) => msg.role === "user" ? /* @__PURE__ */ jsx(UserBlock, { content: msg.content }, msg.id) : /* @__PURE__ */ jsx(AgentBlock, { content: msg.content }, msg.id)),
|
|
374
|
-
isThinking && !streamingText && /* @__PURE__ */ jsx(ThinkingIndicator, {}),
|
|
375
|
-
activeTools.map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id)),
|
|
376
|
-
streamingText && /* @__PURE__ */ jsx(AgentBlock, {
|
|
377
|
-
content: streamingText,
|
|
378
|
-
isStreaming: true
|
|
379
|
-
}),
|
|
380
|
-
/* @__PURE__ */ jsx("div", { ref: bottomRef })
|
|
381
|
-
]
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/components/AgentActivityPanel.tsx
|
|
386
|
-
function AgentActivityPanel({ tasks, className }) {
|
|
387
|
-
const runningCount = tasks.filter((t) => t.status === "running").length;
|
|
388
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
389
|
-
className: `flex flex-col overflow-hidden bg-card/10 ${className ?? ""}`,
|
|
390
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
391
|
-
className: "px-3 py-2 border-b border-border/50 flex items-center gap-2 flex-shrink-0",
|
|
392
|
-
children: [
|
|
393
|
-
runningCount > 0 ? /* @__PURE__ */ jsxs("span", {
|
|
394
|
-
className: "relative flex h-1.5 w-1.5 flex-shrink-0",
|
|
395
|
-
children: [/* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-60" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-amber-500" })]
|
|
396
|
-
}) : /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-zinc-600 flex-shrink-0" }),
|
|
397
|
-
/* @__PURE__ */ jsx("span", {
|
|
398
|
-
className: "text-[10px] font-mono font-semibold tracking-[0.14em] uppercase text-muted-foreground",
|
|
399
|
-
children: "Agents"
|
|
400
|
-
}),
|
|
401
|
-
runningCount > 0 && /* @__PURE__ */ jsxs("span", {
|
|
402
|
-
className: "ml-auto text-[10px] font-mono text-amber-400/70 tabular-nums",
|
|
403
|
-
children: [runningCount, " running"]
|
|
404
|
-
})
|
|
405
|
-
]
|
|
406
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
407
|
-
className: "flex-1 overflow-y-auto px-2 py-2 space-y-1.5",
|
|
408
|
-
children: tasks.map((entry) => /* @__PURE__ */ jsx(AgentCard, { entry }, entry.id))
|
|
409
|
-
})]
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
function AgentCard({ entry }) {
|
|
413
|
-
const tokens = getStatusTokens(entry.status, entry.attention);
|
|
414
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
415
|
-
className: `relative rounded-md overflow-hidden border transition-opacity duration-500 ${tokens.cardCls} ${entry.status === "completed" ? "opacity-40" : ""}`,
|
|
416
|
-
children: [/* @__PURE__ */ jsx("div", { className: `absolute left-0 top-0 bottom-0 w-0.5 ${tokens.barCls}` }), /* @__PURE__ */ jsxs("div", {
|
|
417
|
-
className: "pl-3.5 pr-3 py-2",
|
|
418
|
-
children: [
|
|
419
|
-
/* @__PURE__ */ jsxs("div", {
|
|
420
|
-
className: "flex items-center gap-2",
|
|
421
|
-
children: [
|
|
422
|
-
/* @__PURE__ */ jsx(StatusDot, {
|
|
423
|
-
status: entry.status,
|
|
424
|
-
attention: entry.attention
|
|
425
|
-
}),
|
|
426
|
-
/* @__PURE__ */ jsx("span", {
|
|
427
|
-
className: "text-[11px] font-mono font-medium text-foreground/90 truncate flex-1 leading-none",
|
|
428
|
-
children: entry.title
|
|
429
|
-
}),
|
|
430
|
-
/* @__PURE__ */ jsx(AttentionTag, {
|
|
431
|
-
attention: entry.attention,
|
|
432
|
-
status: entry.status
|
|
433
|
-
})
|
|
434
|
-
]
|
|
435
|
-
}),
|
|
436
|
-
entry.currentAction && /* @__PURE__ */ jsx("p", {
|
|
437
|
-
className: "text-[10px] text-foreground/50 leading-snug mt-1.5 truncate",
|
|
438
|
-
children: entry.currentAction
|
|
439
|
-
}),
|
|
440
|
-
entry.preview && /* @__PURE__ */ jsxs("p", {
|
|
441
|
-
className: "text-[10px] font-mono text-muted-foreground/40 leading-snug mt-0.5 truncate",
|
|
442
|
-
children: [
|
|
443
|
-
"“",
|
|
444
|
-
entry.preview,
|
|
445
|
-
"”"
|
|
446
|
-
]
|
|
447
|
-
})
|
|
448
|
-
]
|
|
449
|
-
})]
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
function StatusDot({ status, attention }) {
|
|
453
|
-
if (attention === "permission" || status === "waiting_permission") return /* @__PURE__ */ jsxs("span", {
|
|
454
|
-
className: "relative flex h-1.5 w-1.5 flex-shrink-0",
|
|
455
|
-
children: [/* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-70" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-rose-500" })]
|
|
456
|
-
});
|
|
457
|
-
if (attention === "failed" || status === "failed") return /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-rose-500 flex-shrink-0" });
|
|
458
|
-
if (status === "running") return /* @__PURE__ */ jsxs("span", {
|
|
459
|
-
className: "relative flex h-1.5 w-1.5 flex-shrink-0",
|
|
460
|
-
children: [/* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-55" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-amber-400" })]
|
|
279
|
+
style: { animationDelay: `${i * 150}ms` }
|
|
280
|
+
}, i))
|
|
281
|
+
})]
|
|
461
282
|
});
|
|
462
|
-
if (status === "completed") return /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400/60 flex-shrink-0" });
|
|
463
|
-
return /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-zinc-600 flex-shrink-0" });
|
|
464
283
|
}
|
|
465
|
-
function
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
284
|
+
function ConversationView({ messages, activeTools, streamingText, isThinking }) {
|
|
285
|
+
const bottomRef = useRef(null);
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
288
|
+
}, [
|
|
289
|
+
messages.length,
|
|
290
|
+
streamingText,
|
|
291
|
+
isThinking,
|
|
292
|
+
activeTools.length
|
|
293
|
+
]);
|
|
294
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
295
|
+
className: "flex flex-col gap-3 p-4 overflow-y-auto h-full",
|
|
296
|
+
children: [
|
|
297
|
+
messages.length === 0 && !isThinking && activeTools.length === 0 && !streamingText && /* @__PURE__ */ jsx("div", {
|
|
298
|
+
className: "flex h-full items-center justify-center",
|
|
299
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
300
|
+
className: "text-xs font-mono text-muted-foreground/50 tracking-widest uppercase",
|
|
301
|
+
children: "No messages yet"
|
|
302
|
+
})
|
|
303
|
+
}),
|
|
304
|
+
messages.map((msg) => msg.role === "user" ? /* @__PURE__ */ jsx(UserBlock, { content: msg.content }, msg.id) : /* @__PURE__ */ jsx(AgentBlock, { content: msg.content }, msg.id)),
|
|
305
|
+
isThinking && !streamingText && /* @__PURE__ */ jsx(ThinkingIndicator, {}),
|
|
306
|
+
activeTools.map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id)),
|
|
307
|
+
streamingText && /* @__PURE__ */ jsx(AgentBlock, {
|
|
308
|
+
content: streamingText,
|
|
309
|
+
isStreaming: true
|
|
310
|
+
}),
|
|
311
|
+
/* @__PURE__ */ jsx("div", { ref: bottomRef })
|
|
312
|
+
]
|
|
481
313
|
});
|
|
482
|
-
return null;
|
|
483
314
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/client/ws-session-client.ts
|
|
317
|
+
const RECONNECT_DELAY_MS = 2e3;
|
|
318
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
319
|
+
function createWsSessionClient(url, callbacks) {
|
|
320
|
+
let ws = null;
|
|
321
|
+
let currentStatus = "disconnected";
|
|
322
|
+
let reconnectTimer = null;
|
|
323
|
+
let reconnectAttempts = 0;
|
|
324
|
+
let intentionalDisconnect = false;
|
|
325
|
+
function setStatus(s) {
|
|
326
|
+
currentStatus = s;
|
|
327
|
+
callbacks.onStatusChange(s);
|
|
328
|
+
}
|
|
329
|
+
function scheduleReconnect() {
|
|
330
|
+
if (intentionalDisconnect || reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
|
|
331
|
+
reconnectTimer = setTimeout(() => {
|
|
332
|
+
reconnectAttempts++;
|
|
333
|
+
doConnect();
|
|
334
|
+
}, RECONNECT_DELAY_MS);
|
|
335
|
+
}
|
|
336
|
+
function doConnect() {
|
|
337
|
+
setStatus("connecting");
|
|
338
|
+
ws = new WebSocket(url);
|
|
339
|
+
ws.onopen = () => {
|
|
340
|
+
reconnectAttempts = 0;
|
|
341
|
+
setStatus("connected");
|
|
342
|
+
send({ type: "get-messages" });
|
|
343
|
+
};
|
|
344
|
+
ws.onmessage = (event) => {
|
|
345
|
+
const data = event.data;
|
|
346
|
+
if (typeof data !== "string") return;
|
|
347
|
+
const msg = JSON.parse(data);
|
|
348
|
+
callbacks.onMessage(msg);
|
|
349
|
+
};
|
|
350
|
+
ws.onclose = () => {
|
|
351
|
+
ws = null;
|
|
352
|
+
if (!intentionalDisconnect) {
|
|
353
|
+
setStatus("disconnected");
|
|
354
|
+
scheduleReconnect();
|
|
355
|
+
} else setStatus("disconnected");
|
|
356
|
+
};
|
|
357
|
+
ws.onerror = () => {
|
|
358
|
+
setStatus("error");
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function connect() {
|
|
362
|
+
intentionalDisconnect = false;
|
|
363
|
+
if (reconnectTimer !== null) {
|
|
364
|
+
clearTimeout(reconnectTimer);
|
|
365
|
+
reconnectTimer = null;
|
|
366
|
+
}
|
|
367
|
+
doConnect();
|
|
368
|
+
}
|
|
369
|
+
function disconnect() {
|
|
370
|
+
intentionalDisconnect = true;
|
|
371
|
+
if (reconnectTimer !== null) {
|
|
372
|
+
clearTimeout(reconnectTimer);
|
|
373
|
+
reconnectTimer = null;
|
|
374
|
+
}
|
|
375
|
+
ws?.close();
|
|
376
|
+
ws = null;
|
|
377
|
+
setStatus("disconnected");
|
|
378
|
+
}
|
|
379
|
+
function send(msg) {
|
|
380
|
+
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
connect,
|
|
384
|
+
disconnect,
|
|
385
|
+
send,
|
|
386
|
+
status: () => currentStatus
|
|
500
387
|
};
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/hooks/useWsSession.ts
|
|
391
|
+
/**
|
|
392
|
+
* React hook that connects to an agent-cli sidecar WebSocket and
|
|
393
|
+
* reconstructs conversation state from TServerMessage events.
|
|
394
|
+
*/
|
|
395
|
+
let msgCounter = 0;
|
|
396
|
+
function nextId() {
|
|
397
|
+
return `msg_${++msgCounter}_${Date.now()}`;
|
|
398
|
+
}
|
|
399
|
+
function useWsSession(url) {
|
|
400
|
+
const [status, setStatus] = useState("disconnected");
|
|
401
|
+
const [messages, setMessages] = useState([]);
|
|
402
|
+
const [activeTools, setActiveTools] = useState([]);
|
|
403
|
+
const [streamingText, setStreamingText] = useState("");
|
|
404
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
405
|
+
const [executionWorkspace, setExecutionWorkspace] = useState(null);
|
|
406
|
+
const clientRef = useRef(null);
|
|
407
|
+
const streamingIdRef = useRef(null);
|
|
408
|
+
const streamingTextRef = useRef("");
|
|
409
|
+
const handleMessage = useCallback((msg) => {
|
|
410
|
+
switch (msg.type) {
|
|
411
|
+
case "messages":
|
|
412
|
+
setMessages(msg.messages.flatMap((m) => {
|
|
413
|
+
if (m.role !== "user" && m.role !== "assistant") return [];
|
|
414
|
+
const content = m.content ?? "";
|
|
415
|
+
return [{
|
|
416
|
+
id: nextId(),
|
|
417
|
+
role: m.role,
|
|
418
|
+
content
|
|
419
|
+
}];
|
|
420
|
+
}));
|
|
421
|
+
break;
|
|
422
|
+
case "user_message":
|
|
423
|
+
setMessages((prev) => [...prev, {
|
|
424
|
+
id: nextId(),
|
|
425
|
+
role: "user",
|
|
426
|
+
content: msg.content
|
|
427
|
+
}]);
|
|
428
|
+
break;
|
|
429
|
+
case "text_delta":
|
|
430
|
+
setStreamingText((prev) => {
|
|
431
|
+
const next = prev + msg.delta;
|
|
432
|
+
streamingTextRef.current = next;
|
|
433
|
+
if (streamingIdRef.current === null) streamingIdRef.current = nextId();
|
|
434
|
+
return next;
|
|
435
|
+
});
|
|
436
|
+
break;
|
|
437
|
+
case "thinking":
|
|
438
|
+
setIsThinking(msg.isThinking);
|
|
439
|
+
break;
|
|
440
|
+
case "tool_start": {
|
|
441
|
+
const { state } = msg;
|
|
442
|
+
const toolId = nextId();
|
|
443
|
+
setActiveTools((prev) => [...prev, {
|
|
444
|
+
id: toolId,
|
|
445
|
+
name: state.toolName,
|
|
446
|
+
status: "running",
|
|
447
|
+
input: state.firstArg
|
|
448
|
+
}]);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "tool_end": {
|
|
452
|
+
const { state } = msg;
|
|
453
|
+
setActiveTools((prev) => prev.map((t) => t.name === state.toolName && t.status === "running" ? {
|
|
454
|
+
...t,
|
|
455
|
+
status: state.isRunning ? "running" : "done",
|
|
456
|
+
result: state.result
|
|
457
|
+
} : t));
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "execution_workspace_event":
|
|
461
|
+
setExecutionWorkspace(msg.snapshot);
|
|
462
|
+
break;
|
|
463
|
+
case "complete":
|
|
464
|
+
case "interrupted": {
|
|
465
|
+
const finalText = streamingTextRef.current;
|
|
466
|
+
const sid = streamingIdRef.current;
|
|
467
|
+
streamingTextRef.current = "";
|
|
468
|
+
streamingIdRef.current = null;
|
|
469
|
+
setStreamingText("");
|
|
470
|
+
setIsThinking(false);
|
|
471
|
+
setActiveTools([]);
|
|
472
|
+
if (finalText) setMessages((prev) => [...prev, {
|
|
473
|
+
id: sid ?? nextId(),
|
|
474
|
+
role: "assistant",
|
|
475
|
+
content: finalText
|
|
476
|
+
}]);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}, []);
|
|
481
|
+
const send = useCallback((msg) => {
|
|
482
|
+
clientRef.current?.send(msg);
|
|
483
|
+
}, []);
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
const client = createWsSessionClient(url, {
|
|
486
|
+
onMessage: handleMessage,
|
|
487
|
+
onStatusChange: setStatus
|
|
488
|
+
});
|
|
489
|
+
clientRef.current = client;
|
|
490
|
+
client.connect();
|
|
491
|
+
return () => {
|
|
492
|
+
client.disconnect();
|
|
493
|
+
clientRef.current = null;
|
|
494
|
+
};
|
|
495
|
+
}, [url, handleMessage]);
|
|
501
496
|
return {
|
|
502
|
-
|
|
503
|
-
|
|
497
|
+
status,
|
|
498
|
+
messages,
|
|
499
|
+
activeTools,
|
|
500
|
+
streamingText,
|
|
501
|
+
isThinking,
|
|
502
|
+
executionWorkspace,
|
|
503
|
+
send
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
506
|
//#endregion
|