@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.
@@ -23,199 +23,130 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  let react = require("react");
25
25
  react = __toESM(react, 1);
26
+ let react_jsx_runtime = require("react/jsx-runtime");
26
27
  let react_markdown = require("react-markdown");
27
28
  react_markdown = __toESM(react_markdown);
28
29
  let remark_gfm = require("remark-gfm");
29
30
  remark_gfm = __toESM(remark_gfm);
30
- let react_jsx_runtime = require("react/jsx-runtime");
31
- //#region src/client/ws-session-client.ts
32
- const RECONNECT_DELAY_MS = 2e3;
33
- const MAX_RECONNECT_ATTEMPTS = 10;
34
- function createWsSessionClient(url, callbacks) {
35
- let ws = null;
36
- let currentStatus = "disconnected";
37
- let reconnectTimer = null;
38
- let reconnectAttempts = 0;
39
- let intentionalDisconnect = false;
40
- function setStatus(s) {
41
- currentStatus = s;
42
- callbacks.onStatusChange(s);
43
- }
44
- function scheduleReconnect() {
45
- if (intentionalDisconnect || reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
46
- reconnectTimer = setTimeout(() => {
47
- reconnectAttempts++;
48
- doConnect();
49
- }, RECONNECT_DELAY_MS);
50
- }
51
- function doConnect() {
52
- setStatus("connecting");
53
- ws = new WebSocket(url);
54
- ws.onopen = () => {
55
- reconnectAttempts = 0;
56
- setStatus("connected");
57
- send({ type: "get-messages" });
58
- };
59
- ws.onmessage = (event) => {
60
- const data = event.data;
61
- if (typeof data !== "string") return;
62
- const msg = JSON.parse(data);
63
- callbacks.onMessage(msg);
64
- };
65
- ws.onclose = () => {
66
- ws = null;
67
- if (!intentionalDisconnect) {
68
- setStatus("disconnected");
69
- scheduleReconnect();
70
- } else setStatus("disconnected");
71
- };
72
- ws.onerror = () => {
73
- setStatus("error");
74
- };
75
- }
76
- function connect() {
77
- intentionalDisconnect = false;
78
- if (reconnectTimer !== null) {
79
- clearTimeout(reconnectTimer);
80
- reconnectTimer = null;
81
- }
82
- doConnect();
83
- }
84
- function disconnect() {
85
- intentionalDisconnect = true;
86
- if (reconnectTimer !== null) {
87
- clearTimeout(reconnectTimer);
88
- reconnectTimer = null;
89
- }
90
- ws?.close();
91
- ws = null;
92
- setStatus("disconnected");
93
- }
94
- function send(msg) {
95
- if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
96
- }
31
+ //#region src/components/AgentActivityPanel.tsx
32
+ function AgentActivityPanel({ tasks, className }) {
33
+ const runningCount = tasks.filter((t) => t.status === "running").length;
34
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35
+ className: `flex flex-col overflow-hidden bg-card/10 ${className ?? ""}`,
36
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
37
+ className: "px-3 py-2 border-b border-border/50 flex items-center gap-2 flex-shrink-0",
38
+ children: [
39
+ runningCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
40
+ className: "relative flex h-1.5 w-1.5 flex-shrink-0",
41
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-60" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-amber-500" })]
42
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-zinc-600 flex-shrink-0" }),
43
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
44
+ className: "text-[10px] font-mono font-semibold tracking-[0.14em] uppercase text-muted-foreground",
45
+ children: "Agents"
46
+ }),
47
+ runningCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48
+ className: "ml-auto text-[10px] font-mono text-amber-400/70 tabular-nums",
49
+ children: [runningCount, " running"]
50
+ })
51
+ ]
52
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
53
+ className: "flex-1 overflow-y-auto px-2 py-2 space-y-1.5",
54
+ children: tasks.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentCard, { entry }, entry.id))
55
+ })]
56
+ });
57
+ }
58
+ function AgentCard({ entry }) {
59
+ const tokens = getStatusTokens(entry.status, entry.attention);
60
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
61
+ className: `relative rounded-md overflow-hidden border transition-opacity duration-500 ${tokens.cardCls} ${entry.status === "completed" ? "opacity-40" : ""}`,
62
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `absolute left-0 top-0 bottom-0 w-0.5 ${tokens.barCls}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
63
+ className: "pl-3.5 pr-3 py-2",
64
+ children: [
65
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
66
+ className: "flex items-center gap-2",
67
+ children: [
68
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusDot, {
69
+ status: entry.status,
70
+ attention: entry.attention
71
+ }),
72
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
73
+ className: "text-[11px] font-mono font-medium text-foreground/90 truncate flex-1 leading-none",
74
+ children: entry.title
75
+ }),
76
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttentionTag, {
77
+ attention: entry.attention,
78
+ status: entry.status
79
+ })
80
+ ]
81
+ }),
82
+ entry.currentAction && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
83
+ className: "text-[10px] text-foreground/50 leading-snug mt-1.5 truncate",
84
+ children: entry.currentAction
85
+ }),
86
+ entry.preview && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
87
+ className: "text-[10px] font-mono text-muted-foreground/40 leading-snug mt-0.5 truncate",
88
+ children: [
89
+ "“",
90
+ entry.preview,
91
+ "”"
92
+ ]
93
+ })
94
+ ]
95
+ })]
96
+ });
97
+ }
98
+ function StatusDot({ status, attention }) {
99
+ if (attention === "permission" || status === "waiting_permission") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
100
+ className: "relative flex h-1.5 w-1.5 flex-shrink-0",
101
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-70" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-rose-500" })]
102
+ });
103
+ if (attention === "failed" || status === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-rose-500 flex-shrink-0" });
104
+ if (status === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
105
+ className: "relative flex h-1.5 w-1.5 flex-shrink-0",
106
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-55" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-amber-400" })]
107
+ });
108
+ if (status === "completed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400/60 flex-shrink-0" });
109
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-zinc-600 flex-shrink-0" });
110
+ }
111
+ function AttentionTag({ attention, status }) {
112
+ if (attention === "permission" || status === "waiting_permission") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
113
+ 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",
114
+ children: "perm"
115
+ });
116
+ if (attention === "failed" || status === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
117
+ 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",
118
+ children: "err"
119
+ });
120
+ if (status === "completed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
121
+ className: "text-[9px] font-mono text-emerald-400/50 flex-shrink-0",
122
+ children: "done"
123
+ });
124
+ if (status === "queued") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
125
+ className: "text-[9px] font-mono text-zinc-500 flex-shrink-0",
126
+ children: "queued"
127
+ });
128
+ return null;
129
+ }
130
+ function getStatusTokens(status, attention) {
131
+ if (attention === "permission" || status === "waiting_permission") return {
132
+ cardCls: "border-rose-500/40 bg-rose-950/10",
133
+ barCls: "bg-rose-400"
134
+ };
135
+ if (attention === "failed" || status === "failed") return {
136
+ cardCls: "border-rose-500/30 bg-rose-950/15",
137
+ barCls: "bg-rose-500"
138
+ };
139
+ if (status === "running") return {
140
+ cardCls: "border-amber-500/20 bg-card/30",
141
+ barCls: "bg-amber-400"
142
+ };
143
+ if (status === "completed") return {
144
+ cardCls: "border-border/20 bg-card/20",
145
+ barCls: "bg-emerald-400/50"
146
+ };
97
147
  return {
98
- connect,
99
- disconnect,
100
- send,
101
- status: () => currentStatus
102
- };
103
- }
104
- //#endregion
105
- //#region src/hooks/useWsSession.ts
106
- /**
107
- * React hook that connects to an agent-cli sidecar WebSocket and
108
- * reconstructs conversation state from TServerMessage events.
109
- */
110
- let msgCounter = 0;
111
- function nextId() {
112
- return `msg_${++msgCounter}_${Date.now()}`;
113
- }
114
- function useWsSession(url) {
115
- const [status, setStatus] = (0, react.useState)("disconnected");
116
- const [messages, setMessages] = (0, react.useState)([]);
117
- const [activeTools, setActiveTools] = (0, react.useState)([]);
118
- const [streamingText, setStreamingText] = (0, react.useState)("");
119
- const [isThinking, setIsThinking] = (0, react.useState)(false);
120
- const [executionWorkspace, setExecutionWorkspace] = (0, react.useState)(null);
121
- const clientRef = (0, react.useRef)(null);
122
- const streamingIdRef = (0, react.useRef)(null);
123
- const streamingTextRef = (0, react.useRef)("");
124
- const handleMessage = (0, react.useCallback)((msg) => {
125
- switch (msg.type) {
126
- case "messages":
127
- setMessages(msg.messages.flatMap((m) => {
128
- if (m.role !== "user" && m.role !== "assistant") return [];
129
- const content = m.content ?? "";
130
- return [{
131
- id: nextId(),
132
- role: m.role,
133
- content
134
- }];
135
- }));
136
- break;
137
- case "user_message":
138
- setMessages((prev) => [...prev, {
139
- id: nextId(),
140
- role: "user",
141
- content: msg.content
142
- }]);
143
- break;
144
- case "text_delta":
145
- setStreamingText((prev) => {
146
- const next = prev + msg.delta;
147
- streamingTextRef.current = next;
148
- if (streamingIdRef.current === null) streamingIdRef.current = nextId();
149
- return next;
150
- });
151
- break;
152
- case "thinking":
153
- setIsThinking(msg.isThinking);
154
- break;
155
- case "tool_start": {
156
- const { state } = msg;
157
- const toolId = nextId();
158
- setActiveTools((prev) => [...prev, {
159
- id: toolId,
160
- name: state.toolName,
161
- status: "running",
162
- input: state.firstArg
163
- }]);
164
- break;
165
- }
166
- case "tool_end": {
167
- const { state } = msg;
168
- setActiveTools((prev) => prev.map((t) => t.name === state.toolName && t.status === "running" ? {
169
- ...t,
170
- status: state.isRunning ? "running" : "done",
171
- result: state.result
172
- } : t));
173
- break;
174
- }
175
- case "execution_workspace_event":
176
- setExecutionWorkspace(msg.snapshot);
177
- break;
178
- case "complete":
179
- case "interrupted": {
180
- const finalText = streamingTextRef.current;
181
- const sid = streamingIdRef.current;
182
- streamingTextRef.current = "";
183
- streamingIdRef.current = null;
184
- setStreamingText("");
185
- setIsThinking(false);
186
- setActiveTools([]);
187
- if (finalText) setMessages((prev) => [...prev, {
188
- id: sid ?? nextId(),
189
- role: "assistant",
190
- content: finalText
191
- }]);
192
- break;
193
- }
194
- }
195
- }, []);
196
- const send = (0, react.useCallback)((msg) => {
197
- clientRef.current?.send(msg);
198
- }, []);
199
- (0, react.useEffect)(() => {
200
- const client = createWsSessionClient(url, {
201
- onMessage: handleMessage,
202
- onStatusChange: setStatus
203
- });
204
- clientRef.current = client;
205
- client.connect();
206
- return () => {
207
- client.disconnect();
208
- clientRef.current = null;
209
- };
210
- }, [url, handleMessage]);
211
- return {
212
- status,
213
- messages,
214
- activeTools,
215
- streamingText,
216
- isThinking,
217
- executionWorkspace,
218
- send
148
+ cardCls: "border-border/30 bg-card/25",
149
+ barCls: "bg-zinc-600/50"
219
150
  };
220
151
  }
221
152
  //#endregion
@@ -408,125 +339,194 @@ function ConversationView({ messages, activeTools, streamingText, isThinking })
408
339
  });
409
340
  }
410
341
  //#endregion
411
- //#region src/components/AgentActivityPanel.tsx
412
- function AgentActivityPanel({ tasks, className }) {
413
- const runningCount = tasks.filter((t) => t.status === "running").length;
414
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
415
- className: `flex flex-col overflow-hidden bg-card/10 ${className ?? ""}`,
416
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
417
- className: "px-3 py-2 border-b border-border/50 flex items-center gap-2 flex-shrink-0",
418
- children: [
419
- runningCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
420
- className: "relative flex h-1.5 w-1.5 flex-shrink-0",
421
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-60" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-amber-500" })]
422
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-zinc-600 flex-shrink-0" }),
423
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
424
- className: "text-[10px] font-mono font-semibold tracking-[0.14em] uppercase text-muted-foreground",
425
- children: "Agents"
426
- }),
427
- runningCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
428
- className: "ml-auto text-[10px] font-mono text-amber-400/70 tabular-nums",
429
- children: [runningCount, " running"]
430
- })
431
- ]
432
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
433
- className: "flex-1 overflow-y-auto px-2 py-2 space-y-1.5",
434
- children: tasks.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentCard, { entry }, entry.id))
435
- })]
436
- });
437
- }
438
- function AgentCard({ entry }) {
439
- const tokens = getStatusTokens(entry.status, entry.attention);
440
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
441
- className: `relative rounded-md overflow-hidden border transition-opacity duration-500 ${tokens.cardCls} ${entry.status === "completed" ? "opacity-40" : ""}`,
442
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `absolute left-0 top-0 bottom-0 w-0.5 ${tokens.barCls}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
443
- className: "pl-3.5 pr-3 py-2",
444
- children: [
445
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
446
- className: "flex items-center gap-2",
447
- children: [
448
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusDot, {
449
- status: entry.status,
450
- attention: entry.attention
451
- }),
452
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
453
- className: "text-[11px] font-mono font-medium text-foreground/90 truncate flex-1 leading-none",
454
- children: entry.title
455
- }),
456
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttentionTag, {
457
- attention: entry.attention,
458
- status: entry.status
459
- })
460
- ]
461
- }),
462
- entry.currentAction && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
463
- className: "text-[10px] text-foreground/50 leading-snug mt-1.5 truncate",
464
- children: entry.currentAction
465
- }),
466
- entry.preview && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
467
- className: "text-[10px] font-mono text-muted-foreground/40 leading-snug mt-0.5 truncate",
468
- children: [
469
- "“",
470
- entry.preview,
471
- "”"
472
- ]
473
- })
474
- ]
475
- })]
476
- });
477
- }
478
- function StatusDot({ status, attention }) {
479
- if (attention === "permission" || status === "waiting_permission") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
480
- className: "relative flex h-1.5 w-1.5 flex-shrink-0",
481
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-70" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-rose-500" })]
482
- });
483
- if (attention === "failed" || status === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-rose-500 flex-shrink-0" });
484
- if (status === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
485
- className: "relative flex h-1.5 w-1.5 flex-shrink-0",
486
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-55" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-amber-400" })]
487
- });
488
- if (status === "completed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400/60 flex-shrink-0" });
489
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-zinc-600 flex-shrink-0" });
342
+ //#region src/client/ws-session-client.ts
343
+ const RECONNECT_DELAY_MS = 2e3;
344
+ const MAX_RECONNECT_ATTEMPTS = 10;
345
+ function createWsSessionClient(url, callbacks) {
346
+ let ws = null;
347
+ let currentStatus = "disconnected";
348
+ let reconnectTimer = null;
349
+ let reconnectAttempts = 0;
350
+ let intentionalDisconnect = false;
351
+ function setStatus(s) {
352
+ currentStatus = s;
353
+ callbacks.onStatusChange(s);
354
+ }
355
+ function scheduleReconnect() {
356
+ if (intentionalDisconnect || reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
357
+ reconnectTimer = setTimeout(() => {
358
+ reconnectAttempts++;
359
+ doConnect();
360
+ }, RECONNECT_DELAY_MS);
361
+ }
362
+ function doConnect() {
363
+ setStatus("connecting");
364
+ ws = new WebSocket(url);
365
+ ws.onopen = () => {
366
+ reconnectAttempts = 0;
367
+ setStatus("connected");
368
+ send({ type: "get-messages" });
369
+ };
370
+ ws.onmessage = (event) => {
371
+ const data = event.data;
372
+ if (typeof data !== "string") return;
373
+ const msg = JSON.parse(data);
374
+ callbacks.onMessage(msg);
375
+ };
376
+ ws.onclose = () => {
377
+ ws = null;
378
+ if (!intentionalDisconnect) {
379
+ setStatus("disconnected");
380
+ scheduleReconnect();
381
+ } else setStatus("disconnected");
382
+ };
383
+ ws.onerror = () => {
384
+ setStatus("error");
385
+ };
386
+ }
387
+ function connect() {
388
+ intentionalDisconnect = false;
389
+ if (reconnectTimer !== null) {
390
+ clearTimeout(reconnectTimer);
391
+ reconnectTimer = null;
392
+ }
393
+ doConnect();
394
+ }
395
+ function disconnect() {
396
+ intentionalDisconnect = true;
397
+ if (reconnectTimer !== null) {
398
+ clearTimeout(reconnectTimer);
399
+ reconnectTimer = null;
400
+ }
401
+ ws?.close();
402
+ ws = null;
403
+ setStatus("disconnected");
404
+ }
405
+ function send(msg) {
406
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
407
+ }
408
+ return {
409
+ connect,
410
+ disconnect,
411
+ send,
412
+ status: () => currentStatus
413
+ };
490
414
  }
491
- function AttentionTag({ attention, status }) {
492
- if (attention === "permission" || status === "waiting_permission") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
493
- 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",
494
- children: "perm"
495
- });
496
- if (attention === "failed" || status === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
497
- 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",
498
- children: "err"
499
- });
500
- if (status === "completed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
501
- className: "text-[9px] font-mono text-emerald-400/50 flex-shrink-0",
502
- children: "done"
503
- });
504
- if (status === "queued") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
505
- className: "text-[9px] font-mono text-zinc-500 flex-shrink-0",
506
- children: "queued"
507
- });
508
- return null;
415
+ //#endregion
416
+ //#region src/hooks/useWsSession.ts
417
+ /**
418
+ * React hook that connects to an agent-cli sidecar WebSocket and
419
+ * reconstructs conversation state from TServerMessage events.
420
+ */
421
+ let msgCounter = 0;
422
+ function nextId() {
423
+ return `msg_${++msgCounter}_${Date.now()}`;
509
424
  }
