@virtengine/openfleet 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
* Component: Chat View — ChatGPT-style message interface
|
|
3
|
+
* ────────────────────────────────────────────────────────────── */
|
|
4
|
+
import { h } from "preact";
|
|
5
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from "preact/hooks";
|
|
6
|
+
import htm from "htm";
|
|
7
|
+
import { apiFetch } from "../modules/api.js";
|
|
8
|
+
import { showToast } from "../modules/state.js";
|
|
9
|
+
import { formatRelative, truncate } from "../modules/utils.js";
|
|
10
|
+
import {
|
|
11
|
+
sessionMessages,
|
|
12
|
+
loadSessionMessages,
|
|
13
|
+
loadSessions,
|
|
14
|
+
selectedSessionId,
|
|
15
|
+
sessionsData,
|
|
16
|
+
} from "./session-list.js";
|
|
17
|
+
|
|
18
|
+
const html = htm.bind(h);
|
|
19
|
+
|
|
20
|
+
/* ─── Inline markdown formatting ─── */
|
|
21
|
+
function applyInline(text) {
|
|
22
|
+
text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
23
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
24
|
+
text = text.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
|
25
|
+
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
26
|
+
text = text.replace(/(?<![a-zA-Z0-9])_(.+?)_(?![a-zA-Z0-9])/g, '<em>$1</em>');
|
|
27
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
|
|
28
|
+
if (/^(https?:|mailto:|\/|#)/.test(url)) {
|
|
29
|
+
return `<a href="${url}" target="_blank" rel="noopener" class="md-link">${label}</a>`;
|
|
30
|
+
}
|
|
31
|
+
return `${label} (${url})`;
|
|
32
|
+
});
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ─── Convert markdown text to HTML ─── */
|
|
37
|
+
function renderMarkdown(text) {
|
|
38
|
+
const codes = [];
|
|
39
|
+
let s = text.replace(/`([^`\n]+)`/g, (_, c) => {
|
|
40
|
+
codes.push(c);
|
|
41
|
+
return `%%ICODE${codes.length - 1}%%`;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
s = s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
45
|
+
|
|
46
|
+
const lines = s.split('\n');
|
|
47
|
+
const out = [];
|
|
48
|
+
let i = 0;
|
|
49
|
+
|
|
50
|
+
while (i < lines.length) {
|
|
51
|
+
const line = lines[i];
|
|
52
|
+
let m;
|
|
53
|
+
|
|
54
|
+
if ((m = line.match(/^(#{1,3}) (.+)$/))) {
|
|
55
|
+
const lvl = m[1].length;
|
|
56
|
+
out.push(`<div class="md-heading md-h${lvl}">${applyInline(m[2])}</div>`);
|
|
57
|
+
i++; continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (/^-{3,}\s*$/.test(line.trim())) {
|
|
61
|
+
out.push('<hr class="md-hr"/>');
|
|
62
|
+
i++; continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (/^>\s?/.test(line)) {
|
|
66
|
+
const q = [];
|
|
67
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) {
|
|
68
|
+
q.push(applyInline(lines[i].replace(/^>\s?/, '')));
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
out.push(`<div class="md-blockquote">${q.join('<br/>')}</div>`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (/^[-*] /.test(line)) {
|
|
76
|
+
const items = [];
|
|
77
|
+
while (i < lines.length && /^[-*] /.test(lines[i])) {
|
|
78
|
+
items.push(`<li>${applyInline(lines[i].replace(/^[-*] /, ''))}</li>`);
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
out.push(`<ul class="md-list">${items.join('')}</ul>`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (/^\d+\.\s/.test(line)) {
|
|
86
|
+
const items = [];
|
|
87
|
+
while (i < lines.length && /^\d+\.\s/.test(lines[i])) {
|
|
88
|
+
items.push(`<li>${applyInline(lines[i].replace(/^\d+\.\s/, ''))}</li>`);
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
out.push(`<ol class="md-list md-ol">${items.join('')}</ol>`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
out.push(applyInline(line));
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let result = out.join('\n').replace(/\n/g, '<br/>');
|
|
100
|
+
|
|
101
|
+
result = result.replace(/%%ICODE(\d+)%%/g, (_, idx) => {
|
|
102
|
+
const c = codes[parseInt(idx)]
|
|
103
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
104
|
+
return `<span class="md-inline-code">${c}</span>`;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ─── Code block copy button ─── */
|
|
111
|
+
function CodeBlock({ code }) {
|
|
112
|
+
const [copied, setCopied] = useState(false);
|
|
113
|
+
const handleCopy = useCallback(() => {
|
|
114
|
+
try {
|
|
115
|
+
navigator.clipboard.writeText(code);
|
|
116
|
+
setCopied(true);
|
|
117
|
+
setTimeout(() => setCopied(false), 2000);
|
|
118
|
+
} catch { /* noop */ }
|
|
119
|
+
}, [code]);
|
|
120
|
+
|
|
121
|
+
return html`
|
|
122
|
+
<div class="chat-code-block">
|
|
123
|
+
<button class="chat-code-copy" onClick=${handleCopy}>
|
|
124
|
+
${copied ? "✓" : "📋"}
|
|
125
|
+
</button>
|
|
126
|
+
<pre><code>${code}</code></pre>
|
|
127
|
+
</div>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ─── Render message content with code block + markdown support ─── */
|
|
132
|
+
function MessageContent({ text }) {
|
|
133
|
+
if (!text) return null;
|
|
134
|
+
const parts = text.split(/(```[\s\S]*?```)/g);
|
|
135
|
+
return html`${parts.map((part, i) => {
|
|
136
|
+
if (part.startsWith("```") && part.endsWith("```")) {
|
|
137
|
+
const code = part.slice(3, -3).replace(/^\w+\n/, "");
|
|
138
|
+
return html`<${CodeBlock} key=${i} code=${code} />`;
|
|
139
|
+
}
|
|
140
|
+
return html`<div key=${i} class="md-rendered" dangerouslySetInnerHTML=${{ __html: renderMarkdown(part) }} />`;
|
|
141
|
+
})}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ─── Stream helpers ─── */
|
|
145
|
+
function categorizeMessage(msg) {
|
|
146
|
+
const type = (msg?.type || "").toLowerCase();
|
|
147
|
+
if (type === "tool_call") return "tool";
|
|
148
|
+
if (type === "tool_result" || type === "tool_output") return "result";
|
|
149
|
+
if (type === "error" || type === "stream_error") return "error";
|
|
150
|
+
return "message";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatMessageLine(msg) {
|
|
154
|
+
const timestamp = msg?.timestamp || "";
|
|
155
|
+
const kind = msg?.role || msg?.type || "message";
|
|
156
|
+
const content =
|
|
157
|
+
typeof msg?.content === "string"
|
|
158
|
+
? msg.content
|
|
159
|
+
: msg?.content
|
|
160
|
+
? JSON.stringify(msg.content)
|
|
161
|
+
: "";
|
|
162
|
+
return `[${timestamp}] ${String(kind).toUpperCase()}: ${content}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ─── Chat View component ─── */
|
|
166
|
+
export function ChatView({ sessionId, readOnly = false }) {
|
|
167
|
+
const [input, setInput] = useState("");
|
|
168
|
+
const [sending, setSending] = useState(false);
|
|
169
|
+
const [loading, setLoading] = useState(false);
|
|
170
|
+
const [paused, setPaused] = useState(false);
|
|
171
|
+
const [filters, setFilters] = useState({
|
|
172
|
+
tool: false,
|
|
173
|
+
result: false,
|
|
174
|
+
error: false,
|
|
175
|
+
});
|
|
176
|
+
const messagesRef = useRef(null);
|
|
177
|
+
const inputRef = useRef(null);
|
|
178
|
+
const messages = sessionMessages.value || [];
|
|
179
|
+
|
|
180
|
+
const session = (sessionsData.value || []).find((s) => s.id === sessionId);
|
|
181
|
+
const isActive =
|
|
182
|
+
session?.status === "active" || session?.status === "running";
|
|
183
|
+
const resumeLabel =
|
|
184
|
+
session?.status === "archived" ? "Unarchive" : "Resume Session";
|
|
185
|
+
|
|
186
|
+
const activeFilters = Object.entries(filters)
|
|
187
|
+
.filter(([, enabled]) => enabled)
|
|
188
|
+
.map(([key]) => key);
|
|
189
|
+
|
|
190
|
+
const counts = useMemo(() => {
|
|
191
|
+
return messages.reduce(
|
|
192
|
+
(acc, msg) => {
|
|
193
|
+
const category = categorizeMessage(msg);
|
|
194
|
+
acc.total += 1;
|
|
195
|
+
if (category === "tool") acc.tool += 1;
|
|
196
|
+
else if (category === "result") acc.result += 1;
|
|
197
|
+
else if (category === "error") acc.error += 1;
|
|
198
|
+
else acc.message += 1;
|
|
199
|
+
return acc;
|
|
200
|
+
},
|
|
201
|
+
{ total: 0, tool: 0, result: 0, error: 0, message: 0 },
|
|
202
|
+
);
|
|
203
|
+
}, [messages]);
|
|
204
|
+
|
|
205
|
+
const filteredMessages = useMemo(() => {
|
|
206
|
+
if (activeFilters.length === 0) return messages;
|
|
207
|
+
return messages.filter((msg) => activeFilters.includes(categorizeMessage(msg)));
|
|
208
|
+
}, [messages, activeFilters]);
|
|
209
|
+
|
|
210
|
+
const refreshMessages = useCallback(async () => {
|
|
211
|
+
if (!sessionId) return;
|
|
212
|
+
setLoading(true);
|
|
213
|
+
await loadSessionMessages(sessionId).finally(() => setLoading(false));
|
|
214
|
+
}, [sessionId]);
|
|
215
|
+
|
|
216
|
+
/* Load messages on mount and poll while active */
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!sessionId) return;
|
|
219
|
+
let active = true;
|
|
220
|
+
if (!paused) {
|
|
221
|
+
setLoading(true);
|
|
222
|
+
loadSessionMessages(sessionId).finally(() => {
|
|
223
|
+
if (active) setLoading(false);
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
setLoading(false);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const interval = setInterval(() => {
|
|
230
|
+
if (active && !paused) loadSessionMessages(sessionId);
|
|
231
|
+
}, 3000);
|
|
232
|
+
|
|
233
|
+
return () => {
|
|
234
|
+
active = false;
|
|
235
|
+
clearInterval(interval);
|
|
236
|
+
};
|
|
237
|
+
}, [sessionId, session?.status, paused]);
|
|
238
|
+
|
|
239
|
+
/* Auto-scroll to bottom */
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!paused && messagesRef.current) {
|
|
242
|
+
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
|
|
243
|
+
}
|
|
244
|
+
}, [messages.length, paused]);
|
|
245
|
+
|
|
246
|
+
const handleSend = useCallback(async () => {
|
|
247
|
+
const text = input.trim();
|
|
248
|
+
if (!text || sending || readOnly) return;
|
|
249
|
+
|
|
250
|
+
/* Optimistically add user message */
|
|
251
|
+
const optimistic = {
|
|
252
|
+
id: `opt-${Date.now()}`,
|
|
253
|
+
role: "user",
|
|
254
|
+
content: text,
|
|
255
|
+
timestamp: new Date().toISOString(),
|
|
256
|
+
};
|
|
257
|
+
sessionMessages.value = [...sessionMessages.value, optimistic];
|
|
258
|
+
setInput("");
|
|
259
|
+
setSending(true);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await apiFetch(`/api/sessions/${sessionId}/message`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
body: JSON.stringify({ content: text }),
|
|
265
|
+
});
|
|
266
|
+
await loadSessionMessages(sessionId);
|
|
267
|
+
} catch {
|
|
268
|
+
showToast("Failed to send message", "error");
|
|
269
|
+
} finally {
|
|
270
|
+
setSending(false);
|
|
271
|
+
}
|
|
272
|
+
}, [input, sending, sessionId]);
|
|
273
|
+
|
|
274
|
+
const handleResume = useCallback(async () => {
|
|
275
|
+
try {
|
|
276
|
+
await apiFetch(`/api/sessions/${sessionId}/resume`, { method: "POST" });
|
|
277
|
+
showToast(
|
|
278
|
+
session?.status === "archived" ? "Session unarchived" : "Session resumed",
|
|
279
|
+
"success",
|
|
280
|
+
);
|
|
281
|
+
await loadSessions();
|
|
282
|
+
await loadSessionMessages(sessionId);
|
|
283
|
+
} catch {
|
|
284
|
+
showToast("Failed to resume session", "error");
|
|
285
|
+
}
|
|
286
|
+
}, [sessionId]);
|
|
287
|
+
|
|
288
|
+
const handleArchive = useCallback(async () => {
|
|
289
|
+
try {
|
|
290
|
+
await apiFetch(`/api/sessions/${sessionId}/archive`, { method: "POST" });
|
|
291
|
+
showToast("Session archived", "success");
|
|
292
|
+
await loadSessions();
|
|
293
|
+
await loadSessionMessages(sessionId);
|
|
294
|
+
} catch {
|
|
295
|
+
showToast("Failed to archive session", "error");
|
|
296
|
+
}
|
|
297
|
+
}, [sessionId]);
|
|
298
|
+
|
|
299
|
+
const handleKeyDown = useCallback(
|
|
300
|
+
(e) => {
|
|
301
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
handleSend();
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
[handleSend],
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const handleInput = useCallback((e) => {
|
|
310
|
+
setInput(e.target.value);
|
|
311
|
+
const el = e.target;
|
|
312
|
+
el.style.height = 'auto';
|
|
313
|
+
el.style.height = Math.min(el.scrollHeight, 100) + 'px';
|
|
314
|
+
}, []);
|
|
315
|
+
|
|
316
|
+
const toggleFilter = useCallback((key) => {
|
|
317
|
+
setFilters((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
const clearFilters = useCallback(() => {
|
|
321
|
+
setFilters({ tool: false, result: false, error: false });
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
const handleCopyStream = useCallback(() => {
|
|
325
|
+
if (!sessionId) return;
|
|
326
|
+
const title = session?.title || session?.taskId || sessionId;
|
|
327
|
+
const filterLabel =
|
|
328
|
+
activeFilters.length > 0 ? activeFilters.join(", ") : "all";
|
|
329
|
+
const header = `Session: ${title}\nStatus: ${session?.status || "unknown"}\nFilters: ${filterLabel}\nExported: ${new Date().toISOString()}\n\n`;
|
|
330
|
+
const lines = filteredMessages.map(formatMessageLine).join("\n");
|
|
331
|
+
const payload = `${header}${lines}`.trim();
|
|
332
|
+
navigator.clipboard
|
|
333
|
+
.writeText(payload)
|
|
334
|
+
.then(() => showToast("Stream copied", "success"))
|
|
335
|
+
.catch(() => showToast("Copy failed", "error"));
|
|
336
|
+
}, [activeFilters, filteredMessages, session, sessionId]);
|
|
337
|
+
|
|
338
|
+
const handleExportStream = useCallback(() => {
|
|
339
|
+
if (!sessionId) return;
|
|
340
|
+
const payload = {
|
|
341
|
+
sessionId,
|
|
342
|
+
title: session?.title || session?.taskId || null,
|
|
343
|
+
status: session?.status || null,
|
|
344
|
+
exportedAt: new Date().toISOString(),
|
|
345
|
+
filters: activeFilters,
|
|
346
|
+
messages: filteredMessages,
|
|
347
|
+
};
|
|
348
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
|
349
|
+
type: "application/json",
|
|
350
|
+
});
|
|
351
|
+
const url = URL.createObjectURL(blob);
|
|
352
|
+
const link = document.createElement("a");
|
|
353
|
+
link.href = url;
|
|
354
|
+
link.download = `session-${sessionId}-${Date.now()}.json`;
|
|
355
|
+
document.body.appendChild(link);
|
|
356
|
+
link.click();
|
|
357
|
+
link.remove();
|
|
358
|
+
URL.revokeObjectURL(url);
|
|
359
|
+
showToast("Stream exported", "success");
|
|
360
|
+
}, [activeFilters, filteredMessages, session, sessionId]);
|
|
361
|
+
|
|
362
|
+
if (!sessionId) {
|
|
363
|
+
return html`
|
|
364
|
+
<div class="chat-view chat-empty-state">
|
|
365
|
+
<div class="session-empty-icon">💬</div>
|
|
366
|
+
<div class="session-empty-text">
|
|
367
|
+
Select a session to view the live stream.
|
|
368
|
+
<div class="session-empty-subtext">Create a new session or pick one on the left.</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return html`
|
|
375
|
+
<div class="chat-view">
|
|
376
|
+
<div class="chat-header">
|
|
377
|
+
<div class="chat-header-info">
|
|
378
|
+
<div class="chat-header-title">
|
|
379
|
+
${session?.title || session?.taskId || "Session"}
|
|
380
|
+
</div>
|
|
381
|
+
<div class="chat-header-meta">
|
|
382
|
+
${session?.type || "manual"} · ${session?.status || "unknown"}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="chat-header-actions">
|
|
386
|
+
${session?.status === "archived" &&
|
|
387
|
+
html`
|
|
388
|
+
<button class="btn btn-primary btn-sm" onClick=${handleResume}>
|
|
389
|
+
Unarchive
|
|
390
|
+
</button>
|
|
391
|
+
`}
|
|
392
|
+
${session?.status !== "archived" &&
|
|
393
|
+
html`
|
|
394
|
+
<button class="btn btn-ghost btn-sm" onClick=${handleArchive}>
|
|
395
|
+
Archive
|
|
396
|
+
</button>
|
|
397
|
+
`}
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<div class="chat-toolbar">
|
|
402
|
+
<div class="chat-toolbar-left">
|
|
403
|
+
<div class="chat-filter-group">
|
|
404
|
+
<button
|
|
405
|
+
class="chat-filter-chip ${activeFilters.length === 0 ? "active" : ""}"
|
|
406
|
+
onClick=${clearFilters}
|
|
407
|
+
>
|
|
408
|
+
All
|
|
409
|
+
<span class="chat-filter-count">${counts.total}</span>
|
|
410
|
+
</button>
|
|
411
|
+
<button
|
|
412
|
+
class="chat-filter-chip ${filters.tool ? "active" : ""}"
|
|
413
|
+
onClick=${() => toggleFilter("tool")}
|
|
414
|
+
>
|
|
415
|
+
Tool
|
|
416
|
+
<span class="chat-filter-count">${counts.tool}</span>
|
|
417
|
+
</button>
|
|
418
|
+
<button
|
|
419
|
+
class="chat-filter-chip ${filters.result ? "active" : ""}"
|
|
420
|
+
onClick=${() => toggleFilter("result")}
|
|
421
|
+
>
|
|
422
|
+
Result
|
|
423
|
+
<span class="chat-filter-count">${counts.result}</span>
|
|
424
|
+
</button>
|
|
425
|
+
<button
|
|
426
|
+
class="chat-filter-chip ${filters.error ? "active" : ""}"
|
|
427
|
+
onClick=${() => toggleFilter("error")}
|
|
428
|
+
>
|
|
429
|
+
Error
|
|
430
|
+
<span class="chat-filter-count">${counts.error}</span>
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
${paused &&
|
|
434
|
+
html`<span class="chat-paused-pill">Paused</span>`}
|
|
435
|
+
</div>
|
|
436
|
+
<div class="chat-toolbar-actions">
|
|
437
|
+
<button class="btn btn-ghost btn-sm" onClick=${refreshMessages}>
|
|
438
|
+
🔄 Refresh
|
|
439
|
+
</button>
|
|
440
|
+
<button
|
|
441
|
+
class="btn btn-ghost btn-sm"
|
|
442
|
+
onClick=${() => setPaused((prev) => !prev)}
|
|
443
|
+
>
|
|
444
|
+
${paused ? "▶ Resume" : "⏸ Pause"}
|
|
445
|
+
</button>
|
|
446
|
+
<button class="btn btn-ghost btn-sm" onClick=${handleCopyStream}>
|
|
447
|
+
📋 Copy
|
|
448
|
+
</button>
|
|
449
|
+
<button class="btn btn-ghost btn-sm" onClick=${handleExportStream}>
|
|
450
|
+
⬇️ Export
|
|
451
|
+
</button>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div class="chat-messages" ref=${messagesRef}>
|
|
456
|
+
${loading && messages.length === 0 && html`
|
|
457
|
+
<div class="chat-loading">Loading messages…</div>
|
|
458
|
+
`}
|
|
459
|
+
${!loading && messages.length === 0 && html`
|
|
460
|
+
<div class="chat-empty-state-inline">
|
|
461
|
+
<div class="session-empty-icon">🛰️</div>
|
|
462
|
+
<div class="session-empty-text">
|
|
463
|
+
No messages yet.
|
|
464
|
+
<div class="session-empty-subtext">
|
|
465
|
+
${readOnly ? "Stream will appear once the agent starts." : "Send a message to kick things off."}
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
`}
|
|
470
|
+
${messages.length > 0 && filteredMessages.length === 0 && html`
|
|
471
|
+
<div class="chat-empty-state-inline">
|
|
472
|
+
<div class="session-empty-icon">🧰</div>
|
|
473
|
+
<div class="session-empty-text">
|
|
474
|
+
No messages match these filters.
|
|
475
|
+
<div class="session-empty-subtext">Try clearing filters or wait for new tool events.</div>
|
|
476
|
+
</div>
|
|
477
|
+
<button class="btn btn-primary btn-sm mt-sm" onClick=${clearFilters}>
|
|
478
|
+
Clear Filters
|
|
479
|
+
</button>
|
|
480
|
+
</div>
|
|
481
|
+
`}
|
|
482
|
+
${filteredMessages.map((msg) => {
|
|
483
|
+
const isTool =
|
|
484
|
+
msg.type === "tool_call" || msg.type === "tool_result";
|
|
485
|
+
const isError = msg.type === "error" || msg.type === "stream_error";
|
|
486
|
+
const role = msg.role ||
|
|
487
|
+
(isTool || isError ? "system" : msg.type === "system" ? "system" : "assistant");
|
|
488
|
+
const bubbleClass = isError
|
|
489
|
+
? "error"
|
|
490
|
+
: isTool
|
|
491
|
+
? "tool"
|
|
492
|
+
: role === "user"
|
|
493
|
+
? "user"
|
|
494
|
+
: role === "system"
|
|
495
|
+
? "system"
|
|
496
|
+
: "assistant";
|
|
497
|
+
const label =
|
|
498
|
+
isTool
|
|
499
|
+
? msg.type === "tool_call"
|
|
500
|
+
? "TOOL CALL"
|
|
501
|
+
: "TOOL RESULT"
|
|
502
|
+
: isError
|
|
503
|
+
? "ERROR"
|
|
504
|
+
: null;
|
|
505
|
+
return html`
|
|
506
|
+
<div
|
|
507
|
+
key=${msg.id || msg.timestamp}
|
|
508
|
+
class="chat-bubble ${bubbleClass}"
|
|
509
|
+
>
|
|
510
|
+
${role === "system" && !isTool
|
|
511
|
+
? html`<div class="chat-system-text">${msg.content}</div>`
|
|
512
|
+
: html`
|
|
513
|
+
${label
|
|
514
|
+
? html`<div class="chat-bubble-label">${label}</div>`
|
|
515
|
+
: null}
|
|
516
|
+
<div class="chat-bubble-content">
|
|
517
|
+
<${MessageContent} text=${msg.content} />
|
|
518
|
+
</div>
|
|
519
|
+
<div class="chat-bubble-time">
|
|
520
|
+
${msg.timestamp ? formatRelative(msg.timestamp) : ""}
|
|
521
|
+
</div>
|
|
522
|
+
`}
|
|
523
|
+
</div>
|
|
524
|
+
`;
|
|
525
|
+
})}
|
|
526
|
+
${sending && html`
|
|
527
|
+
<div class="chat-bubble assistant">
|
|
528
|
+
<div class="chat-typing">
|
|
529
|
+
<span class="chat-typing-dot"></span>
|
|
530
|
+
<span class="chat-typing-dot"></span>
|
|
531
|
+
<span class="chat-typing-dot"></span>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
`}
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
${!readOnly && html`
|
|
538
|
+
<div class="chat-input-bar">
|
|
539
|
+
${!isActive && session?.status &&
|
|
540
|
+
html`
|
|
541
|
+
<button class="btn btn-primary btn-sm chat-resume-btn" onClick=${handleResume}>
|
|
542
|
+
▶ ${resumeLabel}
|
|
543
|
+
</button>
|
|
544
|
+
`}
|
|
545
|
+
<div class="chat-input-row">
|
|
546
|
+
<textarea
|
|
547
|
+
ref=${inputRef}
|
|
548
|
+
class="input chat-input"
|
|
549
|
+
placeholder="Send a message…"
|
|
550
|
+
rows="1"
|
|
551
|
+
value=${input}
|
|
552
|
+
onInput=${handleInput}
|
|
553
|
+
onKeyDown=${handleKeyDown}
|
|
554
|
+
/>
|
|
555
|
+
<button
|
|
556
|
+
class="btn btn-primary chat-send-btn"
|
|
557
|
+
disabled=${!input.trim() || sending}
|
|
558
|
+
onClick=${handleSend}
|
|
559
|
+
>
|
|
560
|
+
➤
|
|
561
|
+
</button>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
`}
|
|
565
|
+
</div>
|
|
566
|
+
`;
|
|
567
|
+
}
|