@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
package/ui/app.legacy.js
ADDED
|
@@ -0,0 +1,1464 @@
|
|
|
1
|
+
const state = {
|
|
2
|
+
tab: "overview",
|
|
3
|
+
status: null,
|
|
4
|
+
executor: null,
|
|
5
|
+
tasks: [],
|
|
6
|
+
tasksTotal: 0,
|
|
7
|
+
tasksPage: 0,
|
|
8
|
+
tasksPageSize: 8,
|
|
9
|
+
tasksStatus: "todo",
|
|
10
|
+
tasksProject: "",
|
|
11
|
+
tasksQuery: "",
|
|
12
|
+
projects: [],
|
|
13
|
+
logs: null,
|
|
14
|
+
logsLines: 200,
|
|
15
|
+
threads: [],
|
|
16
|
+
worktrees: [],
|
|
17
|
+
worktreeStats: null,
|
|
18
|
+
presence: null,
|
|
19
|
+
sharedWorkspaces: null,
|
|
20
|
+
sharedAvailability: null,
|
|
21
|
+
gitBranches: [],
|
|
22
|
+
gitDiff: "",
|
|
23
|
+
agentLogFiles: [],
|
|
24
|
+
agentLogFile: "",
|
|
25
|
+
agentLogLines: 200,
|
|
26
|
+
agentLogQuery: "",
|
|
27
|
+
agentLogTail: null,
|
|
28
|
+
agentContext: null,
|
|
29
|
+
manualMode: false,
|
|
30
|
+
connected: false,
|
|
31
|
+
ws: null,
|
|
32
|
+
wsConnected: false,
|
|
33
|
+
wsRetryMs: 1000,
|
|
34
|
+
wsReconnectTimer: null,
|
|
35
|
+
wsRefreshTimer: null,
|
|
36
|
+
pendingMutation: false,
|
|
37
|
+
modal: null,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const view = document.getElementById("view");
|
|
41
|
+
const connectionPill = document.getElementById("connection-pill");
|
|
42
|
+
const tabs = document.querySelectorAll(".tab");
|
|
43
|
+
|
|
44
|
+
function setConnection(status, detail = "") {
|
|
45
|
+
state.connected = status;
|
|
46
|
+
connectionPill.textContent = status
|
|
47
|
+
? `Connected ${detail}`.trim()
|
|
48
|
+
: `Offline ${detail}`.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cloneStateValue(value) {
|
|
52
|
+
if (typeof structuredClone === "function") {
|
|
53
|
+
return structuredClone(value);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function scheduleRefresh(delayMs = 100) {
|
|
59
|
+
if (state.wsRefreshTimer) {
|
|
60
|
+
clearTimeout(state.wsRefreshTimer);
|
|
61
|
+
}
|
|
62
|
+
state.wsRefreshTimer = setTimeout(async () => {
|
|
63
|
+
state.wsRefreshTimer = null;
|
|
64
|
+
if (state.pendingMutation) return;
|
|
65
|
+
try {
|
|
66
|
+
await refreshTab();
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore transient refresh failures
|
|
69
|
+
}
|
|
70
|
+
}, delayMs);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function channelsForTab(tab) {
|
|
74
|
+
if (tab === "overview") return ["overview", "executor", "tasks", "agents"];
|
|
75
|
+
if (tab === "tasks") return ["tasks"];
|
|
76
|
+
if (tab === "agents") return ["agents", "executor"];
|
|
77
|
+
if (tab === "worktrees") return ["worktrees"];
|
|
78
|
+
if (tab === "workspaces") return ["workspaces"];
|
|
79
|
+
if (tab === "executor") return ["executor", "overview"];
|
|
80
|
+
return ["*"];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function connectRealtime() {
|
|
84
|
+
const tg = telegram();
|
|
85
|
+
const proto = globalThis.location.protocol === "https:" ? "wss" : "ws";
|
|
86
|
+
const wsUrl = new URL(`${proto}://${globalThis.location.host}/ws`);
|
|
87
|
+
if (tg?.initData) {
|
|
88
|
+
wsUrl.searchParams.set("initData", tg.initData);
|
|
89
|
+
}
|
|
90
|
+
const socket = new WebSocket(wsUrl.toString());
|
|
91
|
+
state.ws = socket;
|
|
92
|
+
|
|
93
|
+
socket.addEventListener("open", () => {
|
|
94
|
+
state.wsConnected = true;
|
|
95
|
+
state.wsRetryMs = 1000;
|
|
96
|
+
setConnection(true, "live");
|
|
97
|
+
socket.send(
|
|
98
|
+
JSON.stringify({ type: "subscribe", channels: channelsForTab(state.tab) }),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
socket.addEventListener("message", (event) => {
|
|
103
|
+
let message = null;
|
|
104
|
+
try {
|
|
105
|
+
message = JSON.parse(event.data || "{}");
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (message?.type !== "invalidate") return;
|
|
110
|
+
const channels = Array.isArray(message.channels) ? message.channels : [];
|
|
111
|
+
const interested = channelsForTab(state.tab);
|
|
112
|
+
const shouldRefresh =
|
|
113
|
+
channels.includes("*") || channels.some((channel) => interested.includes(channel));
|
|
114
|
+
if (shouldRefresh) {
|
|
115
|
+
scheduleRefresh(120);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
socket.addEventListener("close", () => {
|
|
120
|
+
state.wsConnected = false;
|
|
121
|
+
setConnection(false, "reconnecting");
|
|
122
|
+
if (state.wsReconnectTimer) clearTimeout(state.wsReconnectTimer);
|
|
123
|
+
state.wsReconnectTimer = setTimeout(() => {
|
|
124
|
+
connectRealtime();
|
|
125
|
+
}, state.wsRetryMs);
|
|
126
|
+
state.wsRetryMs = Math.min(10000, state.wsRetryMs * 2);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
socket.addEventListener("error", () => {
|
|
130
|
+
setConnection(false, "ws error");
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function runOptimisticMutation(apply, request, rollback) {
|
|
135
|
+
state.pendingMutation = true;
|
|
136
|
+
try {
|
|
137
|
+
apply();
|
|
138
|
+
render();
|
|
139
|
+
const response = await request();
|
|
140
|
+
state.pendingMutation = false;
|
|
141
|
+
return response;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (typeof rollback === "function") rollback();
|
|
144
|
+
state.pendingMutation = false;
|
|
145
|
+
render();
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function telegram() {
|
|
151
|
+
return globalThis.Telegram ? globalThis.Telegram.WebApp : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sendCommandToChat(command) {
|
|
155
|
+
const tg = telegram();
|
|
156
|
+
if (!tg) return;
|
|
157
|
+
tg.sendData(JSON.stringify({ type: "command", command }));
|
|
158
|
+
if (tg.showPopup) {
|
|
159
|
+
tg.showPopup({ title: "Sent", message: command, buttons: [{ type: "ok" }] });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function apiFetch(path, options = {}) {
|
|
164
|
+
const headers = { "Content-Type": "application/json" };
|
|
165
|
+
const tg = telegram();
|
|
166
|
+
if (tg?.initData) {
|
|
167
|
+
headers["X-Telegram-InitData"] = tg.initData;
|
|
168
|
+
}
|
|
169
|
+
const res = await fetch(path, { ...options, headers });
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
const text = await res.text();
|
|
172
|
+
throw new Error(text || `Request failed (${res.status})`);
|
|
173
|
+
}
|
|
174
|
+
return res.json();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function loadOverview() {
|
|
178
|
+
const status = await apiFetch("/api/status").catch(() => ({ data: null }));
|
|
179
|
+
const executor = await apiFetch("/api/executor").catch(() => ({ data: null }));
|
|
180
|
+
state.status = status.data || null;
|
|
181
|
+
state.executor = executor;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function loadProjects() {
|
|
185
|
+
const res = await apiFetch("/api/projects").catch(() => ({ data: [] }));
|
|
186
|
+
state.projects = res.data || [];
|
|
187
|
+
if (!state.tasksProject && state.projects.length) {
|
|
188
|
+
state.tasksProject = state.projects[0].id || "";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function loadTasks() {
|
|
193
|
+
const params = new URLSearchParams({
|
|
194
|
+
status: state.tasksStatus,
|
|
195
|
+
page: String(state.tasksPage),
|
|
196
|
+
pageSize: String(state.tasksPageSize),
|
|
197
|
+
});
|
|
198
|
+
if (state.tasksProject) params.set("project", state.tasksProject);
|
|
199
|
+
const res = await apiFetch(`/api/tasks?${params.toString()}`);
|
|
200
|
+
state.tasks = res.data || [];
|
|
201
|
+
state.tasksTotal = res.total || 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function loadLogs() {
|
|
205
|
+
const res = await apiFetch(`/api/logs?lines=${state.logsLines}`);
|
|
206
|
+
state.logs = res.data || null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function loadThreads() {
|
|
210
|
+
const res = await apiFetch("/api/threads").catch(() => ({ data: [] }));
|
|
211
|
+
state.threads = res.data || [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function loadWorktrees() {
|
|
215
|
+
const res = await apiFetch("/api/worktrees").catch(() => ({ data: [], stats: null }));
|
|
216
|
+
state.worktrees = res.data || [];
|
|
217
|
+
state.worktreeStats = res.stats || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function loadPresence() {
|
|
221
|
+
const res = await apiFetch("/api/presence").catch(() => ({ data: null }));
|
|
222
|
+
state.presence = res.data || null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function loadSharedWorkspaces() {
|
|
226
|
+
const res = await apiFetch("/api/shared-workspaces").catch(() => ({
|
|
227
|
+
data: null,
|
|
228
|
+
availability: null,
|
|
229
|
+
}));
|
|
230
|
+
state.sharedWorkspaces = res.data || null;
|
|
231
|
+
state.sharedAvailability = res.availability || null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function loadGit() {
|
|
235
|
+
const branches = await apiFetch("/api/git/branches").catch(() => ({ data: [] }));
|
|
236
|
+
const diff = await apiFetch("/api/git/diff").catch(() => ({ data: "" }));
|
|
237
|
+
state.gitBranches = branches.data || [];
|
|
238
|
+
state.gitDiff = diff.data || "";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function loadAgentLogFiles() {
|
|
242
|
+
const params = new URLSearchParams();
|
|
243
|
+
if (state.agentLogQuery) params.set("query", state.agentLogQuery);
|
|
244
|
+
const path = params.toString()
|
|
245
|
+
? `/api/agent-logs?${params.toString()}`
|
|
246
|
+
: "/api/agent-logs";
|
|
247
|
+
const res = await apiFetch(path).catch(() => ({
|
|
248
|
+
data: [],
|
|
249
|
+
}));
|
|
250
|
+
state.agentLogFiles = res.data || [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function loadAgentLogTail() {
|
|
254
|
+
if (!state.agentLogFile) {
|
|
255
|
+
state.agentLogTail = null;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const params = new URLSearchParams({
|
|
259
|
+
file: state.agentLogFile,
|
|
260
|
+
lines: String(state.agentLogLines),
|
|
261
|
+
});
|
|
262
|
+
const res = await apiFetch(`/api/agent-logs?${params.toString()}`).catch(() => ({
|
|
263
|
+
data: null,
|
|
264
|
+
}));
|
|
265
|
+
state.agentLogTail = res.data || null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function loadAgentContext(query) {
|
|
269
|
+
if (!query) {
|
|
270
|
+
state.agentContext = null;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const res = await apiFetch(`/api/agent-logs/context?query=${encodeURIComponent(query)}`).catch(
|
|
274
|
+
() => ({ data: null }),
|
|
275
|
+
);
|
|
276
|
+
state.agentContext = res.data || null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function refreshTab() {
|
|
280
|
+
if (state.tab === "overview") {
|
|
281
|
+
await loadOverview();
|
|
282
|
+
}
|
|
283
|
+
if (state.tab === "tasks") {
|
|
284
|
+
await loadProjects();
|
|
285
|
+
await loadTasks();
|
|
286
|
+
}
|
|
287
|
+
if (state.tab === "agents") {
|
|
288
|
+
await loadOverview();
|
|
289
|
+
await loadThreads();
|
|
290
|
+
}
|
|
291
|
+
if (state.tab === "worktrees") {
|
|
292
|
+
await loadWorktrees();
|
|
293
|
+
}
|
|
294
|
+
if (state.tab === "workspaces") {
|
|
295
|
+
await loadSharedWorkspaces();
|
|
296
|
+
}
|
|
297
|
+
if (state.tab === "presence") {
|
|
298
|
+
await loadPresence();
|
|
299
|
+
}
|
|
300
|
+
if (state.tab === "executor") {
|
|
301
|
+
await loadOverview();
|
|
302
|
+
}
|
|
303
|
+
if (state.tab === "logs") {
|
|
304
|
+
await loadLogs();
|
|
305
|
+
}
|
|
306
|
+
if (state.tab === "git") {
|
|
307
|
+
await loadGit();
|
|
308
|
+
}
|
|
309
|
+
if (state.tab === "agentlogs") {
|
|
310
|
+
await loadAgentLogFiles();
|
|
311
|
+
await loadAgentLogTail();
|
|
312
|
+
}
|
|
313
|
+
render();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function renderOverview() {
|
|
317
|
+
const counts = state.status?.counts || {};
|
|
318
|
+
const summary = state.status?.success_metrics || {};
|
|
319
|
+
const executor = state.executor?.data;
|
|
320
|
+
const mode = state.executor?.mode || "vk";
|
|
321
|
+
const totalActive =
|
|
322
|
+
Number(counts.running || 0) +
|
|
323
|
+
Number(counts.review || 0) +
|
|
324
|
+
Number(counts.error || 0);
|
|
325
|
+
const backlog = Number(state.status?.backlog_remaining || 0) || 0;
|
|
326
|
+
const progressPct = backlog + totalActive > 0 ? Math.round((totalActive / (backlog + totalActive)) * 100) : 0;
|
|
327
|
+
return `
|
|
328
|
+
<section class="card">
|
|
329
|
+
<h2>Today at a glance</h2>
|
|
330
|
+
<div class="grid columns-2">
|
|
331
|
+
<div class="stat"><strong>${counts.running ?? 0}</strong>Running</div>
|
|
332
|
+
<div class="stat"><strong>${counts.review ?? 0}</strong>In Review</div>
|
|
333
|
+
<div class="stat"><strong>${counts.error ?? 0}</strong>Blocked</div>
|
|
334
|
+
<div class="stat"><strong>${state.status?.backlog_remaining ?? "?"}</strong>Backlog</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div style="margin-top:14px">
|
|
337
|
+
<div class="meta">Active progress · ${progressPct}% engaged</div>
|
|
338
|
+
<div class="progress" style="margin-top:6px"><span style="width:${progressPct}%"></span></div>
|
|
339
|
+
</div>
|
|
340
|
+
</section>
|
|
341
|
+
<section class="card">
|
|
342
|
+
<h3>Executor</h3>
|
|
343
|
+
<p>Mode: ${mode} · Slots: ${executor?.activeSlots ?? 0}/${executor?.maxParallel ?? "-"}</p>
|
|
344
|
+
<p>Paused: ${state.executor?.paused ? "Yes" : "No"}</p>
|
|
345
|
+
<div class="button-row">
|
|
346
|
+
<button class="action" data-action="executor:pause">Pause</button>
|
|
347
|
+
<button class="action secondary" data-action="executor:resume">Resume</button>
|
|
348
|
+
</div>
|
|
349
|
+
</section>
|
|
350
|
+
<section class="card">
|
|
351
|
+
<h3>Quality</h3>
|
|
352
|
+
<p>First-shot: ${summary.first_shot_rate ?? 0}% · Needed fix: ${summary.needed_fix ?? 0} · Failed: ${summary.failed ?? 0}</p>
|
|
353
|
+
<div class="button-row">
|
|
354
|
+
<button class="action muted" data-action="command:/status">Send /status to chat</button>
|
|
355
|
+
<button class="action muted" data-action="command:/health">Send /health</button>
|
|
356
|
+
</div>
|
|
357
|
+
</section>
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function renderTasks() {
|
|
362
|
+
const canManual = Boolean(state.executor?.data);
|
|
363
|
+
const totalPages = Math.max(1, Math.ceil((state.tasksTotal || 0) / state.tasksPageSize));
|
|
364
|
+
const search = state.tasksQuery.trim().toLowerCase();
|
|
365
|
+
const visibleTasks = search
|
|
366
|
+
? state.tasks.filter((task) => {
|
|
367
|
+
const hay = `${task.title || ""} ${task.description || ""} ${task.id || ""}`.toLowerCase();
|
|
368
|
+
return hay.includes(search);
|
|
369
|
+
})
|
|
370
|
+
: state.tasks;
|
|
371
|
+
const tasksHtml = visibleTasks
|
|
372
|
+
.map(
|
|
373
|
+
(task) => `
|
|
374
|
+
<div class="task-card">
|
|
375
|
+
<header>
|
|
376
|
+
<div>
|
|
377
|
+
<div class="task-title">${task.title || "(untitled)"}</div>
|
|
378
|
+
<div class="badge">${task.id}</div>
|
|
379
|
+
</div>
|
|
380
|
+
<span class="badge">${task.status}</span>
|
|
381
|
+
</header>
|
|
382
|
+
<p>${task.description ? task.description.slice(0, 120) : "No description."}</p>
|
|
383
|
+
<div class="button-row">
|
|
384
|
+
${
|
|
385
|
+
state.manualMode && task.status === "todo" && canManual
|
|
386
|
+
? `<button class="action" data-action="task:start:${task.id}">Start</button>`
|
|
387
|
+
: ""
|
|
388
|
+
}
|
|
389
|
+
<button class="action secondary" data-action="task:update:${task.id}:inreview">Mark Review</button>
|
|
390
|
+
<button class="action muted" data-action="task:update:${task.id}:done">Mark Done</button>
|
|
391
|
+
<button class="action muted" data-action="task:detail:${task.id}">Details</button>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
`,
|
|
395
|
+
)
|
|
396
|
+
.join("");
|
|
397
|
+
|
|
398
|
+
const projectOptions = state.projects
|
|
399
|
+
.map(
|
|
400
|
+
(project) =>
|
|
401
|
+
`<option value="${project.id}" ${project.id === state.tasksProject ? "selected" : ""}>${project.name || project.id}</option>`,
|
|
402
|
+
)
|
|
403
|
+
.join("");
|
|
404
|
+
|
|
405
|
+
return `
|
|
406
|
+
<section class="card">
|
|
407
|
+
<h2>Task Board</h2>
|
|
408
|
+
<div class="chips">
|
|
409
|
+
${["todo", "inprogress", "inreview", "done"].map(
|
|
410
|
+
(status) =>
|
|
411
|
+
`<button class="chip ${state.tasksStatus === status ? "active" : ""}" data-action="tasks:filter:${status}">${status.toUpperCase()}</button>`,
|
|
412
|
+
).join("")}
|
|
413
|
+
</div>
|
|
414
|
+
<div class="input-row" style="margin-top:12px">
|
|
415
|
+
<select data-action="tasks:project">
|
|
416
|
+
${projectOptions}
|
|
417
|
+
</select>
|
|
418
|
+
<label class="switch" data-action="manual:toggle">
|
|
419
|
+
<input type="checkbox" ${state.manualMode ? "checked" : ""} ${canManual ? "" : "disabled"} />
|
|
420
|
+
<span class="switch-track"><span class="switch-thumb"></span></span>
|
|
421
|
+
Manual Mode
|
|
422
|
+
</label>
|
|
423
|
+
</div>
|
|
424
|
+
<div class="input-row" style="margin-top:12px">
|
|
425
|
+
<input type="text" data-action="tasks:search" placeholder="Search tasks..." value="${state.tasksQuery}" />
|
|
426
|
+
<span class="pill">${visibleTasks.length} shown</span>
|
|
427
|
+
</div>
|
|
428
|
+
<div class="list" style="margin-top:16px">
|
|
429
|
+
${tasksHtml || "<p>No tasks found.</p>"}
|
|
430
|
+
</div>
|
|
431
|
+
<div class="pager" style="margin-top:16px">
|
|
432
|
+
<button class="action xs muted" data-action="tasks:prev">Prev</button>
|
|
433
|
+
<span>Page ${state.tasksPage + 1} / ${totalPages}</span>
|
|
434
|
+
<button class="action xs muted" data-action="tasks:next">Next</button>
|
|
435
|
+
</div>
|
|
436
|
+
</section>
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function renderAgents() {
|
|
441
|
+
const executor = state.executor?.data;
|
|
442
|
+
const slots = executor?.slots || [];
|
|
443
|
+
const slotsHtml = slots
|
|
444
|
+
.map(
|
|
445
|
+
(slot) => `
|
|
446
|
+
<div class="task-card">
|
|
447
|
+
<header>
|
|
448
|
+
<div>
|
|
449
|
+
<div class="task-title">${slot.taskTitle}</div>
|
|
450
|
+
<div class="badge">${slot.taskId}</div>
|
|
451
|
+
</div>
|
|
452
|
+
<span class="badge">${slot.status}</span>
|
|
453
|
+
</header>
|
|
454
|
+
<p>Agent ${slot.agentInstanceId || "n/a"} · ${slot.sdk} · Attempt ${slot.attempt}</p>
|
|
455
|
+
<div class="button-row">
|
|
456
|
+
<button class="action muted" data-action="command:/agentlogs ${slot.branch || slot.taskId}">View Logs</button>
|
|
457
|
+
<button class="action secondary" data-action="command:/steer focus on ${slot.taskTitle}">Steer</button>
|
|
458
|
+
<button class="action muted" data-action="agentlogs:search:${(slot.taskId || slot.branch || "").slice(0, 12)}">Log Files</button>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
`,
|
|
462
|
+
)
|
|
463
|
+
.join("");
|
|
464
|
+
|
|
465
|
+
const threadsHtml = state.threads
|
|
466
|
+
.map(
|
|
467
|
+
(thread) => `
|
|
468
|
+
<div class="stat">
|
|
469
|
+
<strong>${thread.taskKey}</strong>
|
|
470
|
+
<div>SDK: ${thread.sdk}</div>
|
|
471
|
+
<div>Turns: ${thread.turnCount}</div>
|
|
472
|
+
</div>
|
|
473
|
+
`,
|
|
474
|
+
)
|
|
475
|
+
.join("");
|
|
476
|
+
|
|
477
|
+
return `
|
|
478
|
+
<section class="card">
|
|
479
|
+
<h2>Active Agents</h2>
|
|
480
|
+
<div class="list">${slotsHtml || "<p>No active agents.</p>"}</div>
|
|
481
|
+
</section>
|
|
482
|
+
<section class="card">
|
|
483
|
+
<h3>Threads</h3>
|
|
484
|
+
<div class="grid columns-2">${threadsHtml || "<p>No threads.</p>"}</div>
|
|
485
|
+
</section>
|
|
486
|
+
`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function renderWorktrees() {
|
|
490
|
+
const stats = state.worktreeStats || {};
|
|
491
|
+
const worktrees = state.worktrees || [];
|
|
492
|
+
const listHtml = worktrees
|
|
493
|
+
.map((wt) => {
|
|
494
|
+
const ageMin = Math.round((wt.age || 0) / 60000);
|
|
495
|
+
const ageStr = ageMin >= 60 ? `${Math.round(ageMin / 60)}h` : `${ageMin}m`;
|
|
496
|
+
const taskKey = wt.taskKey ? ` · ${wt.taskKey}` : "";
|
|
497
|
+
return `
|
|
498
|
+
<div class="task-card">
|
|
499
|
+
<header>
|
|
500
|
+
<div>
|
|
501
|
+
<div class="task-title">${wt.branch || "(detached)"}</div>
|
|
502
|
+
<div class="meta">${wt.path}</div>
|
|
503
|
+
</div>
|
|
504
|
+
<span class="badge">${wt.status || "active"}</span>
|
|
505
|
+
</header>
|
|
506
|
+
<p>Age ${ageStr}${taskKey} ${wt.owner ? `· Owner ${wt.owner}` : ""}</p>
|
|
507
|
+
<div class="button-row">
|
|
508
|
+
${wt.taskKey ? `<button class="action muted" data-action="worktrees:release:${wt.taskKey}">Release</button>` : ""}
|
|
509
|
+
${wt.branch ? `<button class="action muted" data-action="worktrees:release-branch:${wt.branch}">Release Branch</button>` : ""}
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
`;
|
|
513
|
+
})
|
|
514
|
+
.join("");
|
|
515
|
+
|
|
516
|
+
return `
|
|
517
|
+
<section class="card">
|
|
518
|
+
<h2>Worktrees</h2>
|
|
519
|
+
<div class="data-grid">
|
|
520
|
+
<div class="stat"><strong>${stats.total ?? worktrees.length}</strong>Total</div>
|
|
521
|
+
<div class="stat"><strong>${stats.active ?? 0}</strong>Active</div>
|
|
522
|
+
<div class="stat"><strong>${stats.stale ?? 0}</strong>Stale</div>
|
|
523
|
+
</div>
|
|
524
|
+
<div class="input-row" style="margin-top:12px">
|
|
525
|
+
<input id="worktree-release-input" type="text" placeholder="Task key or branch" />
|
|
526
|
+
<button class="action muted" data-action="worktrees:release-input">Release</button>
|
|
527
|
+
<button class="action secondary" data-action="worktrees:prune">Prune stale</button>
|
|
528
|
+
</div>
|
|
529
|
+
</section>
|
|
530
|
+
<section class="card">
|
|
531
|
+
<h3>Active Worktrees</h3>
|
|
532
|
+
<div class="list">${listHtml || "<p>No worktrees tracked.</p>"}</div>
|
|
533
|
+
</section>
|
|
534
|
+
`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function renderWorkspaces() {
|
|
538
|
+
const registry = state.sharedWorkspaces;
|
|
539
|
+
const workspaces = registry?.workspaces || [];
|
|
540
|
+
const availability = state.sharedAvailability || {};
|
|
541
|
+
const availabilityHtml = Object.entries(availability)
|
|
542
|
+
.map(
|
|
543
|
+
([key, value]) =>
|
|
544
|
+
`<span class="pill">${key}: ${value}</span>`,
|
|
545
|
+
)
|
|
546
|
+
.join("");
|
|
547
|
+
const workspaceHtml = workspaces
|
|
548
|
+
.map((ws) => {
|
|
549
|
+
const lease = ws.lease;
|
|
550
|
+
const leaseInfo = lease
|
|
551
|
+
? `Leased to ${lease.owner} until ${new Date(lease.lease_expires_at).toLocaleString()}`
|
|
552
|
+
: "Available";
|
|
553
|
+
return `
|
|
554
|
+
<div class="task-card">
|
|
555
|
+
<header>
|
|
556
|
+
<div>
|
|
557
|
+
<div class="task-title">${ws.name || ws.id}</div>
|
|
558
|
+
<div class="meta">${ws.provider || "provider"} · ${ws.region || "region?"}</div>
|
|
559
|
+
</div>
|
|
560
|
+
<span class="badge">${ws.availability}</span>
|
|
561
|
+
</header>
|
|
562
|
+
<p>${leaseInfo}</p>
|
|
563
|
+
<div class="button-row">
|
|
564
|
+
<button class="action" data-action="shared:claim:${ws.id}">Claim</button>
|
|
565
|
+
<button class="action secondary" data-action="shared:renew:${ws.id}">Renew</button>
|
|
566
|
+
<button class="action muted" data-action="shared:release:${ws.id}">Release</button>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
`;
|
|
570
|
+
})
|
|
571
|
+
.join("");
|
|
572
|
+
|
|
573
|
+
return `
|
|
574
|
+
<section class="card">
|
|
575
|
+
<h2>Shared Workspaces</h2>
|
|
576
|
+
<div class="chips">${availabilityHtml || "<span class=\"pill\">No registry</span>"}</div>
|
|
577
|
+
<div class="input-row" style="margin-top:12px">
|
|
578
|
+
<input id="shared-owner" type="text" placeholder="Owner (e.g. you@team)" />
|
|
579
|
+
<input id="shared-ttl" type="number" min="30" step="15" placeholder="TTL (min)" />
|
|
580
|
+
<input id="shared-note" type="text" placeholder="Note (optional)" />
|
|
581
|
+
</div>
|
|
582
|
+
</section>
|
|
583
|
+
<section class="card">
|
|
584
|
+
<h3>Workspace Pool</h3>
|
|
585
|
+
<div class="list">${workspaceHtml || "<p>No shared workspaces configured.</p>"}</div>
|
|
586
|
+
</section>
|
|
587
|
+
`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function renderPresence() {
|
|
591
|
+
const instances = state.presence?.instances || [];
|
|
592
|
+
const coordinator = state.presence?.coordinator || null;
|
|
593
|
+
const instanceHtml = instances
|
|
594
|
+
.map((inst) => {
|
|
595
|
+
const lastSeen = inst.last_seen_at
|
|
596
|
+
? new Date(inst.last_seen_at).toLocaleString()
|
|
597
|
+
: "unknown";
|
|
598
|
+
return `
|
|
599
|
+
<div class="stat">
|
|
600
|
+
<strong>${inst.instance_label || inst.instance_id}</strong>
|
|
601
|
+
<div>${inst.workspace_role || "workspace"} · ${inst.host || "host"}</div>
|
|
602
|
+
<div class="meta">Last seen ${lastSeen}</div>
|
|
603
|
+
</div>
|
|
604
|
+
`;
|
|
605
|
+
})
|
|
606
|
+
.join("");
|
|
607
|
+
|
|
608
|
+
return `
|
|
609
|
+
<section class="card">
|
|
610
|
+
<h2>Presence</h2>
|
|
611
|
+
<p>Active openfleet instances discovered via presence beacons.</p>
|
|
612
|
+
<div class="stat">
|
|
613
|
+
<strong>${coordinator?.instance_label || coordinator?.instance_id || "none"}</strong>
|
|
614
|
+
<div>Coordinator</div>
|
|
615
|
+
<div class="meta">Priority ${coordinator?.coordinator_priority ?? "-"}</div>
|
|
616
|
+
</div>
|
|
617
|
+
</section>
|
|
618
|
+
<section class="card">
|
|
619
|
+
<h3>Instances</h3>
|
|
620
|
+
<div class="data-grid">${instanceHtml || "<p>No active instances.</p>"}</div>
|
|
621
|
+
</section>
|
|
622
|
+
`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function renderGit() {
|
|
626
|
+
const branchesHtml = state.gitBranches
|
|
627
|
+
.map((line) => `<div class="meta">${line}</div>`)
|
|
628
|
+
.join("");
|
|
629
|
+
return `
|
|
630
|
+
<section class="card">
|
|
631
|
+
<h2>Git Snapshot</h2>
|
|
632
|
+
<div class="button-row">
|
|
633
|
+
<button class="action muted" data-action="git:refresh">Refresh</button>
|
|
634
|
+
<button class="action muted" data-action="command:/diff">Send /diff</button>
|
|
635
|
+
</div>
|
|
636
|
+
<div style="margin-top:12px">
|
|
637
|
+
<h3>Working Tree Diff</h3>
|
|
638
|
+
<div class="log-box">${state.gitDiff || "Clean working tree."}</div>
|
|
639
|
+
</div>
|
|
640
|
+
</section>
|
|
641
|
+
<section class="card">
|
|
642
|
+
<h3>Recent Branches</h3>
|
|
643
|
+
<div class="list-compact">${branchesHtml || "<p>No branches found.</p>"}</div>
|
|
644
|
+
</section>
|
|
645
|
+
<section class="card">
|
|
646
|
+
<h3>Run Git Command</h3>
|
|
647
|
+
<div class="input-row">
|
|
648
|
+
<input id="git-command" type="text" placeholder="log --oneline -5" />
|
|
649
|
+
<button class="action" data-action="command:git">Send</button>
|
|
650
|
+
</div>
|
|
651
|
+
</section>
|
|
652
|
+
`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function renderAgentLogs() {
|
|
656
|
+
const logList = state.agentLogFiles
|
|
657
|
+
.map(
|
|
658
|
+
(file) => `
|
|
659
|
+
<div class="task-card">
|
|
660
|
+
<header>
|
|
661
|
+
<div>
|
|
662
|
+
<div class="task-title">${file.name}</div>
|
|
663
|
+
<div class="meta">${Math.round(file.size / 1024)}kb · ${new Date(file.mtime).toLocaleString()}</div>
|
|
664
|
+
</div>
|
|
665
|
+
<span class="badge">log</span>
|
|
666
|
+
</header>
|
|
667
|
+
<div class="button-row">
|
|
668
|
+
<button class="action muted" data-action="agentlogs:open:${file.name}">Open</button>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
`,
|
|
672
|
+
)
|
|
673
|
+
.join("");
|
|
674
|
+
const tailText = state.agentLogTail?.lines ? state.agentLogTail.lines.join("\n") : "Select a log file.";
|
|
675
|
+
const tailMeta = state.agentLogTail?.truncated ? `<span class="pill">Tail clipped</span>` : "";
|
|
676
|
+
|
|
677
|
+
return `
|
|
678
|
+
<section class="card">
|
|
679
|
+
<h2>Agent Log Library</h2>
|
|
680
|
+
<div class="input-row">
|
|
681
|
+
<input id="agentlog-search" type="text" placeholder="Search log files" value="${state.agentLogQuery}" />
|
|
682
|
+
<button class="action muted" data-action="agentlogs:search">Search</button>
|
|
683
|
+
</div>
|
|
684
|
+
<div class="range-row" style="margin-top:10px">
|
|
685
|
+
<input type="range" min="50" max="800" step="50" value="${state.agentLogLines}" data-action="agentlogs:lines" />
|
|
686
|
+
<span class="pill">${state.agentLogLines} lines</span>
|
|
687
|
+
</div>
|
|
688
|
+
</section>
|
|
689
|
+
<section class="card">
|
|
690
|
+
<h3>Log Files</h3>
|
|
691
|
+
<div class="list">${logList || "<p>No log files found.</p>"}</div>
|
|
692
|
+
</section>
|
|
693
|
+
<section class="card">
|
|
694
|
+
<h3>${state.agentLogFile || "Log Tail"} ${tailMeta}</h3>
|
|
695
|
+
<div class="log-box">${tailText}</div>
|
|
696
|
+
</section>
|
|
697
|
+
<section class="card">
|
|
698
|
+
<h3>Worktree Context</h3>
|
|
699
|
+
<div class="input-row">
|
|
700
|
+
<input id="agentlog-context" type="text" placeholder="Worktree search (branch fragment)" />
|
|
701
|
+
<button class="action muted" data-action="agentlogs:context">Load</button>
|
|
702
|
+
</div>
|
|
703
|
+
<div class="log-box" style="margin-top:12px">${
|
|
704
|
+
state.agentContext
|
|
705
|
+
? [
|
|
706
|
+
`Worktree: ${state.agentContext.name || "?"}`,
|
|
707
|
+
"",
|
|
708
|
+
state.agentContext.gitLog || "No git log.",
|
|
709
|
+
"",
|
|
710
|
+
state.agentContext.gitStatus || "Clean worktree.",
|
|
711
|
+
"",
|
|
712
|
+
state.agentContext.diffStat || "No diff stat.",
|
|
713
|
+
].join("\n")
|
|
714
|
+
: "Load a worktree context to view git log/status."
|
|
715
|
+
}</div>
|
|
716
|
+
</section>
|
|
717
|
+
`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function renderExecutor() {
|
|
721
|
+
const executor = state.executor?.data;
|
|
722
|
+
const mode = state.executor?.mode || "vk";
|
|
723
|
+
return `
|
|
724
|
+
<section class="card">
|
|
725
|
+
<h2>Executor Status</h2>
|
|
726
|
+
<p>Mode: ${mode}</p>
|
|
727
|
+
<p>Slots: ${executor?.activeSlots ?? 0}/${executor?.maxParallel ?? "-"}</p>
|
|
728
|
+
<p>Poll: ${executor?.pollIntervalMs ? executor.pollIntervalMs / 1000 : "-"}s · Timeout: ${executor?.taskTimeoutMs ? Math.round(executor.taskTimeoutMs / 60000) : "-"}m</p>
|
|
729
|
+
<div class="range-row" style="margin-top:12px">
|
|
730
|
+
<input type="range" min="0" max="20" step="1" value="${executor?.maxParallel ?? 0}" data-action="executor:maxparallel" />
|
|
731
|
+
<span class="pill">Max ${executor?.maxParallel ?? "-"}</span>
|
|
732
|
+
</div>
|
|
733
|
+
<div class="button-row">
|
|
734
|
+
<button class="action" data-action="executor:pause">Pause</button>
|
|
735
|
+
<button class="action secondary" data-action="executor:resume">Resume</button>
|
|
736
|
+
<button class="action muted" data-action="command:/executor">Send /executor</button>
|
|
737
|
+
</div>
|
|
738
|
+
</section>
|
|
739
|
+
`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function renderLogs() {
|
|
743
|
+
const logText = state.logs?.lines ? state.logs.lines.join("\n") : "No logs yet.";
|
|
744
|
+
return `
|
|
745
|
+
<section class="card">
|
|
746
|
+
<h2>Logs</h2>
|
|
747
|
+
<div class="chips">
|
|
748
|
+
${[50, 200, 500].map(
|
|
749
|
+
(lines) =>
|
|
750
|
+
`<button class="chip ${state.logsLines === lines ? "active" : ""}" data-action="logs:lines:${lines}">${lines} lines</button>`,
|
|
751
|
+
).join("")}
|
|
752
|
+
</div>
|
|
753
|
+
<div class="range-row" style="margin-top:10px">
|
|
754
|
+
<input type="range" min="20" max="800" step="20" value="${state.logsLines}" data-action="logs:slider" />
|
|
755
|
+
<span class="pill">${state.logsLines} lines</span>
|
|
756
|
+
</div>
|
|
757
|
+
<div class="log-box" style="margin-top:14px">${logText}</div>
|
|
758
|
+
<div class="button-row" style="margin-top:12px">
|
|
759
|
+
<button class="action muted" data-action="command:/logs ${state.logsLines}">Send /logs to chat</button>
|
|
760
|
+
</div>
|
|
761
|
+
</section>
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function renderCommands() {
|
|
766
|
+
return `
|
|
767
|
+
<section class="card">
|
|
768
|
+
<h2>Command Console</h2>
|
|
769
|
+
<p>Send any slash command to the bot. Responses appear in chat.</p>
|
|
770
|
+
<div class="input-row">
|
|
771
|
+
<input type="text" id="command-input" placeholder="/status" />
|
|
772
|
+
<button class="action" data-action="command:send">Send</button>
|
|
773
|
+
</div>
|
|
774
|
+
<div class="button-row" style="margin-top:12px">
|
|
775
|
+
<button class="action muted" data-action="command:/menu">Open Chat Menu</button>
|
|
776
|
+
<button class="action secondary" data-action="command:/helpfull">All Commands</button>
|
|
777
|
+
</div>
|
|
778
|
+
</section>
|
|
779
|
+
<section class="card">
|
|
780
|
+
<h3>Task Ops</h3>
|
|
781
|
+
<div class="input-row">
|
|
782
|
+
<input id="starttask-input" type="text" placeholder="Task ID" />
|
|
783
|
+
<button class="action muted" data-action="command:starttask">Start Task</button>
|
|
784
|
+
</div>
|
|
785
|
+
<div class="input-row" style="margin-top:10px">
|
|
786
|
+
<input id="retry-input" type="text" placeholder="Retry reason" />
|
|
787
|
+
<button class="action secondary" data-action="command:retry">Retry</button>
|
|
788
|
+
<button class="action muted" data-action="command:/plan">Plan</button>
|
|
789
|
+
</div>
|
|
790
|
+
</section>
|
|
791
|
+
<section class="card">
|
|
792
|
+
<h3>Agent Control</h3>
|
|
793
|
+
<div class="input-row">
|
|
794
|
+
<textarea id="ask-input" rows="2" placeholder="Ask the agent..."></textarea>
|
|
795
|
+
<button class="action" data-action="command:ask">Ask</button>
|
|
796
|
+
</div>
|
|
797
|
+
<div class="input-row" style="margin-top:10px">
|
|
798
|
+
<input id="steer-input" type="text" placeholder="Steer prompt (focus on...)" />
|
|
799
|
+
<button class="action secondary" data-action="command:steer">Steer</button>
|
|
800
|
+
</div>
|
|
801
|
+
</section>
|
|
802
|
+
<section class="card">
|
|
803
|
+
<h3>Routing</h3>
|
|
804
|
+
<div class="segmented">
|
|
805
|
+
${["codex", "copilot", "claude", "auto"].map(
|
|
806
|
+
(sdk) =>
|
|
807
|
+
`<button data-action="command:sdk:${sdk}">${sdk}</button>`,
|
|
808
|
+
).join("")}
|
|
809
|
+
</div>
|
|
810
|
+
<div class="segmented" style="margin-top:10px">
|
|
811
|
+
${["vk", "github", "jira"].map(
|
|
812
|
+
(backend) =>
|
|
813
|
+
`<button data-action="command:kanban:${backend}">${backend}</button>`,
|
|
814
|
+
).join("")}
|
|
815
|
+
</div>
|
|
816
|
+
<div class="segmented" style="margin-top:10px">
|
|
817
|
+
${["us", "sweden", "auto"].map(
|
|
818
|
+
(region) =>
|
|
819
|
+
`<button data-action="command:region:${region}">${region}</button>`,
|
|
820
|
+
).join("")}
|
|
821
|
+
</div>
|
|
822
|
+
</section>
|
|
823
|
+
<section class="card">
|
|
824
|
+
<h3>Shell / Git</h3>
|
|
825
|
+
<div class="input-row">
|
|
826
|
+
<input id="shell-input" type="text" placeholder="ls -la" />
|
|
827
|
+
<button class="action muted" data-action="command:shell">Run /shell</button>
|
|
828
|
+
</div>
|
|
829
|
+
<div class="input-row" style="margin-top:10px">
|
|
830
|
+
<input id="git-input" type="text" placeholder="status --short" />
|
|
831
|
+
<button class="action muted" data-action="command:git">Run /git</button>
|
|
832
|
+
</div>
|
|
833
|
+
</section>
|
|
834
|
+
`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function renderModal() {
|
|
838
|
+
if (!state.modal) return "";
|
|
839
|
+
if (state.modal.type === "task") {
|
|
840
|
+
const task = state.modal.task;
|
|
841
|
+
if (!task) return "";
|
|
842
|
+
const priority = task.priority || "";
|
|
843
|
+
const status = task.status || "todo";
|
|
844
|
+
return `
|
|
845
|
+
<div class="overlay" data-overlay="true">
|
|
846
|
+
<div class="modal">
|
|
847
|
+
<h2>${task.title || "(untitled task)"}</h2>
|
|
848
|
+
<p class="meta">ID: ${task.id}</p>
|
|
849
|
+
<div class="input-row" style="margin-top:10px">
|
|
850
|
+
<input id="task-edit-title" type="text" value="${task.title || ""}" placeholder="Task title" />
|
|
851
|
+
</div>
|
|
852
|
+
<div class="input-row" style="margin-top:10px">
|
|
853
|
+
<textarea id="task-edit-description" rows="5" placeholder="Task description">${task.description || ""}</textarea>
|
|
854
|
+
</div>
|
|
855
|
+
<div class="input-row" style="margin-top:10px">
|
|
856
|
+
<select id="task-edit-status">
|
|
857
|
+
${["todo", "inprogress", "inreview", "done", "cancelled"]
|
|
858
|
+
.map((item) => `<option value="${item}" ${item === status ? "selected" : ""}>${item}</option>`)
|
|
859
|
+
.join("")}
|
|
860
|
+
</select>
|
|
861
|
+
<select id="task-edit-priority">
|
|
862
|
+
<option value="" ${priority ? "" : "selected"}>priority: none</option>
|
|
863
|
+
${["low", "medium", "high", "critical"]
|
|
864
|
+
.map((item) => `<option value="${item}" ${item === priority ? "selected" : ""}>priority: ${item}</option>`)
|
|
865
|
+
.join("")}
|
|
866
|
+
</select>
|
|
867
|
+
</div>
|
|
868
|
+
<div class="button-row" style="margin-top:14px">
|
|
869
|
+
${state.manualMode && task.status === "todo" ? `<button class="action" data-action="task:start:${task.id}">Start</button>` : ""}
|
|
870
|
+
<button class="action secondary" data-action="task:save:${task.id}">Save</button>
|
|
871
|
+
<button class="action muted" data-action="task:update:${task.id}:inreview">Mark Review</button>
|
|
872
|
+
<button class="action muted" data-action="modal:close">Close</button>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
`;
|
|
877
|
+
}
|
|
878
|
+
return "";
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function render() {
|
|
882
|
+
tabs.forEach((tab) => {
|
|
883
|
+
const target = tab.dataset.action.replace("tab:", "");
|
|
884
|
+
tab.classList.toggle("active", target === state.tab);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
if (state.tab === "overview") view.innerHTML = renderOverview();
|
|
888
|
+
if (state.tab === "tasks") view.innerHTML = renderTasks();
|
|
889
|
+
if (state.tab === "agents") view.innerHTML = renderAgents();
|
|
890
|
+
if (state.tab === "worktrees") view.innerHTML = renderWorktrees();
|
|
891
|
+
if (state.tab === "workspaces") view.innerHTML = renderWorkspaces();
|
|
892
|
+
if (state.tab === "presence") view.innerHTML = renderPresence();
|
|
893
|
+
if (state.tab === "executor") view.innerHTML = renderExecutor();
|
|
894
|
+
if (state.tab === "logs") view.innerHTML = renderLogs();
|
|
895
|
+
if (state.tab === "git") view.innerHTML = renderGit();
|
|
896
|
+
if (state.tab === "agentlogs") view.innerHTML = renderAgentLogs();
|
|
897
|
+
if (state.tab === "commands") view.innerHTML = renderCommands();
|
|
898
|
+
view.insertAdjacentHTML("beforeend", renderModal());
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function handleAction(action, element) {
|
|
902
|
+
if (action.startsWith("tab:")) {
|
|
903
|
+
state.tab = action.replace("tab:", "");
|
|
904
|
+
state.modal = null;
|
|
905
|
+
if (state.ws?.readyState === WebSocket.OPEN) {
|
|
906
|
+
state.ws.send(
|
|
907
|
+
JSON.stringify({ type: "subscribe", channels: channelsForTab(state.tab) }),
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
await refreshTab();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (action === "refresh") {
|
|
914
|
+
await refreshTab();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (action.startsWith("tasks:filter:")) {
|
|
918
|
+
state.tasksStatus = action.replace("tasks:filter:", "");
|
|
919
|
+
state.tasksPage = 0;
|
|
920
|
+
await refreshTab();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (action === "tasks:prev") {
|
|
924
|
+
state.tasksPage = Math.max(0, state.tasksPage - 1);
|
|
925
|
+
await refreshTab();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (action === "tasks:next") {
|
|
929
|
+
state.tasksPage += 1;
|
|
930
|
+
await refreshTab();
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (action === "manual:toggle") {
|
|
934
|
+
const input = element?.querySelector?.("input");
|
|
935
|
+
if (input?.disabled) return;
|
|
936
|
+
const checked =
|
|
937
|
+
typeof element?.checked === "boolean" ? element.checked : input?.checked;
|
|
938
|
+
state.manualMode = typeof checked === "boolean" ? checked : !state.manualMode;
|
|
939
|
+
render();
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (action === "modal:close") {
|
|
943
|
+
state.modal = null;
|
|
944
|
+
render();
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (action.startsWith("task:start:")) {
|
|
948
|
+
const taskId = action.replace("task:start:", "");
|
|
949
|
+
const previousTasks = cloneStateValue(state.tasks);
|
|
950
|
+
const previousModal = cloneStateValue(state.modal);
|
|
951
|
+
await runOptimisticMutation(
|
|
952
|
+
() => {
|
|
953
|
+
state.tasks = state.tasks.map((task) =>
|
|
954
|
+
task.id === taskId ? { ...task, status: "inprogress" } : task,
|
|
955
|
+
);
|
|
956
|
+
if (state.modal?.task?.id === taskId) {
|
|
957
|
+
state.modal.task.status = "inprogress";
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
() =>
|
|
961
|
+
apiFetch("/api/tasks/start", {
|
|
962
|
+
method: "POST",
|
|
963
|
+
body: JSON.stringify({ taskId }),
|
|
964
|
+
}),
|
|
965
|
+
() => {
|
|
966
|
+
state.tasks = previousTasks;
|
|
967
|
+
state.modal = previousModal;
|
|
968
|
+
},
|
|
969
|
+
).catch((err) => alert(err.message));
|
|
970
|
+
state.modal = null;
|
|
971
|
+
scheduleRefresh(150);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (action.startsWith("task:detail:")) {
|
|
975
|
+
const taskId = action.replace("task:detail:", "");
|
|
976
|
+
const localTask = state.tasks.find((t) => t.id === taskId) || null;
|
|
977
|
+
const result = await apiFetch(`/api/tasks/detail?taskId=${encodeURIComponent(taskId)}`).catch(
|
|
978
|
+
() => ({ data: localTask }),
|
|
979
|
+
);
|
|
980
|
+
const task = result.data || localTask;
|
|
981
|
+
state.modal = { type: "task", task };
|
|
982
|
+
render();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (action.startsWith("task:save:")) {
|
|
986
|
+
const taskId = action.replace("task:save:", "");
|
|
987
|
+
const title = document.getElementById("task-edit-title")?.value ?? "";
|
|
988
|
+
const description = document.getElementById("task-edit-description")?.value ?? "";
|
|
989
|
+
const status = document.getElementById("task-edit-status")?.value ?? "todo";
|
|
990
|
+
const priority = document.getElementById("task-edit-priority")?.value ?? "";
|
|
991
|
+
const previousTasks = cloneStateValue(state.tasks);
|
|
992
|
+
const previousModal = cloneStateValue(state.modal);
|
|
993
|
+
await runOptimisticMutation(
|
|
994
|
+
() => {
|
|
995
|
+
state.tasks = state.tasks.map((task) =>
|
|
996
|
+
task.id === taskId
|
|
997
|
+
? {
|
|
998
|
+
...task,
|
|
999
|
+
title,
|
|
1000
|
+
description,
|
|
1001
|
+
status,
|
|
1002
|
+
priority: priority || null,
|
|
1003
|
+
}
|
|
1004
|
+
: task,
|
|
1005
|
+
);
|
|
1006
|
+
if (state.modal?.task?.id === taskId) {
|
|
1007
|
+
state.modal.task = {
|
|
1008
|
+
...state.modal.task,
|
|
1009
|
+
title,
|
|
1010
|
+
description,
|
|
1011
|
+
status,
|
|
1012
|
+
priority: priority || null,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
async () => {
|
|
1017
|
+
const response = await apiFetch("/api/tasks/edit", {
|
|
1018
|
+
method: "POST",
|
|
1019
|
+
body: JSON.stringify({ taskId, title, description, status, priority }),
|
|
1020
|
+
});
|
|
1021
|
+
if (response?.data) {
|
|
1022
|
+
state.tasks = state.tasks.map((task) =>
|
|
1023
|
+
task.id === taskId ? { ...task, ...response.data } : task,
|
|
1024
|
+
);
|
|
1025
|
+
if (state.modal?.task?.id === taskId) {
|
|
1026
|
+
state.modal.task = { ...state.modal.task, ...response.data };
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return response;
|
|
1030
|
+
},
|
|
1031
|
+
() => {
|
|
1032
|
+
state.tasks = previousTasks;
|
|
1033
|
+
state.modal = previousModal;
|
|
1034
|
+
},
|
|
1035
|
+
).catch((err) => alert(err.message));
|
|
1036
|
+
render();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (action.startsWith("task:update:")) {
|
|
1040
|
+
const [, taskId, status] = action.split(":");
|
|
1041
|
+
const previousTasks = cloneStateValue(state.tasks);
|
|
1042
|
+
const previousModal = cloneStateValue(state.modal);
|
|
1043
|
+
await runOptimisticMutation(
|
|
1044
|
+
() => {
|
|
1045
|
+
state.tasks = state.tasks.map((task) =>
|
|
1046
|
+
task.id === taskId ? { ...task, status } : task,
|
|
1047
|
+
);
|
|
1048
|
+
if (state.modal?.task?.id === taskId) {
|
|
1049
|
+
state.modal.task.status = status;
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
async () => {
|
|
1053
|
+
const response = await apiFetch("/api/tasks/update", {
|
|
1054
|
+
method: "POST",
|
|
1055
|
+
body: JSON.stringify({ taskId, status }),
|
|
1056
|
+
});
|
|
1057
|
+
if (response?.data) {
|
|
1058
|
+
state.tasks = state.tasks.map((task) =>
|
|
1059
|
+
task.id === taskId ? { ...task, ...response.data } : task,
|
|
1060
|
+
);
|
|
1061
|
+
if (state.modal?.task?.id === taskId) {
|
|
1062
|
+
state.modal.task = { ...state.modal.task, ...response.data };
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return response;
|
|
1066
|
+
},
|
|
1067
|
+
() => {
|
|
1068
|
+
state.tasks = previousTasks;
|
|
1069
|
+
state.modal = previousModal;
|
|
1070
|
+
},
|
|
1071
|
+
).catch((err) => alert(err.message));
|
|
1072
|
+
if (state.modal && status === "done") {
|
|
1073
|
+
state.modal = null;
|
|
1074
|
+
}
|
|
1075
|
+
render();
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (action === "tasks:search" && element) {
|
|
1079
|
+
state.tasksQuery = element.value || "";
|
|
1080
|
+
render();
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (action === "executor:pause") {
|
|
1084
|
+
const previous = cloneStateValue(state.executor);
|
|
1085
|
+
await runOptimisticMutation(
|
|
1086
|
+
() => {
|
|
1087
|
+
if (state.executor) state.executor.paused = true;
|
|
1088
|
+
},
|
|
1089
|
+
() => apiFetch("/api/executor/pause", { method: "POST" }),
|
|
1090
|
+
() => {
|
|
1091
|
+
state.executor = previous;
|
|
1092
|
+
},
|
|
1093
|
+
).catch((err) => alert(err.message));
|
|
1094
|
+
scheduleRefresh(120);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (action === "executor:resume") {
|
|
1098
|
+
const previous = cloneStateValue(state.executor);
|
|
1099
|
+
await runOptimisticMutation(
|
|
1100
|
+
() => {
|
|
1101
|
+
if (state.executor) state.executor.paused = false;
|
|
1102
|
+
},
|
|
1103
|
+
() => apiFetch("/api/executor/resume", { method: "POST" }),
|
|
1104
|
+
() => {
|
|
1105
|
+
state.executor = previous;
|
|
1106
|
+
},
|
|
1107
|
+
).catch((err) => alert(err.message));
|
|
1108
|
+
scheduleRefresh(120);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (action === "executor:maxparallel" && element) {
|
|
1112
|
+
const value = Number(element.value || "0");
|
|
1113
|
+
const previous = cloneStateValue(state.executor);
|
|
1114
|
+
await runOptimisticMutation(
|
|
1115
|
+
() => {
|
|
1116
|
+
if (state.executor?.data) {
|
|
1117
|
+
state.executor.data.maxParallel = value;
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
() =>
|
|
1121
|
+
apiFetch("/api/executor/maxparallel", {
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
body: JSON.stringify({ value }),
|
|
1124
|
+
}),
|
|
1125
|
+
() => {
|
|
1126
|
+
state.executor = previous;
|
|
1127
|
+
},
|
|
1128
|
+
).catch((err) => alert(err.message));
|
|
1129
|
+
scheduleRefresh(120);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (action.startsWith("logs:lines:")) {
|
|
1133
|
+
state.logsLines = Number(action.replace("logs:lines:", "")) || 200;
|
|
1134
|
+
await refreshTab();
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (action === "logs:slider" && element) {
|
|
1138
|
+
state.logsLines = Number(element.value || "200");
|
|
1139
|
+
await refreshTab();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (action === "git:refresh") {
|
|
1143
|
+
await loadGit();
|
|
1144
|
+
render();
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (action === "agentlogs:search") {
|
|
1148
|
+
const input = document.getElementById("agentlog-search");
|
|
1149
|
+
state.agentLogQuery = input?.value?.trim() || "";
|
|
1150
|
+
state.agentLogFile = "";
|
|
1151
|
+
await loadAgentLogFiles();
|
|
1152
|
+
await loadAgentLogTail();
|
|
1153
|
+
render();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (action.startsWith("agentlogs:search:")) {
|
|
1157
|
+
const query = action.replace("agentlogs:search:", "");
|
|
1158
|
+
state.tab = "agentlogs";
|
|
1159
|
+
state.agentLogQuery = query;
|
|
1160
|
+
state.agentLogFile = "";
|
|
1161
|
+
await refreshTab();
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (action.startsWith("agentlogs:open:")) {
|
|
1165
|
+
state.agentLogFile = action.replace("agentlogs:open:", "");
|
|
1166
|
+
await loadAgentLogTail();
|
|
1167
|
+
render();
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (action === "agentlogs:lines" && element) {
|
|
1171
|
+
state.agentLogLines = Number(element.value || "200");
|
|
1172
|
+
await loadAgentLogTail();
|
|
1173
|
+
render();
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (action === "agentlogs:context") {
|
|
1177
|
+
const input = document.getElementById("agentlog-context");
|
|
1178
|
+
await loadAgentContext(input?.value?.trim() || "");
|
|
1179
|
+
render();
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (action === "worktrees:prune") {
|
|
1183
|
+
await apiFetch("/api/worktrees/prune", { method: "POST" }).catch((err) =>
|
|
1184
|
+
alert(err.message),
|
|
1185
|
+
);
|
|
1186
|
+
scheduleRefresh(120);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (action.startsWith("worktrees:release-branch:")) {
|
|
1190
|
+
const branch = action.replace("worktrees:release-branch:", "");
|
|
1191
|
+
const previous = cloneStateValue(state.worktrees);
|
|
1192
|
+
await runOptimisticMutation(
|
|
1193
|
+
() => {
|
|
1194
|
+
state.worktrees = state.worktrees.filter((item) => item.branch !== branch);
|
|
1195
|
+
},
|
|
1196
|
+
() =>
|
|
1197
|
+
apiFetch("/api/worktrees/release", {
|
|
1198
|
+
method: "POST",
|
|
1199
|
+
body: JSON.stringify({ branch }),
|
|
1200
|
+
}),
|
|
1201
|
+
() => {
|
|
1202
|
+
state.worktrees = previous;
|
|
1203
|
+
},
|
|
1204
|
+
).catch((err) => alert(err.message));
|
|
1205
|
+
scheduleRefresh(120);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (action.startsWith("worktrees:release:")) {
|
|
1209
|
+
const taskKey = action.replace("worktrees:release:", "");
|
|
1210
|
+
const previous = cloneStateValue(state.worktrees);
|
|
1211
|
+
await runOptimisticMutation(
|
|
1212
|
+
() => {
|
|
1213
|
+
state.worktrees = state.worktrees.filter((item) => item.taskKey !== taskKey);
|
|
1214
|
+
},
|
|
1215
|
+
() =>
|
|
1216
|
+
apiFetch("/api/worktrees/release", {
|
|
1217
|
+
method: "POST",
|
|
1218
|
+
body: JSON.stringify({ taskKey }),
|
|
1219
|
+
}),
|
|
1220
|
+
() => {
|
|
1221
|
+
state.worktrees = previous;
|
|
1222
|
+
},
|
|
1223
|
+
).catch((err) => alert(err.message));
|
|
1224
|
+
scheduleRefresh(120);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (action === "worktrees:release-input") {
|
|
1228
|
+
const input = document.getElementById("worktree-release-input");
|
|
1229
|
+
const value = input?.value?.trim();
|
|
1230
|
+
if (!value) return;
|
|
1231
|
+
await apiFetch("/api/worktrees/release", {
|
|
1232
|
+
method: "POST",
|
|
1233
|
+
body: JSON.stringify({ taskKey: value, branch: value }),
|
|
1234
|
+
}).catch((err) => alert(err.message));
|
|
1235
|
+
scheduleRefresh(120);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (action.startsWith("shared:claim:")) {
|
|
1239
|
+
const workspaceId = action.replace("shared:claim:", "");
|
|
1240
|
+
const owner = document.getElementById("shared-owner")?.value?.trim() || "";
|
|
1241
|
+
const ttlMinutes = Number(document.getElementById("shared-ttl")?.value || "");
|
|
1242
|
+
const note = document.getElementById("shared-note")?.value?.trim() || "";
|
|
1243
|
+
const previous = cloneStateValue(state.sharedWorkspaces);
|
|
1244
|
+
await runOptimisticMutation(
|
|
1245
|
+
() => {
|
|
1246
|
+
const now = Date.now();
|
|
1247
|
+
const ws = state.sharedWorkspaces?.workspaces?.find((item) => item.id === workspaceId);
|
|
1248
|
+
if (ws) {
|
|
1249
|
+
ws.availability = "leased";
|
|
1250
|
+
ws.lease = {
|
|
1251
|
+
owner: owner || "telegram-ui",
|
|
1252
|
+
lease_expires_at: new Date(now + (ttlMinutes || 60) * 60000).toISOString(),
|
|
1253
|
+
note,
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
() =>
|
|
1258
|
+
apiFetch("/api/shared-workspaces/claim", {
|
|
1259
|
+
method: "POST",
|
|
1260
|
+
body: JSON.stringify({ workspaceId, owner, ttlMinutes, note }),
|
|
1261
|
+
}),
|
|
1262
|
+
() => {
|
|
1263
|
+
state.sharedWorkspaces = previous;
|
|
1264
|
+
},
|
|
1265
|
+
).catch((err) => alert(err.message));
|
|
1266
|
+
scheduleRefresh(120);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (action.startsWith("shared:renew:")) {
|
|
1270
|
+
const workspaceId = action.replace("shared:renew:", "");
|
|
1271
|
+
const owner = document.getElementById("shared-owner")?.value?.trim() || "";
|
|
1272
|
+
const ttlMinutes = Number(document.getElementById("shared-ttl")?.value || "");
|
|
1273
|
+
const previous = cloneStateValue(state.sharedWorkspaces);
|
|
1274
|
+
await runOptimisticMutation(
|
|
1275
|
+
() => {
|
|
1276
|
+
const ws = state.sharedWorkspaces?.workspaces?.find((item) => item.id === workspaceId);
|
|
1277
|
+
if (ws?.lease) {
|
|
1278
|
+
ws.lease.owner = owner || ws.lease.owner;
|
|
1279
|
+
ws.lease.lease_expires_at = new Date(
|
|
1280
|
+
Date.now() + (ttlMinutes || 60) * 60000,
|
|
1281
|
+
).toISOString();
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
() =>
|
|
1285
|
+
apiFetch("/api/shared-workspaces/renew", {
|
|
1286
|
+
method: "POST",
|
|
1287
|
+
body: JSON.stringify({ workspaceId, owner, ttlMinutes }),
|
|
1288
|
+
}),
|
|
1289
|
+
() => {
|
|
1290
|
+
state.sharedWorkspaces = previous;
|
|
1291
|
+
},
|
|
1292
|
+
).catch((err) => alert(err.message));
|
|
1293
|
+
scheduleRefresh(120);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (action.startsWith("shared:release:")) {
|
|
1297
|
+
const workspaceId = action.replace("shared:release:", "");
|
|
1298
|
+
const owner = document.getElementById("shared-owner")?.value?.trim() || "";
|
|
1299
|
+
const previous = cloneStateValue(state.sharedWorkspaces);
|
|
1300
|
+
await runOptimisticMutation(
|
|
1301
|
+
() => {
|
|
1302
|
+
const ws = state.sharedWorkspaces?.workspaces?.find((item) => item.id === workspaceId);
|
|
1303
|
+
if (ws) {
|
|
1304
|
+
ws.availability = "available";
|
|
1305
|
+
ws.lease = null;
|
|
1306
|
+
}
|
|
1307
|
+
},
|
|
1308
|
+
() =>
|
|
1309
|
+
apiFetch("/api/shared-workspaces/release", {
|
|
1310
|
+
method: "POST",
|
|
1311
|
+
body: JSON.stringify({ workspaceId, owner }),
|
|
1312
|
+
}),
|
|
1313
|
+
() => {
|
|
1314
|
+
state.sharedWorkspaces = previous;
|
|
1315
|
+
},
|
|
1316
|
+
).catch((err) => alert(err.message));
|
|
1317
|
+
scheduleRefresh(120);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (action === "command:send") {
|
|
1321
|
+
const input = document.getElementById("command-input");
|
|
1322
|
+
if (input && input.value) {
|
|
1323
|
+
sendCommandToChat(input.value.trim());
|
|
1324
|
+
input.value = "";
|
|
1325
|
+
}
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
if (action === "command:starttask") {
|
|
1329
|
+
const input = document.getElementById("starttask-input");
|
|
1330
|
+
const taskId = input?.value?.trim();
|
|
1331
|
+
if (taskId) sendCommandToChat(`/starttask ${taskId}`);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (action === "command:retry") {
|
|
1335
|
+
const input = document.getElementById("retry-input");
|
|
1336
|
+
const reason = input?.value?.trim();
|
|
1337
|
+
sendCommandToChat(reason ? `/retry ${reason}` : "/retry");
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (action === "command:ask") {
|
|
1341
|
+
const input = document.getElementById("ask-input");
|
|
1342
|
+
const prompt = input?.value?.trim();
|
|
1343
|
+
if (prompt) {
|
|
1344
|
+
sendCommandToChat(`/ask ${prompt}`);
|
|
1345
|
+
input.value = "";
|
|
1346
|
+
}
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (action === "command:steer") {
|
|
1350
|
+
const input = document.getElementById("steer-input");
|
|
1351
|
+
const prompt = input?.value?.trim();
|
|
1352
|
+
if (prompt) {
|
|
1353
|
+
sendCommandToChat(`/steer ${prompt}`);
|
|
1354
|
+
input.value = "";
|
|
1355
|
+
}
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (action === "command:git") {
|
|
1359
|
+
const input = document.getElementById("git-input") || document.getElementById("git-command");
|
|
1360
|
+
const args = input?.value?.trim() || "";
|
|
1361
|
+
sendCommandToChat(`/git ${args}`.trim());
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (action === "command:shell") {
|
|
1365
|
+
const input = document.getElementById("shell-input");
|
|
1366
|
+
const args = input?.value?.trim() || "";
|
|
1367
|
+
sendCommandToChat(`/shell ${args}`.trim());
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (action.startsWith("command:sdk:")) {
|
|
1371
|
+
const sdk = action.replace("command:sdk:", "");
|
|
1372
|
+
sendCommandToChat(`/sdk ${sdk}`);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (action.startsWith("command:kanban:")) {
|
|
1376
|
+
const backend = action.replace("command:kanban:", "");
|
|
1377
|
+
sendCommandToChat(`/kanban ${backend}`);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (action.startsWith("command:region:")) {
|
|
1381
|
+
const region = action.replace("command:region:", "");
|
|
1382
|
+
sendCommandToChat(`/region ${region}`);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (action.startsWith("command:")) {
|
|
1386
|
+
const command = action.replace("command:", "");
|
|
1387
|
+
sendCommandToChat(command);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (action === "tasks:project" && element?.value) {
|
|
1391
|
+
state.tasksProject = element.value;
|
|
1392
|
+
state.tasksPage = 0;
|
|
1393
|
+
await refreshTab();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
document.body.addEventListener("click", (event) => {
|
|
1398
|
+
if (event.target?.dataset?.overlay === "true") {
|
|
1399
|
+
state.modal = null;
|
|
1400
|
+
render();
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const target = event.target.closest("[data-action]");
|
|
1404
|
+
if (!target) return;
|
|
1405
|
+
handleAction(target.dataset.action, target);
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
document.body.addEventListener("change", (event) => {
|
|
1409
|
+
const target = event.target.closest("[data-action]");
|
|
1410
|
+
if (!target) return;
|
|
1411
|
+
const action = target.dataset.action;
|
|
1412
|
+
if (action === "tasks:project") {
|
|
1413
|
+
handleAction("tasks:project", target);
|
|
1414
|
+
}
|
|
1415
|
+
if (action === "executor:maxparallel") {
|
|
1416
|
+
handleAction("executor:maxparallel", target);
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
document.body.addEventListener("input", (event) => {
|
|
1421
|
+
const target = event.target.closest("[data-action]");
|
|
1422
|
+
if (!target) return;
|
|
1423
|
+
const action = target.dataset.action;
|
|
1424
|
+
if (action === "tasks:search") {
|
|
1425
|
+
handleAction("tasks:search", target);
|
|
1426
|
+
}
|
|
1427
|
+
if (action === "logs:slider") {
|
|
1428
|
+
handleAction("logs:slider", target);
|
|
1429
|
+
}
|
|
1430
|
+
if (action === "agentlogs:lines") {
|
|
1431
|
+
handleAction("agentlogs:lines", target);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
async function boot() {
|
|
1436
|
+
const tg = telegram();
|
|
1437
|
+
if (tg) {
|
|
1438
|
+
tg.expand();
|
|
1439
|
+
tg.ready();
|
|
1440
|
+
setConnection(true, "via Telegram");
|
|
1441
|
+
} else {
|
|
1442
|
+
setConnection(false, "(open in Telegram)" );
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
await refreshTab();
|
|
1446
|
+
connectRealtime();
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
console.error(err);
|
|
1449
|
+
setConnection(false, "(API unavailable)");
|
|
1450
|
+
render();
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
boot();
|
|
1455
|
+
|
|
1456
|
+
window.addEventListener("beforeunload", () => {
|
|
1457
|
+
try {
|
|
1458
|
+
state.ws?.close();
|
|
1459
|
+
} catch {
|
|
1460
|
+
// no-op
|
|
1461
|
+
}
|
|
1462
|
+
if (state.wsReconnectTimer) clearTimeout(state.wsReconnectTimer);
|
|
1463
|
+
if (state.wsRefreshTimer) clearTimeout(state.wsRefreshTimer);
|
|
1464
|
+
});
|