510
- function getStatusTokens(status, attention) {
511
- if (attention === "permission" || status === "waiting_permission") return {
512
- cardCls: "border-rose-500/40 bg-rose-950/10",
513
- barCls: "bg-rose-400"
514
- };
515
- if (attention === "failed" || status === "failed") return {
516
- cardCls: "border-rose-500/30 bg-rose-950/15",
517
- barCls: "bg-rose-500"
518
- };
519
- if (status === "running") return {
520
- cardCls: "border-amber-500/20 bg-card/30",
521
- barCls: "bg-amber-400"
522
- };
523
- if (status === "completed") return {
524
- cardCls: "border-border/20 bg-card/20",
525
- barCls: "bg-emerald-400/50"
526
- };
425
+ function useWsSession(url) {
426
+ const [status, setStatus] = (0, react.useState)("disconnected");
427
+ const [messages, setMessages] = (0, react.useState)([]);
428
+ const [activeTools, setActiveTools] = (0, react.useState)([]);
429
+ const [streamingText, setStreamingText] = (0, react.useState)("");
430
+ const [isThinking, setIsThinking] = (0, react.useState)(false);
431
+ const [executionWorkspace, setExecutionWorkspace] = (0, react.useState)(null);
432
+ const clientRef = (0, react.useRef)(null);
433
+ const streamingIdRef = (0, react.useRef)(null);
434
+ const streamingTextRef = (0, react.useRef)("");
435
+ const handleMessage = (0, react.useCallback)((msg) => {
436
+ switch (msg.type) {
437
+ case "messages":
438
+ setMessages(msg.messages.flatMap((m) => {
439
+ if (m.role !== "user" && m.role !== "assistant") return [];
440
+ const content = m.content ?? "";
441
+ return [{
442
+ id: nextId(),
443
+ role: m.role,
444
+ content
445
+ }];
446
+ }));
447
+ break;
448
+ case "user_message":
449
+ setMessages((prev) => [...prev, {
450
+ id: nextId(),
451
+ role: "user",
452
+ content: msg.content
453
+ }]);
454
+ break;
455
+ case "text_delta":
456
+ setStreamingText((prev) => {
457
+ const next = prev + msg.delta;
458
+ streamingTextRef.current = next;
459
+ if (streamingIdRef.current === null) streamingIdRef.current = nextId();
460
+ return next;
461
+ });
462
+ break;
463
+ case "thinking":
464
+ setIsThinking(msg.isThinking);
465
+ break;
466
+ case "tool_start": {
467
+ const { state } = msg;
468
+ const toolId = nextId();
469
+ setActiveTools((prev) => [...prev, {
470
+ id: toolId,
471
+ name: state.toolName,
472
+ status: "running",
473
+ input: state.firstArg
474
+ }]);
475
+ break;
476
+ }
477
+ case "tool_end": {
478
+ const { state } = msg;
479
+ setActiveTools((prev) => prev.map((t) => t.name === state.toolName && t.status === "running" ? {
480
+ ...t,
481
+ status: state.isRunning ? "running" : "done",
482
+ result: state.result
483
+ } : t));
484
+ break;
485
+ }
486
+ case "execution_workspace_event":
487
+ setExecutionWorkspace(msg.snapshot);
488
+ break;
489
+ case "complete":
490
+ case "interrupted": {
491
+ const finalText = streamingTextRef.current;
492
+ const sid = streamingIdRef.current;
493
+ streamingTextRef.current = "";
494
+ streamingIdRef.current = null;
495
+ setStreamingText("");
496
+ setIsThinking(false);
497
+ setActiveTools([]);
498
+ if (finalText) setMessages((prev) => [...prev, {
499
+ id: sid ?? nextId(),
500
+ role: "assistant",
501
+ content: finalText
502
+ }]);
503
+ break;
504
+ }
505
+ }
506
+ }, []);
507
+ const send = (0, react.useCallback)((msg) => {
508
+ clientRef.current?.send(msg);
509
+ }, []);
510
+ (0, react.useEffect)(() => {
511
+ const client = createWsSessionClient(url, {
512
+ onMessage: handleMessage,
513
+ onStatusChange: setStatus
514
+ });
515
+ clientRef.current = client;
516
+ client.connect();
517
+ return () => {
518
+ client.disconnect();
519
+ clientRef.current = null;
520
+ };
521
+ }, [url, handleMessage]);
527
522
  return {
528
- cardCls: "border-border/30 bg-card/25",
529
- barCls: "bg-zinc-600/50"
523
+ status,
524
+ messages,
525
+ activeTools,
526
+ streamingText,
527
+ isThinking,
528
+ executionWorkspace,
529
+ send
530
530
  };
531
531
  }
532
532
  //#endregion