@teamclaws/teamclaw 2026.3.21
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 +37 -0
- package/api.ts +10 -0
- package/index.ts +246 -0
- package/openclaw.plugin.json +41 -0
- package/package.json +63 -0
- package/src/config.ts +297 -0
- package/src/controller/controller-service.ts +197 -0
- package/src/controller/controller-tools.ts +224 -0
- package/src/controller/http-server.ts +1946 -0
- package/src/controller/local-worker-manager.ts +531 -0
- package/src/controller/message-router.ts +62 -0
- package/src/controller/prompt-injector.ts +116 -0
- package/src/controller/task-router.ts +97 -0
- package/src/controller/websocket.ts +63 -0
- package/src/controller/worker-provisioning.ts +1286 -0
- package/src/discovery.ts +101 -0
- package/src/git-collaboration.ts +690 -0
- package/src/identity.ts +149 -0
- package/src/openclaw-workspace.ts +101 -0
- package/src/protocol.ts +88 -0
- package/src/roles.ts +275 -0
- package/src/state.ts +118 -0
- package/src/task-executor.ts +478 -0
- package/src/types.ts +469 -0
- package/src/ui/app.js +1400 -0
- package/src/ui/index.html +207 -0
- package/src/ui/style.css +1281 -0
- package/src/worker/http-handler.ts +136 -0
- package/src/worker/message-queue.ts +31 -0
- package/src/worker/prompt-injector.ts +72 -0
- package/src/worker/tools.ts +318 -0
- package/src/worker/worker-service.ts +194 -0
- package/src/workspace-browser.ts +312 -0
- package/tsconfig.json +22 -0
package/src/ui/app.js
ADDED
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
// TeamClaw Web UI
|
|
2
|
+
(function () {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const API_BASE = "/api/v1";
|
|
6
|
+
let ws = null;
|
|
7
|
+
let currentFilter = "all";
|
|
8
|
+
let activeTab = "tasks";
|
|
9
|
+
let teamState = { workers: [], tasks: [], messages: [], clarifications: [] };
|
|
10
|
+
let selectedTaskId = null;
|
|
11
|
+
let selectedTaskDetail = null;
|
|
12
|
+
let selectedTaskDetailTab = "overview";
|
|
13
|
+
let followTaskOutput = true;
|
|
14
|
+
let workspaceTree = [];
|
|
15
|
+
let selectedWorkspacePath = null;
|
|
16
|
+
let selectedWorkspaceFile = null;
|
|
17
|
+
let selectedWorkspaceView = "source";
|
|
18
|
+
let workspaceLoaded = false;
|
|
19
|
+
const CONTROLLER_SESSION_STORAGE_KEY = "teamclaw.controllerSessionKey";
|
|
20
|
+
const CONTROLLER_CONVERSATION_STORAGE_KEY = "teamclaw.controllerConversation";
|
|
21
|
+
let controllerConversation = loadControllerConversation();
|
|
22
|
+
let controllerCommandPending = false;
|
|
23
|
+
|
|
24
|
+
function $(selector) { return document.querySelector(selector); }
|
|
25
|
+
function $$(selector) { return document.querySelectorAll(selector); }
|
|
26
|
+
|
|
27
|
+
function getSessionStorage() {
|
|
28
|
+
try {
|
|
29
|
+
return window.sessionStorage;
|
|
30
|
+
} catch (_err) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadControllerConversation() {
|
|
36
|
+
const storage = getSessionStorage();
|
|
37
|
+
if (!storage) return [];
|
|
38
|
+
try {
|
|
39
|
+
const raw = storage.getItem(CONTROLLER_CONVERSATION_STORAGE_KEY);
|
|
40
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
41
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
42
|
+
} catch (_err) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveControllerConversation() {
|
|
48
|
+
const storage = getSessionStorage();
|
|
49
|
+
if (!storage) return;
|
|
50
|
+
storage.setItem(CONTROLLER_CONVERSATION_STORAGE_KEY, JSON.stringify(controllerConversation.slice(-50)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getControllerSessionKey() {
|
|
54
|
+
const storage = getSessionStorage();
|
|
55
|
+
const fallback = "default";
|
|
56
|
+
if (!storage) return fallback;
|
|
57
|
+
let sessionKey = storage.getItem(CONTROLLER_SESSION_STORAGE_KEY);
|
|
58
|
+
if (!sessionKey) {
|
|
59
|
+
sessionKey = (window.crypto && typeof window.crypto.randomUUID === "function")
|
|
60
|
+
? window.crypto.randomUUID()
|
|
61
|
+
: ("web-" + Date.now());
|
|
62
|
+
storage.setItem(CONTROLLER_SESSION_STORAGE_KEY, sessionKey);
|
|
63
|
+
}
|
|
64
|
+
return sessionKey;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createControllerConversationEntry(entry) {
|
|
68
|
+
return Object.assign({
|
|
69
|
+
id: "controller-ui-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8),
|
|
70
|
+
createdAt: Date.now(),
|
|
71
|
+
}, entry);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function appendControllerConversation(entry) {
|
|
75
|
+
controllerConversation = controllerConversation.concat([createControllerConversationEntry(entry)]).slice(-50);
|
|
76
|
+
saveControllerConversation();
|
|
77
|
+
renderMessages(teamState.messages || []);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function escapeHtml(str) {
|
|
81
|
+
const div = document.createElement("div");
|
|
82
|
+
div.textContent = str == null ? "" : String(str);
|
|
83
|
+
return div.innerHTML;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatTime(ts) {
|
|
87
|
+
if (!ts) return "";
|
|
88
|
+
const d = new Date(ts);
|
|
89
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
90
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
91
|
+
const s = String(d.getSeconds()).padStart(2, "0");
|
|
92
|
+
return h + ":" + m + ":" + s;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function humanizeStatus(value) {
|
|
96
|
+
return String(value || "").replace(/_/g, " ").replace(/-/g, " ");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatBytes(value) {
|
|
100
|
+
const bytes = Number(value || 0);
|
|
101
|
+
if (!bytes) return "0 B";
|
|
102
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
103
|
+
let size = bytes;
|
|
104
|
+
let unitIndex = 0;
|
|
105
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
106
|
+
size /= 1024;
|
|
107
|
+
unitIndex += 1;
|
|
108
|
+
}
|
|
109
|
+
const digits = size >= 10 || unitIndex === 0 ? 0 : 1;
|
|
110
|
+
return size.toFixed(digits) + " " + units[unitIndex];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isWorkspacePreviewAvailable(file) {
|
|
114
|
+
return !!file && (file.previewType === "markdown" || file.previewType === "html");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sanitizeUrl(url) {
|
|
118
|
+
const value = String(url || "").trim();
|
|
119
|
+
if (!value) return "#";
|
|
120
|
+
if (/^(https?:|mailto:)/i.test(value)) {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || !value.includes(":")) {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
return "#";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderMarkdownInline(text) {
|
|
130
|
+
const codeTokens = [];
|
|
131
|
+
let safe = escapeHtml(text || "");
|
|
132
|
+
safe = safe.replace(/`([^`]+)`/g, function (_match, code) {
|
|
133
|
+
const token = "@@CODE-TOKEN-" + codeTokens.length + "@@";
|
|
134
|
+
codeTokens.push("<code>" + escapeHtml(code) + "</code>");
|
|
135
|
+
return token;
|
|
136
|
+
});
|
|
137
|
+
safe = safe.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_match, label, url) {
|
|
138
|
+
return '<a href="' + escapeHtml(sanitizeUrl(url)) + '" target="_blank" rel="noreferrer">' + escapeHtml(label) + "</a>";
|
|
139
|
+
});
|
|
140
|
+
safe = safe.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
141
|
+
safe = safe.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
142
|
+
safe = safe.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
143
|
+
safe = safe.replace(/_([^_]+)_/g, "<em>$1</em>");
|
|
144
|
+
codeTokens.forEach(function (tokenValue, index) {
|
|
145
|
+
safe = safe.replace("@@CODE-TOKEN-" + index + "@@", tokenValue);
|
|
146
|
+
});
|
|
147
|
+
return safe;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseMarkdownTableRow(line) {
|
|
151
|
+
const trimmed = String(line || "").trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
152
|
+
return trimmed.split("|").map(function (cell) {
|
|
153
|
+
return renderMarkdownInline(cell.trim());
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderMarkdown(markdown) {
|
|
158
|
+
const codeBlocks = [];
|
|
159
|
+
const lines = String(markdown || "")
|
|
160
|
+
.replace(/\r\n?/g, "\n")
|
|
161
|
+
.replace(/```([\w-]*)\n([\s\S]*?)```/g, function (_match, language, code) {
|
|
162
|
+
const token = "@@FENCE-BLOCK-" + codeBlocks.length + "@@";
|
|
163
|
+
codeBlocks.push(
|
|
164
|
+
'<pre><code data-language="' + escapeHtml(language || "") + '">' + escapeHtml(code.replace(/\n$/, "")) + "</code></pre>"
|
|
165
|
+
);
|
|
166
|
+
return token;
|
|
167
|
+
})
|
|
168
|
+
.split("\n");
|
|
169
|
+
const html = [];
|
|
170
|
+
let index = 0;
|
|
171
|
+
|
|
172
|
+
while (index < lines.length) {
|
|
173
|
+
const rawLine = lines[index] || "";
|
|
174
|
+
const line = rawLine.trim();
|
|
175
|
+
|
|
176
|
+
if (!line) {
|
|
177
|
+
index += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (/^@@FENCE-BLOCK-\d+@@$/.test(line)) {
|
|
182
|
+
const blockIndex = Number(line.replace(/\D/g, ""));
|
|
183
|
+
html.push(codeBlocks[blockIndex] || "");
|
|
184
|
+
index += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (/^#{1,6}\s+/.test(line)) {
|
|
189
|
+
const level = Math.min(6, line.match(/^#+/)[0].length);
|
|
190
|
+
html.push("<h" + level + ">" + renderMarkdownInline(line.slice(level).trim()) + "</h" + level + ">");
|
|
191
|
+
index += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (/^>\s?/.test(line)) {
|
|
196
|
+
const quoteLines = [];
|
|
197
|
+
while (index < lines.length && /^>\s?/.test((lines[index] || "").trim())) {
|
|
198
|
+
quoteLines.push(renderMarkdownInline((lines[index] || "").trim().replace(/^>\s?/, "")));
|
|
199
|
+
index += 1;
|
|
200
|
+
}
|
|
201
|
+
html.push("<blockquote><p>" + quoteLines.join("<br>") + "</p></blockquote>");
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (/^[-*_]{3,}$/.test(line)) {
|
|
206
|
+
html.push("<hr>");
|
|
207
|
+
index += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (line.includes("|") && index + 1 < lines.length && /^\s*\|?[\s:-]+\|[\s|:-]*$/.test(lines[index + 1] || "")) {
|
|
212
|
+
const headers = parseMarkdownTableRow(line);
|
|
213
|
+
const rows = [];
|
|
214
|
+
index += 2;
|
|
215
|
+
while (index < lines.length && (lines[index] || "").includes("|")) {
|
|
216
|
+
rows.push(parseMarkdownTableRow(lines[index]));
|
|
217
|
+
index += 1;
|
|
218
|
+
}
|
|
219
|
+
html.push(
|
|
220
|
+
"<table><thead><tr>" + headers.map(function (cell) { return "<th>" + cell + "</th>"; }).join("") + "</tr></thead>" +
|
|
221
|
+
"<tbody>" + rows.map(function (row) {
|
|
222
|
+
return "<tr>" + row.map(function (cell) { return "<td>" + cell + "</td>"; }).join("") + "</tr>";
|
|
223
|
+
}).join("") + "</tbody></table>"
|
|
224
|
+
);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (/^([-*+]\s+|\d+\.\s+)/.test(line)) {
|
|
229
|
+
const ordered = /^\d+\.\s+/.test(line);
|
|
230
|
+
const items = [];
|
|
231
|
+
while (index < lines.length) {
|
|
232
|
+
const current = (lines[index] || "").trim();
|
|
233
|
+
const matchesList = ordered ? /^\d+\.\s+/.test(current) : /^[-*+]\s+/.test(current);
|
|
234
|
+
if (!matchesList) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
items.push(renderMarkdownInline(current.replace(/^([-*+]\s+|\d+\.\s+)/, "")));
|
|
238
|
+
index += 1;
|
|
239
|
+
}
|
|
240
|
+
html.push((ordered ? "<ol>" : "<ul>") + items.map(function (item) {
|
|
241
|
+
return "<li>" + item + "</li>";
|
|
242
|
+
}).join("") + (ordered ? "</ol>" : "</ul>"));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const paragraphLines = [];
|
|
247
|
+
while (index < lines.length) {
|
|
248
|
+
const currentLine = lines[index] || "";
|
|
249
|
+
const current = currentLine.trim();
|
|
250
|
+
if (!current ||
|
|
251
|
+
/^@@FENCE-BLOCK-\d+@@$/.test(current) ||
|
|
252
|
+
/^#{1,6}\s+/.test(current) ||
|
|
253
|
+
/^>\s?/.test(current) ||
|
|
254
|
+
/^[-*_]{3,}$/.test(current) ||
|
|
255
|
+
/^([-*+]\s+|\d+\.\s+)/.test(current) ||
|
|
256
|
+
(current.includes("|") && index + 1 < lines.length && /^\s*\|?[\s:-]+\|[\s|:-]*$/.test(lines[index + 1] || ""))) {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
paragraphLines.push(renderMarkdownInline(current));
|
|
260
|
+
index += 1;
|
|
261
|
+
}
|
|
262
|
+
html.push("<p>" + paragraphLines.join(" ") + "</p>");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return html.join("");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderMarkdownContent(content) {
|
|
269
|
+
return renderMarkdown(String(content || ""));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function renderMarkdownCard(content) {
|
|
273
|
+
return '<div class="task-detail-card markdown-body">' + renderMarkdownContent(content) + "</div>";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function findWorkspaceNodeByPath(nodes, relativePath) {
|
|
277
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
278
|
+
const node = nodes[index];
|
|
279
|
+
if (node.path === relativePath) {
|
|
280
|
+
return node;
|
|
281
|
+
}
|
|
282
|
+
if (node.type === "directory" && node.children && node.children.length) {
|
|
283
|
+
const found = findWorkspaceNodeByPath(node.children, relativePath);
|
|
284
|
+
if (found) {
|
|
285
|
+
return found;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function findDefaultWorkspacePath(nodes) {
|
|
293
|
+
const preferredNames = ["README.md", "SPEC.md", "index.html"];
|
|
294
|
+
const queue = [].concat(nodes || []);
|
|
295
|
+
let firstFile = null;
|
|
296
|
+
|
|
297
|
+
while (queue.length > 0) {
|
|
298
|
+
const node = queue.shift();
|
|
299
|
+
if (!node) continue;
|
|
300
|
+
if (node.type === "file") {
|
|
301
|
+
if (!firstFile) {
|
|
302
|
+
firstFile = node.path;
|
|
303
|
+
}
|
|
304
|
+
if (preferredNames.indexOf(node.name) !== -1) {
|
|
305
|
+
return node.path;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (node.type === "directory" && Array.isArray(node.children)) {
|
|
309
|
+
queue.push.apply(queue, node.children);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return firstFile;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isTaskLive(task) {
|
|
317
|
+
return !!task && ["assigned", "in_progress", "review"].indexOf(task.status) !== -1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getTaskById(taskId) {
|
|
321
|
+
return (teamState.tasks || []).find(function (task) { return task.id === taskId; }) || null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function getSelectedTaskExecution() {
|
|
325
|
+
if (!selectedTaskDetail || !selectedTaskDetail.task || !selectedTaskDetail.task.execution) {
|
|
326
|
+
return { events: [] };
|
|
327
|
+
}
|
|
328
|
+
return selectedTaskDetail.task.execution;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function showError(message) {
|
|
332
|
+
window.alert(message);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function apiRequest(path, options) {
|
|
336
|
+
const res = await fetch(API_BASE + path, options);
|
|
337
|
+
let data = {};
|
|
338
|
+
try {
|
|
339
|
+
data = await res.json();
|
|
340
|
+
} catch (_err) {
|
|
341
|
+
data = {};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!res.ok) {
|
|
345
|
+
const message = data && (data.error || data.message)
|
|
346
|
+
? (data.error || data.message)
|
|
347
|
+
: ("Request failed: " + res.status);
|
|
348
|
+
throw new Error(message);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return data;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function apiGet(path) {
|
|
355
|
+
return apiRequest(path);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function apiPost(path, body) {
|
|
359
|
+
return apiRequest(path, {
|
|
360
|
+
method: "POST",
|
|
361
|
+
headers: { "Content-Type": "application/json" },
|
|
362
|
+
body: JSON.stringify(body),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function refreshWorkspaceTree(silent) {
|
|
367
|
+
try {
|
|
368
|
+
const data = await apiGet("/workspace/tree");
|
|
369
|
+
workspaceTree = data.entries || [];
|
|
370
|
+
workspaceLoaded = true;
|
|
371
|
+
renderWorkspaceTree(workspaceTree);
|
|
372
|
+
|
|
373
|
+
const nextPath = selectedWorkspacePath && findWorkspaceNodeByPath(workspaceTree, selectedWorkspacePath)
|
|
374
|
+
? selectedWorkspacePath
|
|
375
|
+
: findDefaultWorkspacePath(workspaceTree);
|
|
376
|
+
|
|
377
|
+
if (!nextPath) {
|
|
378
|
+
selectedWorkspacePath = null;
|
|
379
|
+
selectedWorkspaceFile = null;
|
|
380
|
+
selectedWorkspaceView = "source";
|
|
381
|
+
renderWorkspaceFile();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (nextPath !== selectedWorkspacePath || !selectedWorkspaceFile) {
|
|
386
|
+
await loadWorkspaceFile(nextPath, { silent: true });
|
|
387
|
+
} else if (activeTab === "workspace") {
|
|
388
|
+
await loadWorkspaceFile(nextPath, { keepView: true, silent: true });
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error("Failed to load workspace tree:", err);
|
|
392
|
+
if (!silent) {
|
|
393
|
+
showError(err instanceof Error ? err.message : "Failed to load workspace tree");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function loadWorkspaceFile(relativePath, options) {
|
|
399
|
+
const settings = Object.assign({ keepView: false, silent: false }, options || {});
|
|
400
|
+
try {
|
|
401
|
+
const data = await apiGet("/workspace/file?path=" + encodeURIComponent(relativePath));
|
|
402
|
+
selectedWorkspacePath = relativePath;
|
|
403
|
+
selectedWorkspaceFile = data.file || null;
|
|
404
|
+
if (!(settings.keepView && selectedWorkspaceView === "preview" && isWorkspacePreviewAvailable(selectedWorkspaceFile))) {
|
|
405
|
+
selectedWorkspaceView = "source";
|
|
406
|
+
}
|
|
407
|
+
renderWorkspaceTree(workspaceTree);
|
|
408
|
+
renderWorkspaceFile();
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.error("Failed to load workspace file:", err);
|
|
411
|
+
if (!settings.silent) {
|
|
412
|
+
showError(err instanceof Error ? err.message : "Failed to load workspace file");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function renderWorkspaceTree(nodes) {
|
|
418
|
+
const container = $("#workspace-tree");
|
|
419
|
+
if (!container) return;
|
|
420
|
+
|
|
421
|
+
if (!workspaceLoaded) {
|
|
422
|
+
container.innerHTML = '<div class="empty-state">Workspace tree loading…</div>';
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!nodes || nodes.length === 0) {
|
|
427
|
+
container.innerHTML = '<div class="empty-state">No project files in the workspace yet.</div>';
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
container.innerHTML = renderWorkspaceTreeNodes(nodes);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function renderWorkspaceTreeNodes(nodes) {
|
|
435
|
+
return '<ul class="workspace-tree-list">' + nodes.map(function (node) {
|
|
436
|
+
if (node.type === "directory") {
|
|
437
|
+
return (
|
|
438
|
+
'<li class="workspace-tree-folder">' +
|
|
439
|
+
' <details open>' +
|
|
440
|
+
' <summary class="workspace-tree-summary">' +
|
|
441
|
+
' <span class="workspace-tree-icon">▾</span>' +
|
|
442
|
+
' <span class="workspace-tree-label">' + escapeHtml(node.name) + "</span>" +
|
|
443
|
+
" </summary>" +
|
|
444
|
+
' <div class="workspace-tree-children">' + renderWorkspaceTreeNodes(node.children || []) + "</div>" +
|
|
445
|
+
" </details>" +
|
|
446
|
+
"</li>"
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const selectedClass = node.path === selectedWorkspacePath ? " is-selected" : "";
|
|
451
|
+
const previewBadge = node.previewType === "markdown"
|
|
452
|
+
? "MD"
|
|
453
|
+
: (node.previewType === "html" ? "HTML" : "FILE");
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
'<li>' +
|
|
457
|
+
' <button type="button" class="workspace-tree-file' + selectedClass + '" data-workspace-path="' + escapeHtml(node.path) + '">' +
|
|
458
|
+
' <span class="workspace-tree-icon">' + escapeHtml(previewBadge) + "</span>" +
|
|
459
|
+
' <span class="workspace-tree-label">' + escapeHtml(node.name) + "</span>" +
|
|
460
|
+
" </button>" +
|
|
461
|
+
"</li>"
|
|
462
|
+
);
|
|
463
|
+
}).join("") + "</ul>";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function renderWorkspaceFile() {
|
|
467
|
+
const fileName = $("#workspace-file-name");
|
|
468
|
+
const fileMeta = $("#workspace-file-meta");
|
|
469
|
+
const openRaw = $("#workspace-open-raw");
|
|
470
|
+
|
|
471
|
+
if (fileName) {
|
|
472
|
+
fileName.textContent = selectedWorkspaceFile ? selectedWorkspaceFile.name : "Select a file";
|
|
473
|
+
}
|
|
474
|
+
if (fileMeta) {
|
|
475
|
+
fileMeta.textContent = selectedWorkspaceFile
|
|
476
|
+
? [selectedWorkspaceFile.path, formatBytes(selectedWorkspaceFile.size), humanizeStatus(selectedWorkspaceFile.previewType)].join(" • ")
|
|
477
|
+
: "Choose a workspace file to inspect source or preview output.";
|
|
478
|
+
}
|
|
479
|
+
if (openRaw) {
|
|
480
|
+
if (selectedWorkspaceFile && selectedWorkspaceFile.rawUrl) {
|
|
481
|
+
openRaw.href = selectedWorkspaceFile.rawUrl;
|
|
482
|
+
openRaw.classList.remove("hidden");
|
|
483
|
+
} else {
|
|
484
|
+
openRaw.classList.add("hidden");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const sourceTab = $("#workspace-view-source");
|
|
489
|
+
const previewTab = $("#workspace-view-preview");
|
|
490
|
+
if (sourceTab) {
|
|
491
|
+
sourceTab.classList.toggle("active", selectedWorkspaceView === "source");
|
|
492
|
+
}
|
|
493
|
+
if (previewTab) {
|
|
494
|
+
const previewEnabled = isWorkspacePreviewAvailable(selectedWorkspaceFile);
|
|
495
|
+
previewTab.disabled = !previewEnabled;
|
|
496
|
+
previewTab.classList.toggle("active", selectedWorkspaceView === "preview" && previewEnabled);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
renderWorkspaceSource();
|
|
500
|
+
renderWorkspacePreview();
|
|
501
|
+
syncWorkspaceViewPanels();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function renderWorkspaceSource() {
|
|
505
|
+
const container = $("#workspace-source-view");
|
|
506
|
+
if (!container) return;
|
|
507
|
+
|
|
508
|
+
if (!selectedWorkspaceFile) {
|
|
509
|
+
container.innerHTML = '<div class="workspace-preview-empty">Select a file from the workspace tree to view its source.</div>';
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (selectedWorkspaceFile.previewType === "binary") {
|
|
514
|
+
container.innerHTML = '<div class="workspace-preview-empty">This file looks binary. Use <strong>Open Raw</strong> to inspect or download it.</div>';
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const content = selectedWorkspaceFile.content || "";
|
|
519
|
+
const lines = content.split("\n");
|
|
520
|
+
const warning = selectedWorkspaceFile.truncated
|
|
521
|
+
? '<div class="workspace-source-warning">Showing the first 256 KB of this file for UI performance.</div>'
|
|
522
|
+
: "";
|
|
523
|
+
|
|
524
|
+
container.innerHTML =
|
|
525
|
+
'<div class="workspace-source-shell">' +
|
|
526
|
+
warning +
|
|
527
|
+
'<div class="workspace-source-lines">' +
|
|
528
|
+
lines.map(function (line, index) {
|
|
529
|
+
return (
|
|
530
|
+
'<div class="workspace-source-line">' +
|
|
531
|
+
' <div class="workspace-source-line-number">' + (index + 1) + "</div>" +
|
|
532
|
+
' <div class="workspace-source-line-text">' + (line ? escapeHtml(line) : " ") + "</div>" +
|
|
533
|
+
"</div>"
|
|
534
|
+
);
|
|
535
|
+
}).join("") +
|
|
536
|
+
"</div>" +
|
|
537
|
+
"</div>";
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function renderWorkspacePreview() {
|
|
541
|
+
const container = $("#workspace-preview-view");
|
|
542
|
+
if (!container) return;
|
|
543
|
+
|
|
544
|
+
if (!selectedWorkspaceFile) {
|
|
545
|
+
container.innerHTML = '<div class="workspace-preview-empty">Select a file from the workspace tree to preview Markdown or HTML output.</div>';
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (selectedWorkspaceFile.previewType === "markdown") {
|
|
550
|
+
container.innerHTML = '<div class="workspace-markdown-preview markdown-body">' + renderMarkdownContent(selectedWorkspaceFile.content) + "</div>";
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (selectedWorkspaceFile.previewType === "html") {
|
|
555
|
+
container.innerHTML = '<iframe class="workspace-preview-frame" sandbox="allow-scripts allow-forms" src="' + escapeHtml(selectedWorkspaceFile.rawUrl) + '"></iframe>';
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
container.innerHTML = '<div class="workspace-preview-empty">Preview is available for Markdown and HTML files. This file stays in source mode.</div>';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function syncWorkspaceViewPanels() {
|
|
563
|
+
const sourcePanel = $("#workspace-source-view");
|
|
564
|
+
const previewPanel = $("#workspace-preview-view");
|
|
565
|
+
if (sourcePanel) {
|
|
566
|
+
sourcePanel.classList.toggle("active", selectedWorkspaceView === "source");
|
|
567
|
+
}
|
|
568
|
+
if (previewPanel) {
|
|
569
|
+
previewPanel.classList.toggle("active", selectedWorkspaceView === "preview");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function connectWebSocket() {
|
|
574
|
+
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
575
|
+
const wsUrl = `${protocol}://${location.host}/ws`;
|
|
576
|
+
|
|
577
|
+
setStatus("connecting");
|
|
578
|
+
ws = new WebSocket(wsUrl);
|
|
579
|
+
|
|
580
|
+
ws.onopen = function () {
|
|
581
|
+
setStatus("connected");
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
ws.onclose = function () {
|
|
585
|
+
setStatus("disconnected");
|
|
586
|
+
setTimeout(connectWebSocket, 3000);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
ws.onerror = function () {
|
|
590
|
+
ws.close();
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
ws.onmessage = function (event) {
|
|
594
|
+
try {
|
|
595
|
+
const msg = JSON.parse(event.data);
|
|
596
|
+
handleWsEvent(msg);
|
|
597
|
+
} catch (_err) {
|
|
598
|
+
console.error("Invalid WS message:", event.data);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function setStatus(status) {
|
|
604
|
+
const dot = $("#connection-status");
|
|
605
|
+
if (dot) {
|
|
606
|
+
dot.className = "status-dot " + status;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function handleWsEvent(event) {
|
|
611
|
+
const taskId = event && event.data
|
|
612
|
+
? (event.data.taskId || event.data.id || null)
|
|
613
|
+
: null;
|
|
614
|
+
|
|
615
|
+
switch (event.type) {
|
|
616
|
+
case "task:execution":
|
|
617
|
+
handleTaskExecutionEvent(event.data || {});
|
|
618
|
+
break;
|
|
619
|
+
case "worker:online":
|
|
620
|
+
case "worker:offline":
|
|
621
|
+
case "task:created":
|
|
622
|
+
case "task:updated":
|
|
623
|
+
case "task:completed":
|
|
624
|
+
case "message:new":
|
|
625
|
+
case "clarification:requested":
|
|
626
|
+
case "clarification:answered":
|
|
627
|
+
refreshAll();
|
|
628
|
+
if (selectedTaskId && taskId && taskId === selectedTaskId) {
|
|
629
|
+
refreshTaskDetail(true);
|
|
630
|
+
}
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function refreshAll() {
|
|
636
|
+
try {
|
|
637
|
+
const [statusRes, rolesRes] = await Promise.all([
|
|
638
|
+
apiGet("/team/status"),
|
|
639
|
+
apiGet("/roles"),
|
|
640
|
+
]);
|
|
641
|
+
|
|
642
|
+
teamState = {
|
|
643
|
+
workers: statusRes.workers || [],
|
|
644
|
+
tasks: statusRes.tasks || [],
|
|
645
|
+
messages: statusRes.messages || [],
|
|
646
|
+
clarifications: statusRes.clarifications || [],
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
renderWorkers(teamState.workers);
|
|
650
|
+
renderTasks(teamState.tasks);
|
|
651
|
+
renderClarifications(teamState.clarifications);
|
|
652
|
+
renderMessages(teamState.messages);
|
|
653
|
+
renderRoles(rolesRes.roles || []);
|
|
654
|
+
renderClarificationCount(statusRes.pendingClarificationCount || 0);
|
|
655
|
+
|
|
656
|
+
const teamName = $("#team-name");
|
|
657
|
+
if (teamName) {
|
|
658
|
+
teamName.textContent = statusRes.teamName || "Team";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
syncSelectedTaskSummary();
|
|
662
|
+
if (activeTab === "workspace") {
|
|
663
|
+
await refreshWorkspaceTree(true);
|
|
664
|
+
}
|
|
665
|
+
} catch (err) {
|
|
666
|
+
console.error("Failed to refresh:", err);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function renderWorkers(workers) {
|
|
671
|
+
const container = $("#workers-list");
|
|
672
|
+
if (!container) return;
|
|
673
|
+
|
|
674
|
+
if (workers.length === 0) {
|
|
675
|
+
container.innerHTML = '<div class="empty-state">No workers connected</div>';
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
container.innerHTML = workers.map(function (worker) {
|
|
680
|
+
return (
|
|
681
|
+
'<div class="worker-card">' +
|
|
682
|
+
' <span class="worker-icon">' + escapeHtml(worker.label || worker.role).charAt(0) + '</span>' +
|
|
683
|
+
' <span class="worker-label">' + escapeHtml(worker.label || worker.role) + '</span>' +
|
|
684
|
+
' <span class="worker-status ' + escapeHtml(worker.status || "offline") + '">' + escapeHtml(worker.status || "offline") + "</span>" +
|
|
685
|
+
"</div>"
|
|
686
|
+
);
|
|
687
|
+
}).join("");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function renderTasks(tasks) {
|
|
691
|
+
const container = $("#tasks-board");
|
|
692
|
+
if (!container) return;
|
|
693
|
+
|
|
694
|
+
const filtered = currentFilter === "all"
|
|
695
|
+
? tasks
|
|
696
|
+
: tasks.filter(function (task) { return task.status === currentFilter; });
|
|
697
|
+
|
|
698
|
+
if (filtered.length === 0) {
|
|
699
|
+
container.innerHTML = '<div class="empty-state">No tasks' +
|
|
700
|
+
(currentFilter !== "all" ? ' with status "' + escapeHtml(currentFilter) + '"' : "") + "</div>";
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
container.innerHTML = filtered.map(function (task) {
|
|
705
|
+
const priority = task.priority || "medium";
|
|
706
|
+
const status = task.status || "pending";
|
|
707
|
+
const assignee = task.assignedWorkerId
|
|
708
|
+
? "Assigned to " + task.assignedWorkerId
|
|
709
|
+
: (task.assignedRole ? "Role: " + task.assignedRole : "Unassigned");
|
|
710
|
+
const note = task.progress
|
|
711
|
+
? '<div class="task-note">' + escapeHtml(task.progress).slice(0, 220) + "</div>"
|
|
712
|
+
: "";
|
|
713
|
+
const clarification = task.clarificationRequestId
|
|
714
|
+
? '<span>Clarification: ' + escapeHtml(task.clarificationRequestId) + "</span>"
|
|
715
|
+
: "";
|
|
716
|
+
const liveClass = isTaskLive(task) ? " is-live" : "";
|
|
717
|
+
|
|
718
|
+
return (
|
|
719
|
+
'<div class="task-card' + liveClass + '" data-task-id="' + escapeHtml(task.id) + '" tabindex="0" role="button" aria-label="Open details for ' + escapeHtml(task.title) + '">' +
|
|
720
|
+
' <span class="task-priority ' + escapeHtml(priority) + '">' + escapeHtml(priority) + "</span>" +
|
|
721
|
+
' <div class="task-body">' +
|
|
722
|
+
' <div class="task-title">' + escapeHtml(task.title) + "</div>" +
|
|
723
|
+
(task.description ? '<div class="task-desc">' + escapeHtml(task.description).slice(0, 220) + "</div>" : "") +
|
|
724
|
+
note +
|
|
725
|
+
' <div class="task-meta">' +
|
|
726
|
+
' <span class="task-status-badge ' + escapeHtml(status) + '">' + escapeHtml(humanizeStatus(status)) + "</span>" +
|
|
727
|
+
" <span>" + escapeHtml(assignee) + "</span>" +
|
|
728
|
+
clarification +
|
|
729
|
+
" <span>" + escapeHtml(formatTime(task.updatedAt)) + "</span>" +
|
|
730
|
+
" </div>" +
|
|
731
|
+
" </div>" +
|
|
732
|
+
"</div>"
|
|
733
|
+
);
|
|
734
|
+
}).join("");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function syncSelectedTaskSummary() {
|
|
738
|
+
if (!selectedTaskId || !selectedTaskDetail) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const latestTask = getTaskById(selectedTaskId);
|
|
743
|
+
if (!latestTask) {
|
|
744
|
+
closeTaskDetail();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const existingExecution = getSelectedTaskExecution();
|
|
749
|
+
const mergedExecution = Object.assign({}, existingExecution, latestTask.execution || {});
|
|
750
|
+
if (existingExecution.events) {
|
|
751
|
+
mergedExecution.events = existingExecution.events;
|
|
752
|
+
}
|
|
753
|
+
selectedTaskDetail.task = Object.assign({}, selectedTaskDetail.task || {}, latestTask, {
|
|
754
|
+
execution: mergedExecution,
|
|
755
|
+
});
|
|
756
|
+
renderTaskDetail();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function openTaskDetail(taskId) {
|
|
760
|
+
selectedTaskId = taskId;
|
|
761
|
+
selectedTaskDetail = {
|
|
762
|
+
task: getTaskById(taskId),
|
|
763
|
+
messages: [],
|
|
764
|
+
clarifications: [],
|
|
765
|
+
};
|
|
766
|
+
const modal = $("#task-detail-modal");
|
|
767
|
+
if (modal) {
|
|
768
|
+
modal.classList.add("open");
|
|
769
|
+
modal.setAttribute("aria-hidden", "false");
|
|
770
|
+
}
|
|
771
|
+
renderTaskDetail();
|
|
772
|
+
await refreshTaskDetail(false);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function closeTaskDetail() {
|
|
776
|
+
selectedTaskId = null;
|
|
777
|
+
selectedTaskDetail = null;
|
|
778
|
+
const modal = $("#task-detail-modal");
|
|
779
|
+
if (modal) {
|
|
780
|
+
modal.classList.remove("open");
|
|
781
|
+
modal.setAttribute("aria-hidden", "true");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async function refreshTaskDetail(silent) {
|
|
786
|
+
if (!selectedTaskId) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
const data = await apiGet("/tasks/" + selectedTaskId + "/execution");
|
|
792
|
+
selectedTaskDetail = {
|
|
793
|
+
task: data.task || null,
|
|
794
|
+
messages: data.messages || [],
|
|
795
|
+
clarifications: data.clarifications || [],
|
|
796
|
+
};
|
|
797
|
+
renderTaskDetail();
|
|
798
|
+
} catch (err) {
|
|
799
|
+
console.error("Failed to load task detail:", err);
|
|
800
|
+
if (!silent) {
|
|
801
|
+
showError(err instanceof Error ? err.message : "Failed to load task detail");
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function renderTaskDetail() {
|
|
807
|
+
const task = selectedTaskDetail && selectedTaskDetail.task ? selectedTaskDetail.task : null;
|
|
808
|
+
const title = $("#task-detail-title");
|
|
809
|
+
const subtitle = $("#task-detail-subtitle");
|
|
810
|
+
const liveBadge = $("#task-detail-live-badge");
|
|
811
|
+
|
|
812
|
+
if (title) {
|
|
813
|
+
title.textContent = task ? task.title : "Select a task";
|
|
814
|
+
}
|
|
815
|
+
if (subtitle) {
|
|
816
|
+
subtitle.textContent = task
|
|
817
|
+
? [
|
|
818
|
+
"Task ID: " + task.id,
|
|
819
|
+
task.assignedWorkerId ? "Worker: " + task.assignedWorkerId : null,
|
|
820
|
+
task.assignedRole ? "Role: " + task.assignedRole : null,
|
|
821
|
+
].filter(Boolean).join(" • ")
|
|
822
|
+
: "";
|
|
823
|
+
}
|
|
824
|
+
if (liveBadge) {
|
|
825
|
+
const live = isTaskLive(task);
|
|
826
|
+
liveBadge.textContent = live ? "Live" : (task ? humanizeStatus(task.status) : "Idle");
|
|
827
|
+
liveBadge.classList.toggle("is-live", live);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
renderTaskDetailOverview(task);
|
|
831
|
+
renderTaskDetailTimeline(task);
|
|
832
|
+
renderTaskDetailOutput(task);
|
|
833
|
+
syncTaskDetailTab();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function renderTaskDetailOverview(task) {
|
|
837
|
+
const container = $("#task-detail-overview");
|
|
838
|
+
if (!container) return;
|
|
839
|
+
|
|
840
|
+
if (!task) {
|
|
841
|
+
container.innerHTML = '<div class="task-detail-empty">Select a task to inspect its execution details.</div>';
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const execution = task.execution || {};
|
|
846
|
+
const stats = [
|
|
847
|
+
{ label: "Status", value: humanizeStatus(task.status) },
|
|
848
|
+
{ label: "Priority", value: task.priority || "medium" },
|
|
849
|
+
{ label: "Assigned Worker", value: task.assignedWorkerId || "—" },
|
|
850
|
+
{ label: "Assigned Role", value: task.assignedRole || "—" },
|
|
851
|
+
{ label: "Created", value: formatTime(task.createdAt) || "—" },
|
|
852
|
+
{ label: "Updated", value: formatTime(task.updatedAt) || "—" },
|
|
853
|
+
{ label: "Started", value: formatTime(task.startedAt || execution.startedAt) || "—" },
|
|
854
|
+
{ label: "Completed", value: formatTime(task.completedAt || execution.endedAt) || "—" },
|
|
855
|
+
{ label: "Run ID", value: execution.runId || "—" },
|
|
856
|
+
{ label: "Execution Status", value: execution.status ? humanizeStatus(execution.status) : "—" },
|
|
857
|
+
{ label: "Events", value: String(execution.eventCount || (execution.events ? execution.events.length : 0) || 0) },
|
|
858
|
+
{ label: "Created By", value: task.createdBy || "—" },
|
|
859
|
+
];
|
|
860
|
+
|
|
861
|
+
container.innerHTML =
|
|
862
|
+
'<div class="task-detail-grid">' +
|
|
863
|
+
stats.map(function (item) {
|
|
864
|
+
return (
|
|
865
|
+
'<div class="task-detail-stat">' +
|
|
866
|
+
' <div class="task-detail-stat-label">' + escapeHtml(item.label) + "</div>" +
|
|
867
|
+
' <div class="task-detail-stat-value">' + escapeHtml(item.value) + "</div>" +
|
|
868
|
+
"</div>"
|
|
869
|
+
);
|
|
870
|
+
}).join("") +
|
|
871
|
+
"</div>" +
|
|
872
|
+
'<div class="task-detail-section">' +
|
|
873
|
+
" <h3>Description</h3>" +
|
|
874
|
+
renderMarkdownCard(task.description || "No description") +
|
|
875
|
+
"</div>" +
|
|
876
|
+
(task.progress
|
|
877
|
+
? '<div class="task-detail-section"><h3>Latest Progress</h3>' + renderMarkdownCard(task.progress) + "</div>"
|
|
878
|
+
: "") +
|
|
879
|
+
(task.result
|
|
880
|
+
? '<div class="task-detail-section"><h3>Result</h3>' + renderMarkdownCard(task.result) + "</div>"
|
|
881
|
+
: "") +
|
|
882
|
+
(task.error
|
|
883
|
+
? '<div class="task-detail-section"><h3>Error</h3>' + renderMarkdownCard(task.error) + "</div>"
|
|
884
|
+
: "");
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function buildTimelineEntries(task) {
|
|
888
|
+
if (!task || !selectedTaskDetail) {
|
|
889
|
+
return [];
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const executionEvents = (getSelectedTaskExecution().events || []).map(function (event) {
|
|
893
|
+
return {
|
|
894
|
+
kind: "execution",
|
|
895
|
+
createdAt: event.createdAt || 0,
|
|
896
|
+
label: humanizeStatus(event.phase || event.type),
|
|
897
|
+
meta: [event.source || "execution", event.workerId || event.role || event.stream].filter(Boolean).join(" • "),
|
|
898
|
+
body: event.message || "",
|
|
899
|
+
};
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
const messages = (selectedTaskDetail.messages || []).map(function (message) {
|
|
903
|
+
return {
|
|
904
|
+
kind: "message",
|
|
905
|
+
createdAt: message.createdAt || 0,
|
|
906
|
+
label: humanizeStatus(message.type || "message"),
|
|
907
|
+
meta: [message.fromRole || message.from || "unknown", message.toRole ? ("to " + message.toRole) : null].filter(Boolean).join(" • "),
|
|
908
|
+
body: message.content || "",
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
return executionEvents.concat(messages).sort(function (left, right) {
|
|
913
|
+
return (left.createdAt || 0) - (right.createdAt || 0);
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function renderTaskDetailTimeline(task) {
|
|
918
|
+
const container = $("#task-detail-timeline");
|
|
919
|
+
if (!container) return;
|
|
920
|
+
|
|
921
|
+
if (!task) {
|
|
922
|
+
container.innerHTML = '<div class="task-detail-empty">No task selected.</div>';
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const entries = buildTimelineEntries(task);
|
|
927
|
+
if (entries.length === 0) {
|
|
928
|
+
container.innerHTML = '<div class="task-detail-empty">No execution history recorded yet.</div>';
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
container.innerHTML = '<div class="task-detail-timeline">' +
|
|
933
|
+
entries.map(function (entry) {
|
|
934
|
+
return (
|
|
935
|
+
'<article class="timeline-entry ' + escapeHtml(entry.kind) + '">' +
|
|
936
|
+
' <div class="timeline-entry-header">' +
|
|
937
|
+
' <div class="timeline-entry-label">' + escapeHtml(entry.label) + "</div>" +
|
|
938
|
+
' <div class="timeline-entry-meta">' + escapeHtml(formatTime(entry.createdAt)) + "</div>" +
|
|
939
|
+
" </div>" +
|
|
940
|
+
(entry.meta ? '<div class="timeline-entry-meta">' + escapeHtml(entry.meta) + "</div>" : "") +
|
|
941
|
+
' <div class="timeline-entry-body markdown-body">' + renderMarkdownContent(entry.body) + "</div>" +
|
|
942
|
+
"</article>"
|
|
943
|
+
);
|
|
944
|
+
}).join("") +
|
|
945
|
+
"</div>";
|
|
946
|
+
|
|
947
|
+
if (followTaskOutput) {
|
|
948
|
+
container.scrollTop = container.scrollHeight;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function renderTaskDetailOutput(task) {
|
|
953
|
+
const container = $("#task-detail-output");
|
|
954
|
+
if (!container) return;
|
|
955
|
+
|
|
956
|
+
if (!task) {
|
|
957
|
+
container.innerHTML = '<div class="task-detail-empty">No task selected.</div>';
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const outputEvents = (getSelectedTaskExecution().events || []).filter(function (event) {
|
|
962
|
+
return ["output", "progress", "error"].indexOf(event.type) !== -1;
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
if (outputEvents.length === 0) {
|
|
966
|
+
container.innerHTML = '<div class="task-detail-empty">No live output captured yet.</div>';
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
container.innerHTML = '<div class="task-detail-output-stream">' +
|
|
971
|
+
outputEvents.map(function (event) {
|
|
972
|
+
const label = event.stream || humanizeStatus(event.type || "output");
|
|
973
|
+
const meta = [formatTime(event.createdAt), event.source || null, event.workerId || event.role || null]
|
|
974
|
+
.filter(Boolean)
|
|
975
|
+
.join(" • ");
|
|
976
|
+
const stateClass = event.type === "error" ? " is-error" : "";
|
|
977
|
+
return (
|
|
978
|
+
'<article class="task-output-entry' + stateClass + '">' +
|
|
979
|
+
' <div class="task-output-header">' +
|
|
980
|
+
' <div class="task-output-label">' + escapeHtml(label) + "</div>" +
|
|
981
|
+
(meta ? '<div class="task-output-meta">' + escapeHtml(meta) + "</div>" : "") +
|
|
982
|
+
" </div>" +
|
|
983
|
+
' <div class="task-output-body markdown-body">' + renderMarkdownContent(event.message) + "</div>" +
|
|
984
|
+
"</article>"
|
|
985
|
+
);
|
|
986
|
+
}).join("") +
|
|
987
|
+
"</div>";
|
|
988
|
+
if (followTaskOutput) {
|
|
989
|
+
container.scrollTop = container.scrollHeight;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function syncTaskDetailTab() {
|
|
994
|
+
$$(".task-detail-tab").forEach(function (tab) {
|
|
995
|
+
tab.classList.toggle("active", tab.dataset.taskDetailTab === selectedTaskDetailTab);
|
|
996
|
+
});
|
|
997
|
+
["overview", "timeline", "output"].forEach(function (name) {
|
|
998
|
+
const panel = $("#task-detail-" + name);
|
|
999
|
+
if (panel) {
|
|
1000
|
+
panel.classList.toggle("active", name === selectedTaskDetailTab);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function handleTaskExecutionEvent(payload) {
|
|
1006
|
+
if (!payload || !selectedTaskId || payload.taskId !== selectedTaskId) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (!selectedTaskDetail) {
|
|
1011
|
+
selectedTaskDetail = {
|
|
1012
|
+
task: getTaskById(selectedTaskId),
|
|
1013
|
+
messages: [],
|
|
1014
|
+
clarifications: [],
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const task = selectedTaskDetail.task || getTaskById(selectedTaskId) || { id: selectedTaskId };
|
|
1019
|
+
const execution = Object.assign({ events: [] }, task.execution || {}, payload.execution || {});
|
|
1020
|
+
const events = Array.isArray(execution.events) ? execution.events.slice() : [];
|
|
1021
|
+
if (payload.event) {
|
|
1022
|
+
events.push(payload.event);
|
|
1023
|
+
}
|
|
1024
|
+
execution.events = events;
|
|
1025
|
+
task.execution = execution;
|
|
1026
|
+
selectedTaskDetail.task = Object.assign({}, task);
|
|
1027
|
+
|
|
1028
|
+
renderTaskDetail();
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function renderClarifications(clarifications) {
|
|
1032
|
+
const container = $("#clarifications-list");
|
|
1033
|
+
if (!container) return;
|
|
1034
|
+
|
|
1035
|
+
if (clarifications.length === 0) {
|
|
1036
|
+
container.innerHTML = '<div class="empty-state">No clarification requests</div>';
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
container.innerHTML = clarifications.map(function (item) {
|
|
1041
|
+
const status = item.status || "pending";
|
|
1042
|
+
const context = item.context
|
|
1043
|
+
? '<div class="clarification-context"><strong>Context:</strong> ' + escapeHtml(item.context) + "</div>"
|
|
1044
|
+
: "";
|
|
1045
|
+
const answerBlock = status === "pending"
|
|
1046
|
+
? (
|
|
1047
|
+
'<form class="clarification-answer-form" data-clarification-id="' + escapeHtml(item.id) + '">' +
|
|
1048
|
+
' <label class="clarification-label" for="answer-' + escapeHtml(item.id) + '">Answer as human</label>' +
|
|
1049
|
+
' <textarea id="answer-' + escapeHtml(item.id) + '" name="answer" rows="3" placeholder="Type the exact clarification answer..." required></textarea>' +
|
|
1050
|
+
' <div class="clarification-actions">' +
|
|
1051
|
+
' <button type="submit" class="btn btn-primary">Submit Answer</button>' +
|
|
1052
|
+
" </div>" +
|
|
1053
|
+
"</form>"
|
|
1054
|
+
)
|
|
1055
|
+
: (
|
|
1056
|
+
'<div class="clarification-answer">' +
|
|
1057
|
+
' <strong>Answer:</strong> ' + escapeHtml(item.answer || "") +
|
|
1058
|
+
(item.answeredBy ? ' <span class="clarification-answer-meta">(by ' + escapeHtml(item.answeredBy) + ')</span>' : "") +
|
|
1059
|
+
"</div>"
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
return (
|
|
1063
|
+
'<div class="clarification-card">' +
|
|
1064
|
+
' <div class="clarification-header">' +
|
|
1065
|
+
' <span class="clarification-status ' + escapeHtml(status) + '">' + escapeHtml(humanizeStatus(status)) + "</span>" +
|
|
1066
|
+
' <span class="clarification-time">' + escapeHtml(formatTime(item.updatedAt || item.createdAt)) + "</span>" +
|
|
1067
|
+
" </div>" +
|
|
1068
|
+
' <div class="clarification-question">' + escapeHtml(item.question) + "</div>" +
|
|
1069
|
+
' <div class="clarification-meta">' +
|
|
1070
|
+
' <span><strong>Task:</strong> ' + escapeHtml(item.taskId) + "</span>" +
|
|
1071
|
+
' <span><strong>Role:</strong> ' + escapeHtml(item.requestedByRole || "unknown") + "</span>" +
|
|
1072
|
+
' <span><strong>Requester:</strong> ' + escapeHtml(item.requestedByWorkerId || item.requestedBy || "unknown") + "</span>" +
|
|
1073
|
+
" </div>" +
|
|
1074
|
+
' <div class="clarification-reason"><strong>Blocked because:</strong> ' + escapeHtml(item.blockingReason) + "</div>" +
|
|
1075
|
+
context +
|
|
1076
|
+
answerBlock +
|
|
1077
|
+
"</div>"
|
|
1078
|
+
);
|
|
1079
|
+
}).join("");
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function renderClarificationCount(count) {
|
|
1083
|
+
const badge = $("#clarifications-tab-count");
|
|
1084
|
+
if (!badge) return;
|
|
1085
|
+
|
|
1086
|
+
badge.textContent = String(count);
|
|
1087
|
+
badge.classList.toggle("has-items", count > 0);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function renderMessages(messages) {
|
|
1091
|
+
const container = $("#messages-feed");
|
|
1092
|
+
if (!container) return;
|
|
1093
|
+
|
|
1094
|
+
const recent = (messages || [])
|
|
1095
|
+
.concat(controllerConversation || [])
|
|
1096
|
+
.sort(function (left, right) {
|
|
1097
|
+
return (right.createdAt || 0) - (left.createdAt || 0);
|
|
1098
|
+
})
|
|
1099
|
+
.slice(0, 50);
|
|
1100
|
+
if (recent.length === 0) {
|
|
1101
|
+
container.innerHTML = '<div class="empty-state">No messages yet</div>';
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
container.innerHTML = recent.map(function (message) {
|
|
1106
|
+
const from = message.fromRole || message.from || "unknown";
|
|
1107
|
+
const type = message.type || "direct";
|
|
1108
|
+
|
|
1109
|
+
return (
|
|
1110
|
+
'<div class="message-card">' +
|
|
1111
|
+
' <div class="message-header">' +
|
|
1112
|
+
' <span class="message-from">' + escapeHtml(from) + "</span>" +
|
|
1113
|
+
' <span class="message-type ' + escapeHtml(type) + '">' + escapeHtml(humanizeStatus(type)) + "</span>" +
|
|
1114
|
+
" </div>" +
|
|
1115
|
+
' <div class="message-content markdown-body">' + renderMarkdownContent(message.content) + "</div>" +
|
|
1116
|
+
"</div>"
|
|
1117
|
+
);
|
|
1118
|
+
}).join("");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function renderRoles(roles) {
|
|
1122
|
+
const container = $("#roles-list");
|
|
1123
|
+
if (!container) return;
|
|
1124
|
+
|
|
1125
|
+
container.innerHTML = roles.map(function (role) {
|
|
1126
|
+
return (
|
|
1127
|
+
'<div class="role-chip">' +
|
|
1128
|
+
" <span>" + escapeHtml(role.icon || "") + "</span>" +
|
|
1129
|
+
" <span>" + escapeHtml(role.label) + "</span>" +
|
|
1130
|
+
"</div>"
|
|
1131
|
+
);
|
|
1132
|
+
}).join("");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
$$(".tab").forEach(function (tab) {
|
|
1136
|
+
tab.addEventListener("click", function () {
|
|
1137
|
+
$$(".tab").forEach(function (item) { item.classList.remove("active"); });
|
|
1138
|
+
$$(".tab-panel").forEach(function (panel) { panel.classList.remove("active"); });
|
|
1139
|
+
tab.classList.add("active");
|
|
1140
|
+
activeTab = tab.dataset.tab || "tasks";
|
|
1141
|
+
const panel = $("#tab-" + activeTab);
|
|
1142
|
+
if (panel) {
|
|
1143
|
+
panel.classList.add("active");
|
|
1144
|
+
}
|
|
1145
|
+
if (activeTab === "workspace") {
|
|
1146
|
+
refreshWorkspaceTree(false);
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
$$(".filter-btn").forEach(function (btn) {
|
|
1152
|
+
btn.addEventListener("click", function () {
|
|
1153
|
+
$$(".filter-btn").forEach(function (item) { item.classList.remove("active"); });
|
|
1154
|
+
btn.classList.add("active");
|
|
1155
|
+
currentFilter = btn.dataset.filter;
|
|
1156
|
+
renderTasks(teamState.tasks || []);
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
const workspaceTreeRefresh = $("#workspace-tree-refresh");
|
|
1161
|
+
if (workspaceTreeRefresh) {
|
|
1162
|
+
workspaceTreeRefresh.addEventListener("click", function () {
|
|
1163
|
+
refreshWorkspaceTree(false);
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const workspaceTreeContainer = $("#workspace-tree");
|
|
1168
|
+
if (workspaceTreeContainer) {
|
|
1169
|
+
workspaceTreeContainer.addEventListener("click", function (event) {
|
|
1170
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
1171
|
+
const button = target ? target.closest("[data-workspace-path]") : null;
|
|
1172
|
+
const relativePath = button && button.dataset ? button.dataset.workspacePath : "";
|
|
1173
|
+
if (!relativePath) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
loadWorkspaceFile(relativePath, { keepView: true, silent: false });
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
$$(".workspace-view-tab").forEach(function (tab) {
|
|
1181
|
+
tab.addEventListener("click", function () {
|
|
1182
|
+
const nextView = tab.dataset.workspaceView || "source";
|
|
1183
|
+
if (nextView === "preview" && !isWorkspacePreviewAvailable(selectedWorkspaceFile)) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
selectedWorkspaceView = nextView;
|
|
1187
|
+
renderWorkspaceFile();
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
const tasksBoard = $("#tasks-board");
|
|
1192
|
+
if (tasksBoard) {
|
|
1193
|
+
tasksBoard.addEventListener("click", function (event) {
|
|
1194
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
1195
|
+
const card = target ? target.closest(".task-card") : null;
|
|
1196
|
+
if (card && card.dataset.taskId) {
|
|
1197
|
+
openTaskDetail(card.dataset.taskId);
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
tasksBoard.addEventListener("keydown", function (event) {
|
|
1202
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
1203
|
+
const card = target ? target.closest(".task-card") : null;
|
|
1204
|
+
if (!card || !card.dataset.taskId) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1208
|
+
event.preventDefault();
|
|
1209
|
+
openTaskDetail(card.dataset.taskId);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const taskDetailClose = $("#task-detail-close");
|
|
1215
|
+
if (taskDetailClose) {
|
|
1216
|
+
taskDetailClose.addEventListener("click", closeTaskDetail);
|
|
1217
|
+
}
|
|
1218
|
+
$$("[data-task-detail-close]").forEach(function (node) {
|
|
1219
|
+
node.addEventListener("click", closeTaskDetail);
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
const taskDetailRefresh = $("#task-detail-refresh");
|
|
1223
|
+
if (taskDetailRefresh) {
|
|
1224
|
+
taskDetailRefresh.addEventListener("click", function () {
|
|
1225
|
+
refreshTaskDetail(false);
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const followToggle = $("#task-detail-follow-toggle");
|
|
1230
|
+
if (followToggle) {
|
|
1231
|
+
followToggle.checked = followTaskOutput;
|
|
1232
|
+
followToggle.addEventListener("change", function () {
|
|
1233
|
+
followTaskOutput = !!followToggle.checked;
|
|
1234
|
+
renderTaskDetail();
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
$$(".task-detail-tab").forEach(function (tab) {
|
|
1239
|
+
tab.addEventListener("click", function () {
|
|
1240
|
+
selectedTaskDetailTab = tab.dataset.taskDetailTab || "overview";
|
|
1241
|
+
syncTaskDetailTab();
|
|
1242
|
+
renderTaskDetail();
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
document.addEventListener("keydown", function (event) {
|
|
1247
|
+
if (event.key === "Escape") {
|
|
1248
|
+
closeTaskDetail();
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
const taskForm = $("#create-task-form");
|
|
1253
|
+
if (taskForm) {
|
|
1254
|
+
taskForm.addEventListener("submit", async function (event) {
|
|
1255
|
+
event.preventDefault();
|
|
1256
|
+
const title = $("#task-title").value.trim();
|
|
1257
|
+
const desc = $("#task-desc").value.trim();
|
|
1258
|
+
const priority = $("#task-priority").value;
|
|
1259
|
+
const role = $("#task-role").value;
|
|
1260
|
+
|
|
1261
|
+
if (!title || !desc) return;
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
const body = { title: title, description: desc, priority: priority, createdBy: "boss" };
|
|
1265
|
+
if (role) {
|
|
1266
|
+
body.assignedRole = role;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
await apiPost("/tasks", body);
|
|
1270
|
+
taskForm.reset();
|
|
1271
|
+
refreshAll();
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
console.error("Failed to create task:", err);
|
|
1274
|
+
showError(err instanceof Error ? err.message : "Failed to create task");
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
document.addEventListener("submit", async function (event) {
|
|
1280
|
+
const form = event.target;
|
|
1281
|
+
if (!(form instanceof HTMLFormElement) || !form.matches(".clarification-answer-form")) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
event.preventDefault();
|
|
1286
|
+
const clarificationId = form.dataset.clarificationId;
|
|
1287
|
+
const answerInput = form.querySelector('textarea[name="answer"]');
|
|
1288
|
+
const submitButton = form.querySelector('button[type="submit"]');
|
|
1289
|
+
const answer = answerInput ? answerInput.value.trim() : "";
|
|
1290
|
+
|
|
1291
|
+
if (!clarificationId || !answer) {
|
|
1292
|
+
showError("Please provide an answer before submitting.");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (submitButton) {
|
|
1297
|
+
submitButton.disabled = true;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
try {
|
|
1301
|
+
await apiPost("/clarifications/" + clarificationId + "/answer", {
|
|
1302
|
+
answer: answer,
|
|
1303
|
+
answeredBy: "simulated-human",
|
|
1304
|
+
});
|
|
1305
|
+
refreshAll();
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
console.error("Failed to answer clarification:", err);
|
|
1308
|
+
showError(err instanceof Error ? err.message : "Failed to answer clarification");
|
|
1309
|
+
} finally {
|
|
1310
|
+
if (submitButton) {
|
|
1311
|
+
submitButton.disabled = false;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
const cmdInput = $("#command-input");
|
|
1317
|
+
const cmdSend = $("#command-send");
|
|
1318
|
+
|
|
1319
|
+
function handleCommand() {
|
|
1320
|
+
const cmd = (cmdInput && cmdInput.value ? cmdInput.value : "").trim();
|
|
1321
|
+
if (!cmd || !cmdInput || controllerCommandPending) return;
|
|
1322
|
+
cmdInput.value = "";
|
|
1323
|
+
|
|
1324
|
+
if (cmd === "/status" || cmd === "/s") {
|
|
1325
|
+
refreshAll();
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (cmd.startsWith("/assign ")) {
|
|
1330
|
+
const parts = cmd.split(" ");
|
|
1331
|
+
const taskId = parts[1];
|
|
1332
|
+
const role = parts[2];
|
|
1333
|
+
if (taskId && role) {
|
|
1334
|
+
apiPost("/tasks/" + taskId + "/assign", { targetRole: role })
|
|
1335
|
+
.then(function () { refreshAll(); })
|
|
1336
|
+
.catch(function (err) {
|
|
1337
|
+
console.error(err);
|
|
1338
|
+
showError(err instanceof Error ? err.message : "Failed to assign task");
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
controllerCommandPending = true;
|
|
1345
|
+
if (cmdSend) {
|
|
1346
|
+
cmdSend.disabled = true;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
appendControllerConversation({
|
|
1350
|
+
from: "human",
|
|
1351
|
+
fromRole: "human",
|
|
1352
|
+
type: "controller-input",
|
|
1353
|
+
content: cmd,
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
apiPost("/controller/intake", {
|
|
1357
|
+
message: cmd,
|
|
1358
|
+
sessionKey: getControllerSessionKey(),
|
|
1359
|
+
}).then(function (data) {
|
|
1360
|
+
appendControllerConversation({
|
|
1361
|
+
from: "controller",
|
|
1362
|
+
fromRole: "controller",
|
|
1363
|
+
type: "controller-reply",
|
|
1364
|
+
content: data && data.reply ? data.reply : "Controller finished without a textual reply.",
|
|
1365
|
+
});
|
|
1366
|
+
refreshAll();
|
|
1367
|
+
}).catch(function (err) {
|
|
1368
|
+
console.error(err);
|
|
1369
|
+
appendControllerConversation({
|
|
1370
|
+
from: "controller",
|
|
1371
|
+
fromRole: "controller",
|
|
1372
|
+
type: "controller-error",
|
|
1373
|
+
content: err instanceof Error ? err.message : "Failed to send message to controller",
|
|
1374
|
+
});
|
|
1375
|
+
showError(err instanceof Error ? err.message : "Failed to send message to controller");
|
|
1376
|
+
}).finally(function () {
|
|
1377
|
+
controllerCommandPending = false;
|
|
1378
|
+
if (cmdSend) {
|
|
1379
|
+
cmdSend.disabled = false;
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (cmdSend) {
|
|
1385
|
+
cmdSend.addEventListener("click", handleCommand);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (cmdInput) {
|
|
1389
|
+
cmdInput.addEventListener("keydown", function (event) {
|
|
1390
|
+
if (event.key === "Enter") {
|
|
1391
|
+
handleCommand();
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
renderWorkspaceTree(workspaceTree);
|
|
1397
|
+
renderWorkspaceFile();
|
|
1398
|
+
refreshAll();
|
|
1399
|
+
connectWebSocket();
|
|
1400
|
+
})();
|