@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.
Files changed (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. package/worktree-manager.mjs +1266 -0
@@ -0,0 +1,887 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Tab: Control β€” executor, commands, routing, quick commands
3
+ * ────────────────────────────────────────────────────────────── */
4
+ import { h } from "preact";
5
+ import { useState, useCallback, useEffect, useRef } from "preact/hooks";
6
+ import htm from "htm";
7
+
8
+ const html = htm.bind(h);
9
+
10
+ import { haptic, showConfirm } from "../modules/telegram.js";
11
+ import { apiFetch, sendCommandToChat } from "../modules/api.js";
12
+ import {
13
+ executorData,
14
+ configData,
15
+ loadConfig,
16
+ showToast,
17
+ runOptimistic,
18
+ scheduleRefresh,
19
+ } from "../modules/state.js";
20
+ import { ICONS } from "../modules/icons.js";
21
+ import { cloneValue, truncate } from "../modules/utils.js";
22
+ import { Card, Badge, SkeletonCard } from "../components/shared.js";
23
+ import { SegmentedControl } from "../components/forms.js";
24
+
25
+ /* ─── Command registry for autocomplete ─── */
26
+ const CMD_REGISTRY = [
27
+ { cmd: '/status', desc: 'Show orchestrator status', cat: 'System' },
28
+ { cmd: '/health', desc: 'Health check', cat: 'System' },
29
+ { cmd: '/menu', desc: 'Show command menu', cat: 'System' },
30
+ { cmd: '/helpfull', desc: 'Full help text', cat: 'System' },
31
+ { cmd: '/plan', desc: 'Generate execution plan', cat: 'Tasks' },
32
+ { cmd: '/logs', desc: 'View recent logs', cat: 'Logs' },
33
+ { cmd: '/diff', desc: 'View git diff', cat: 'Git' },
34
+ { cmd: '/steer', desc: 'Steer active agent', cat: 'Agent' },
35
+ { cmd: '/ask', desc: 'Ask agent a question', cat: 'Agent' },
36
+ { cmd: '/start', desc: 'Start a task', cat: 'Tasks' },
37
+ { cmd: '/retry', desc: 'Retry failed task', cat: 'Tasks' },
38
+ { cmd: '/cancel', desc: 'Cancel running task', cat: 'Tasks' },
39
+ { cmd: '/shell', desc: 'Execute shell command', cat: 'Shell' },
40
+ { cmd: '/git', desc: 'Execute git command', cat: 'Git' },
41
+ ];
42
+
43
+ /* ─── Category badge colors ─── */
44
+ const CAT_COLORS = {
45
+ System: '#6366f1', Tasks: '#f59e0b', Logs: '#10b981',
46
+ Git: '#f97316', Agent: '#8b5cf6', Shell: '#64748b',
47
+ };
48
+
49
+ /* ─── Persistent history key & limits ─── */
50
+ const HISTORY_KEY = 've-cmd-history';
51
+ const MAX_HISTORY = 50;
52
+ const MAX_OUTPUTS = 3;
53
+ const POLL_INTERVAL = 2000;
54
+ const MAX_POLLS = 7;
55
+
56
+ /* ─── ControlTab ─── */
57
+ export function ControlTab() {
58
+ const executor = executorData.value;
59
+ const execData = executor?.data;
60
+ const mode = executor?.mode || "vk";
61
+ const config = configData.value;
62
+
63
+ /* Form inputs */
64
+ const [commandInput, setCommandInput] = useState("");
65
+ const [startTaskId, setStartTaskId] = useState("");
66
+ const [retryTaskId, setRetryTaskId] = useState("");
67
+ const [retryReason, setRetryReason] = useState("");
68
+ const [askInput, setAskInput] = useState("");
69
+ const [steerInput, setSteerInput] = useState("");
70
+ const [quickCmdInput, setQuickCmdInput] = useState("");
71
+ const [quickCmdPrefix, setQuickCmdPrefix] = useState("shell");
72
+ const [quickCmdFeedback, setQuickCmdFeedback] = useState("");
73
+ const [quickCmdFeedbackTone, setQuickCmdFeedbackTone] = useState("info");
74
+ const [maxParallel, setMaxParallel] = useState(execData?.maxParallel ?? 0);
75
+ const [cmdHistory, setCmdHistory] = useState([]);
76
+ const [showHistory, setShowHistory] = useState(false);
77
+ const [backlogTasks, setBacklogTasks] = useState([]);
78
+ const [retryTasks, setRetryTasks] = useState([]);
79
+ const [tasksLoading, setTasksLoading] = useState(false);
80
+ const [startTaskError, setStartTaskError] = useState("");
81
+ const [retryTaskError, setRetryTaskError] = useState("");
82
+ const startTaskIdRef = useRef("");
83
+ const retryTaskIdRef = useRef("");
84
+
85
+ /* ── Autocomplete state ── */
86
+ const [acItems, setAcItems] = useState([]);
87
+ const [acIndex, setAcIndex] = useState(-1);
88
+ const [showAc, setShowAc] = useState(false);
89
+
90
+ /* ── Persistent history state ── */
91
+ const [historyIndex, setHistoryIndex] = useState(-1);
92
+ const savedInputRef = useRef("");
93
+
94
+ /* ── Inline output state ── */
95
+ const [cmdOutputs, setCmdOutputs] = useState([]);
96
+ const [runningCmd, setRunningCmd] = useState(null);
97
+ const [expandedOutputs, setExpandedOutputs] = useState({});
98
+ const pollRef = useRef(null);
99
+ const isPaused = Boolean(executor?.paused || execData?.paused);
100
+ const slotsLabel = `${execData?.activeSlots ?? 0}/${execData?.maxParallel ?? "β€”"}`;
101
+ const pollLabel = execData?.pollIntervalMs
102
+ ? `${Math.round(execData.pollIntervalMs / 1000)}s`
103
+ : "β€”";
104
+ const timeoutLabel = execData?.taskTimeoutMs
105
+ ? `${Math.round(execData.taskTimeoutMs / 60000)}m`
106
+ : "β€”";
107
+
108
+ /* ── Load persistent history on mount ── */
109
+ useEffect(() => {
110
+ try {
111
+ const saved = localStorage.getItem(HISTORY_KEY);
112
+ if (saved) {
113
+ const parsed = JSON.parse(saved);
114
+ if (Array.isArray(parsed)) setCmdHistory(parsed.slice(0, MAX_HISTORY));
115
+ }
116
+ } catch (_) { /* ignore corrupt data */ }
117
+ return () => { if (pollRef.current) clearInterval(pollRef.current); };
118
+ }, []);
119
+
120
+ useEffect(() => {
121
+ startTaskIdRef.current = startTaskId;
122
+ }, [startTaskId]);
123
+
124
+ useEffect(() => {
125
+ retryTaskIdRef.current = retryTaskId;
126
+ }, [retryTaskId]);
127
+
128
+ /* ── Autocomplete filter ── */
129
+ useEffect(() => {
130
+ if (commandInput.startsWith('/') && commandInput.length > 0) {
131
+ const q = commandInput.toLowerCase();
132
+ const matches = CMD_REGISTRY.filter((r) => r.cmd.toLowerCase().includes(q));
133
+ setAcItems(matches);
134
+ setAcIndex(-1);
135
+ setShowAc(matches.length > 0);
136
+ } else {
137
+ setShowAc(false);
138
+ setAcItems([]);
139
+ setAcIndex(-1);
140
+ }
141
+ }, [commandInput]);
142
+
143
+ /* ── Command history helper (persistent) ── */
144
+ const pushHistory = useCallback((cmd) => {
145
+ setCmdHistory((prev) => {
146
+ const next = [cmd, ...prev.filter((c) => c !== cmd)].slice(0, MAX_HISTORY);
147
+ try { localStorage.setItem(HISTORY_KEY, JSON.stringify(next)); } catch (_) {}
148
+ return next;
149
+ });
150
+ }, []);
151
+
152
+ /* ── Inline output polling ── */
153
+ const startOutputPolling = useCallback((cmd) => {
154
+ if (pollRef.current) clearInterval(pollRef.current);
155
+ const ts = new Date().toISOString();
156
+ setRunningCmd(cmd);
157
+ let pollCount = 0;
158
+ let lastContent = '';
159
+
160
+ pollRef.current = setInterval(async () => {
161
+ pollCount++;
162
+ try {
163
+ const res = await apiFetch('/api/logs?lines=15', { _silent: true });
164
+ const text = typeof res === 'string' ? res : (res?.logs || res?.data || JSON.stringify(res, null, 2));
165
+ if (text === lastContent || pollCount >= MAX_POLLS) {
166
+ clearInterval(pollRef.current);
167
+ pollRef.current = null;
168
+ setRunningCmd(null);
169
+ setCmdOutputs((prev) => {
170
+ const entry = { cmd, ts, output: text || '(no output)' };
171
+ const next = [entry, ...prev].slice(0, MAX_OUTPUTS);
172
+ return next;
173
+ });
174
+ setExpandedOutputs((prev) => ({ ...prev, [0]: true }));
175
+ }
176
+ lastContent = text;
177
+ } catch (_) {
178
+ clearInterval(pollRef.current);
179
+ pollRef.current = null;
180
+ setRunningCmd(null);
181
+ setCmdOutputs((prev) => {
182
+ const entry = { cmd, ts, output: '(failed to fetch output)' };
183
+ return [entry, ...prev].slice(0, MAX_OUTPUTS);
184
+ });
185
+ }
186
+ }, POLL_INTERVAL);
187
+ }, []);
188
+
189
+ const sendCmd = useCallback(
190
+ (cmd) => {
191
+ if (!cmd.trim()) return;
192
+ sendCommandToChat(cmd.trim());
193
+ pushHistory(cmd.trim());
194
+ setHistoryIndex(-1);
195
+ startOutputPolling(cmd.trim());
196
+ },
197
+ [pushHistory, startOutputPolling],
198
+ );
199
+
200
+ /* ── Config update helper ── */
201
+ const updateConfig = useCallback(
202
+ async (key, value) => {
203
+ haptic();
204
+ try {
205
+ await apiFetch("/api/config/update", {
206
+ method: "POST",
207
+ body: JSON.stringify({ key, value }),
208
+ });
209
+ await loadConfig();
210
+ showToast(`${key} β†’ ${value}`, "success");
211
+ } catch {
212
+ showToast(`Failed to update ${key}`, "error");
213
+ }
214
+ },
215
+ [],
216
+ );
217
+
218
+ const refreshTaskOptions = useCallback(async () => {
219
+ setTasksLoading(true);
220
+ try {
221
+ const res = await apiFetch("/api/tasks?page=0&pageSize=200", {
222
+ _silent: true,
223
+ });
224
+ const all = Array.isArray(res?.data) ? res.data : [];
225
+ const priorityRank = { critical: 0, high: 1, medium: 2, low: 3 };
226
+ const score = (t) =>
227
+ priorityRank[String(t?.priority || "").toLowerCase()] ?? 9;
228
+ const byPriority = (a, b) => {
229
+ const pa = score(a);
230
+ const pb = score(b);
231
+ if (pa !== pb) return pa - pb;
232
+ const ta = String(a?.updated_at || a?.updatedAt || "");
233
+ const tb = String(b?.updated_at || b?.updatedAt || "");
234
+ return tb.localeCompare(ta);
235
+ };
236
+
237
+ const backlog = all
238
+ .filter((t) =>
239
+ ["todo", "backlog", "open"].includes(
240
+ String(t?.status || "").toLowerCase(),
241
+ ),
242
+ )
243
+ .sort(byPriority);
244
+ const retryable = all
245
+ .filter((t) =>
246
+ ["error", "cancelled", "blocked", "failed", "inreview"].includes(
247
+ String(t?.status || "").toLowerCase(),
248
+ ),
249
+ )
250
+ .sort(byPriority);
251
+
252
+ setBacklogTasks(backlog);
253
+ setRetryTasks(retryable);
254
+
255
+ if (backlog.length > 0) {
256
+ const current = String(startTaskIdRef.current || "");
257
+ if (!backlog.some((t) => String(t?.id) === current)) {
258
+ setStartTaskId(String(backlog[0].id || ""));
259
+ }
260
+ } else {
261
+ setStartTaskId("");
262
+ }
263
+
264
+ if (retryable.length > 0) {
265
+ const currentRetry = String(retryTaskIdRef.current || "");
266
+ if (!retryable.some((t) => String(t?.id) === currentRetry)) {
267
+ setRetryTaskId(String(retryable[0].id || ""));
268
+ }
269
+ } else {
270
+ setRetryTaskId("");
271
+ }
272
+ } catch {
273
+ setBacklogTasks([]);
274
+ setRetryTasks([]);
275
+ } finally {
276
+ setTasksLoading(false);
277
+ }
278
+ }, []);
279
+
280
+ useEffect(() => {
281
+ refreshTaskOptions();
282
+ }, [refreshTaskOptions]);
283
+
284
+ /* ── Executor controls ── */
285
+ const handlePause = async () => {
286
+ const ok = await showConfirm(
287
+ "Pause the executor? Running tasks will finish but no new tasks will start.",
288
+ );
289
+ if (!ok) return;
290
+ haptic("medium");
291
+ const prev = cloneValue(executor);
292
+ await runOptimistic(
293
+ () => {
294
+ if (executorData.value)
295
+ executorData.value = { ...executorData.value, paused: true };
296
+ },
297
+ () => apiFetch("/api/executor/pause", { method: "POST" }),
298
+ () => {
299
+ executorData.value = prev;
300
+ },
301
+ ).catch(() => {});
302
+ scheduleRefresh(120);
303
+ };
304
+
305
+ const handleResume = async () => {
306
+ haptic("medium");
307
+ const prev = cloneValue(executor);
308
+ await runOptimistic(
309
+ () => {
310
+ if (executorData.value)
311
+ executorData.value = { ...executorData.value, paused: false };
312
+ },
313
+ () => apiFetch("/api/executor/resume", { method: "POST" }),
314
+ () => {
315
+ executorData.value = prev;
316
+ },
317
+ ).catch(() => {});
318
+ scheduleRefresh(120);
319
+ };
320
+
321
+ const handleMaxParallel = async (value) => {
322
+ setMaxParallel(value);
323
+ haptic();
324
+ const prev = cloneValue(executor);
325
+ await runOptimistic(
326
+ () => {
327
+ if (executorData.value?.data)
328
+ executorData.value.data.maxParallel = value;
329
+ },
330
+ () =>
331
+ apiFetch("/api/executor/maxparallel", {
332
+ method: "POST",
333
+ body: JSON.stringify({ value }),
334
+ }),
335
+ () => {
336
+ executorData.value = prev;
337
+ },
338
+ ).catch(() => {});
339
+ scheduleRefresh(120);
340
+ };
341
+
342
+ /* ── Region options from config ── */
343
+ const regions = config?.regions || ["auto"];
344
+ const regionOptions = regions.map((r) => ({
345
+ value: r,
346
+ label: r.charAt(0).toUpperCase() + r.slice(1),
347
+ }));
348
+
349
+ /* ── Quick command submit ── */
350
+ const handleQuickCmd = useCallback(() => {
351
+ const input = quickCmdInput.trim();
352
+ if (!input) {
353
+ setQuickCmdFeedbackTone("error");
354
+ setQuickCmdFeedback("Enter a command to run.");
355
+ return;
356
+ }
357
+ const cmd = `/${quickCmdPrefix} ${input}`;
358
+ sendCmd(cmd);
359
+ setQuickCmdInput("");
360
+ setQuickCmdFeedbackTone("success");
361
+ setQuickCmdFeedback("βœ“ Command sent to monitor");
362
+ setTimeout(() => setQuickCmdFeedback(""), 4000);
363
+ }, [quickCmdInput, quickCmdPrefix, sendCmd]);
364
+
365
+ /* ── Autocomplete select helper ── */
366
+ const selectAcItem = useCallback((item) => {
367
+ setCommandInput(item.cmd + ' ');
368
+ setShowAc(false);
369
+ setAcIndex(-1);
370
+ }, []);
371
+
372
+ /* ── Console input keydown handler ── */
373
+ const handleConsoleKeyDown = useCallback((e) => {
374
+ // Autocomplete navigation
375
+ if (showAc && acItems.length > 0) {
376
+ if (e.key === 'ArrowDown') {
377
+ e.preventDefault();
378
+ setAcIndex((prev) => (prev + 1) % acItems.length);
379
+ return;
380
+ }
381
+ if (e.key === 'ArrowUp') {
382
+ e.preventDefault();
383
+ setAcIndex((prev) => (prev <= 0 ? acItems.length - 1 : prev - 1));
384
+ return;
385
+ }
386
+ if (e.key === 'Enter' && acIndex >= 0) {
387
+ e.preventDefault();
388
+ selectAcItem(acItems[acIndex]);
389
+ return;
390
+ }
391
+ if (e.key === 'Escape') {
392
+ e.preventDefault();
393
+ setShowAc(false);
394
+ return;
395
+ }
396
+ }
397
+
398
+ // History navigation (when input is empty or already in history mode)
399
+ if (!showAc && (commandInput === '' || historyIndex >= 0)) {
400
+ if (e.key === 'ArrowUp' && cmdHistory.length > 0) {
401
+ e.preventDefault();
402
+ const nextIdx = historyIndex + 1;
403
+ if (nextIdx < cmdHistory.length) {
404
+ if (historyIndex === -1) savedInputRef.current = commandInput;
405
+ setHistoryIndex(nextIdx);
406
+ setCommandInput(cmdHistory[nextIdx]);
407
+ }
408
+ return;
409
+ }
410
+ if (e.key === 'ArrowDown' && historyIndex >= 0) {
411
+ e.preventDefault();
412
+ const nextIdx = historyIndex - 1;
413
+ if (nextIdx < 0) {
414
+ setHistoryIndex(-1);
415
+ setCommandInput(savedInputRef.current);
416
+ } else {
417
+ setHistoryIndex(nextIdx);
418
+ setCommandInput(cmdHistory[nextIdx]);
419
+ }
420
+ return;
421
+ }
422
+ }
423
+
424
+ // Submit
425
+ if (e.key === 'Enter' && commandInput.trim()) {
426
+ sendCmd(commandInput.trim());
427
+ setCommandInput('');
428
+ setShowAc(false);
429
+ }
430
+ }, [showAc, acItems, acIndex, commandInput, historyIndex, cmdHistory, sendCmd, selectAcItem]);
431
+
432
+ /* ── Toggle output accordion ── */
433
+ const toggleOutput = useCallback((idx) => {
434
+ setExpandedOutputs((prev) => ({ ...prev, [idx]: !prev[idx] }));
435
+ }, []);
436
+
437
+ const handleStartTask = useCallback(async () => {
438
+ const taskId = String(startTaskId || "").trim();
439
+ if (!taskId) {
440
+ setStartTaskError("Select a backlog task to start.");
441
+ showToast("Select a backlog task to start", "error");
442
+ return;
443
+ }
444
+ setStartTaskError("");
445
+ haptic("medium");
446
+ try {
447
+ await apiFetch("/api/tasks/start", {
448
+ method: "POST",
449
+ body: JSON.stringify({ taskId }),
450
+ });
451
+ showToast("Task started", "success");
452
+ refreshTaskOptions();
453
+ scheduleRefresh(150);
454
+ } catch {
455
+ /* toast via apiFetch */
456
+ }
457
+ }, [startTaskId, refreshTaskOptions]);
458
+
459
+ const handleRetryTask = useCallback(async () => {
460
+ const taskId = String(retryTaskId || "").trim();
461
+ if (!taskId) {
462
+ setRetryTaskError("Select a task to retry.");
463
+ showToast("Select a task to retry", "error");
464
+ return;
465
+ }
466
+ setRetryTaskError("");
467
+ haptic("medium");
468
+ try {
469
+ await apiFetch("/api/tasks/retry", {
470
+ method: "POST",
471
+ body: JSON.stringify({
472
+ taskId,
473
+ retryReason: retryReason.trim() || undefined,
474
+ }),
475
+ });
476
+ showToast("Task retried", "success");
477
+ setRetryReason("");
478
+ refreshTaskOptions();
479
+ scheduleRefresh(150);
480
+ } catch {
481
+ /* toast via apiFetch */
482
+ }
483
+ }, [retryTaskId, retryReason, refreshTaskOptions]);
484
+
485
+ return html`
486
+ <!-- Loading skeleton -->
487
+ ${!executor && !config && html`<${Card} title="Loading…"><${SkeletonCard} /><//>`}
488
+
489
+ <!-- ── Control Unit ── -->
490
+ <${Card}
491
+ title="Control Unit"
492
+ subtitle="Executor status and quick actions"
493
+ className="control-unit-card"
494
+ >
495
+ <div class="control-unit-body">
496
+ <div class="control-unit-meta">
497
+ <span>Mode <strong>${mode}</strong></span>
498
+ <span>Slots <strong>${slotsLabel}</strong></span>
499
+ <span>Poll <strong>${pollLabel}</strong></span>
500
+ <span>Timeout <strong>${timeoutLabel}</strong></span>
501
+ <span>Status ${
502
+ isPaused
503
+ ? html`<${Badge} status="error" text="Paused" />`
504
+ : html`<${Badge} status="done" text="Running" />`
505
+ }</span>
506
+ </div>
507
+ <div class="control-unit-actions">
508
+ <button class="btn btn-primary btn-sm" onClick=${handlePause}>
509
+ Pause Executor
510
+ </button>
511
+ <button class="btn btn-secondary btn-sm" onClick=${handleResume}>
512
+ Resume Executor
513
+ </button>
514
+ <button
515
+ class="btn btn-ghost btn-sm"
516
+ onClick=${() => sendCmd("/executor")}
517
+ title="Open executor menu"
518
+ >
519
+ /executor
520
+ </button>
521
+ </div>
522
+ </div>
523
+
524
+ <div class="form-label mt-sm">Max parallel tasks</div>
525
+ <div class="range-row mb-md">
526
+ <input
527
+ type="range"
528
+ min="0"
529
+ max="20"
530
+ step="1"
531
+ value=${maxParallel}
532
+ aria-label="Max parallel tasks"
533
+ onInput=${(e) => setMaxParallel(Number(e.target.value))}
534
+ onChange=${(e) => handleMaxParallel(Number(e.target.value))}
535
+ />
536
+ <span class="pill">Max ${maxParallel}</span>
537
+ </div>
538
+ <//>
539
+
540
+ <!-- ── Command Console ── -->
541
+ <${Card} title="Command Console">
542
+ <div class="input-row mb-sm">
543
+ <div style="position:relative;flex:1">
544
+ <input
545
+ class="input"
546
+ placeholder="/status"
547
+ value=${commandInput}
548
+ onInput=${(e) => {
549
+ setCommandInput(e.target.value);
550
+ setHistoryIndex(-1);
551
+ }}
552
+ onFocus=${() => setShowHistory(true)}
553
+ onBlur=${() => setTimeout(() => { setShowHistory(false); setShowAc(false); }, 200)}
554
+ onKeyDown=${handleConsoleKeyDown}
555
+ />
556
+ <!-- Autocomplete dropdown (above input) -->
557
+ ${showAc && acItems.length > 0 && html`
558
+ <div class="cmd-dropdown">
559
+ ${acItems.map((item, i) => html`
560
+ <div
561
+ key=${item.cmd}
562
+ class="cmd-dropdown-item${i === acIndex ? ' selected' : ''}"
563
+ onMouseDown=${(e) => { e.preventDefault(); selectAcItem(item); }}
564
+ onMouseEnter=${() => setAcIndex(i)}
565
+ >
566
+ <div>
567
+ <span style="font-weight:600;color:#e2e8f0">${item.cmd}</span>
568
+ <span style="margin-left:8px;color:#94a3b8;font-size:0.85em">${item.desc}</span>
569
+ </div>
570
+ <span style=${{
571
+ fontSize: '0.7rem', padding: '2px 8px', borderRadius: '9999px',
572
+ background: (CAT_COLORS[item.cat] || '#6366f1') + '33',
573
+ color: CAT_COLORS[item.cat] || '#6366f1', fontWeight: 600,
574
+ }}>${item.cat}</span>
575
+ </div>
576
+ `)}
577
+ </div>
578
+ `}
579
+ <!-- Command history dropdown (legacy, when no autocomplete) -->
580
+ ${!showAc && showHistory &&
581
+ cmdHistory.length > 0 &&
582
+ html`
583
+ <div class="cmd-history-dropdown">
584
+ ${cmdHistory.map(
585
+ (c, i) => html`
586
+ <button
587
+ key=${i}
588
+ class="cmd-history-item"
589
+ onMouseDown=${(e) => {
590
+ e.preventDefault();
591
+ setCommandInput(c);
592
+ setShowHistory(false);
593
+ }}
594
+ >
595
+ ${c}
596
+ </button>
597
+ `,
598
+ )}
599
+ </div>
600
+ `}
601
+ </div>
602
+ <button
603
+ class="btn btn-primary btn-sm"
604
+ onClick=${() => {
605
+ if (commandInput.trim()) {
606
+ sendCmd(commandInput.trim());
607
+ setCommandInput("");
608
+ }
609
+ }}
610
+ >
611
+ ${ICONS.send}
612
+ </button>
613
+ </div>
614
+
615
+ <!-- Quick command chips -->
616
+ <div class="btn-row">
617
+ ${["/status", "/health", "/menu", "/helpfull"].map(
618
+ (cmd) => html`
619
+ <button
620
+ key=${cmd}
621
+ class="btn btn-ghost btn-sm"
622
+ onClick=${() => sendCmd(cmd)}
623
+ >
624
+ ${cmd}
625
+ </button>
626
+ `,
627
+ )}
628
+ </div>
629
+
630
+ <!-- Running indicator -->
631
+ ${runningCmd && html`
632
+ <div style="margin-top:8px;display:flex;align-items:center;gap:8px;color:#94a3b8;font-size:0.85rem">
633
+ <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#facc15;animation:pulse 1s infinite"></span>
634
+ Running: <code style="color:#e2e8f0">${runningCmd}</code>
635
+ </div>
636
+ `}
637
+
638
+ <!-- Inline command outputs accordion -->
639
+ ${cmdOutputs.length > 0 && html`
640
+ <div style="margin-top:12px">
641
+ ${cmdOutputs.map((entry, idx) => html`
642
+ <div key=${idx} style="margin-bottom:6px;border:1px solid rgba(255,255,255,0.06);border-radius:8px;overflow:hidden">
643
+ <button
644
+ style="width:100%;text-align:left;padding:6px 12px;background:rgba(255,255,255,0.03);border:none;color:#cbd5e1;cursor:pointer;display:flex;justify-content:space-between;align-items:center;font-size:0.8rem"
645
+ onClick=${() => toggleOutput(idx)}
646
+ >
647
+ <span><code style="color:#818cf8">${entry.cmd}</code></span>
648
+ <span style="color:#64748b;font-size:0.75rem">${new Date(entry.ts).toLocaleTimeString()} ${expandedOutputs[idx] ? 'β–²' : 'β–Ό'}</span>
649
+ </button>
650
+ ${expandedOutputs[idx] && html`
651
+ <div class="cmd-output-panel">${entry.output}</div>
652
+ `}
653
+ </div>
654
+ `)}
655
+ </div>
656
+ `}
657
+ <//>
658
+
659
+ <!-- ── Task Ops ── -->
660
+ <${Card} title="Task Ops">
661
+ <div class="field-group">
662
+ <div class="form-label">Backlog task</div>
663
+ <div class="input-row">
664
+ <select
665
+ class=${startTaskError ? "input input-error" : "input"}
666
+ value=${startTaskId}
667
+ aria-label="Backlog task"
668
+ onChange=${(e) => {
669
+ setStartTaskId(e.target.value);
670
+ setStartTaskError("");
671
+ }}
672
+ >
673
+ <option value="">Select backlog task…</option>
674
+ ${backlogTasks.map(
675
+ (task) => html`
676
+ <option key=${task.id} value=${task.id}>
677
+ ${truncate(task.title || "(untitled)", 48)} Β· ${task.id}
678
+ </option>
679
+ `,
680
+ )}
681
+ </select>
682
+ <button
683
+ class="btn btn-secondary btn-sm"
684
+ disabled=${!startTaskId}
685
+ onClick=${handleStartTask}
686
+ >
687
+ Start Task
688
+ </button>
689
+ <button
690
+ class="btn btn-ghost btn-sm"
691
+ onClick=${refreshTaskOptions}
692
+ title="Refresh task list"
693
+ >
694
+ ↻
695
+ </button>
696
+ </div>
697
+ ${startTaskError
698
+ ? html`<div class="form-hint error">${startTaskError}</div>`
699
+ : null}
700
+ </div>
701
+ <div class="meta-text mb-sm">
702
+ ${tasksLoading
703
+ ? "Loading tasks…"
704
+ : `${backlogTasks.length} backlog Β· ${retryTasks.length} retryable`}
705
+ </div>
706
+ <div class="field-group">
707
+ <div class="form-label">Retry task</div>
708
+ <div class="input-row">
709
+ <select
710
+ class=${retryTaskError ? "input input-error" : "input"}
711
+ value=${retryTaskId}
712
+ aria-label="Retry task"
713
+ onChange=${(e) => {
714
+ setRetryTaskId(e.target.value);
715
+ setRetryTaskError("");
716
+ }}
717
+ >
718
+ <option value="">Select task to retry…</option>
719
+ ${retryTasks.map(
720
+ (task) => html`
721
+ <option key=${task.id} value=${task.id}>
722
+ ${truncate(task.title || "(untitled)", 48)} Β· ${task.id}
723
+ </option>
724
+ `,
725
+ )}
726
+ </select>
727
+ <input
728
+ class="input"
729
+ placeholder="Retry reason (optional)"
730
+ value=${retryReason}
731
+ onInput=${(e) => setRetryReason(e.target.value)}
732
+ />
733
+ <button
734
+ class="btn btn-secondary btn-sm"
735
+ disabled=${!retryTaskId}
736
+ onClick=${handleRetryTask}
737
+ >
738
+ Retry Task
739
+ </button>
740
+ <button class="btn btn-ghost btn-sm" onClick=${() => sendCmd("/plan")}>
741
+ πŸ“‹ Plan
742
+ </button>
743
+ </div>
744
+ ${retryTaskError
745
+ ? html`<div class="form-hint error">${retryTaskError}</div>`
746
+ : null}
747
+ </div>
748
+ <//>
749
+
750
+ <!-- ── Agent Control ── -->
751
+ <${Card} title="Agent Control">
752
+ <div class="form-label">Ask agent</div>
753
+ <textarea
754
+ class="input mb-sm"
755
+ rows="2"
756
+ placeholder="Ask the agent…"
757
+ value=${askInput}
758
+ onInput=${(e) => setAskInput(e.target.value)}
759
+ ></textarea>
760
+ <div class="btn-row mb-md">
761
+ <button
762
+ class="btn btn-primary btn-sm"
763
+ onClick=${() => {
764
+ if (askInput.trim()) {
765
+ sendCmd(`/ask ${askInput.trim()}`);
766
+ setAskInput("");
767
+ }
768
+ }}
769
+ >
770
+ πŸ’¬ Ask
771
+ </button>
772
+ </div>
773
+ <div class="form-label">Steer prompt</div>
774
+ <div class="input-row mb-sm">
775
+ <input
776
+ class="input"
777
+ placeholder="Steer prompt (focus on…)"
778
+ value=${steerInput}
779
+ onInput=${(e) => setSteerInput(e.target.value)}
780
+ />
781
+ <button
782
+ class="btn btn-secondary btn-sm"
783
+ onClick=${() => {
784
+ if (steerInput.trim()) {
785
+ sendCmd(`/steer ${steerInput.trim()}`);
786
+ setSteerInput("");
787
+ }
788
+ }}
789
+ >
790
+ 🎯 Steer
791
+ </button>
792
+ </div>
793
+ <//>
794
+
795
+ <!-- ── Routing ── -->
796
+ <${Card} title="Routing">
797
+ <div class="card-subtitle">SDK</div>
798
+ <${SegmentedControl}
799
+ options=${[
800
+ { value: "codex", label: "Codex" },
801
+ { value: "copilot", label: "Copilot" },
802
+ { value: "claude", label: "Claude" },
803
+ { value: "auto", label: "Auto" },
804
+ ]}
805
+ value=${config?.sdk || "auto"}
806
+ onChange=${(v) => updateConfig("sdk", v)}
807
+ />
808
+ <div class="card-subtitle mt-sm">Kanban</div>
809
+ <${SegmentedControl}
810
+ options=${[
811
+ { value: "vk", label: "VK" },
812
+ { value: "github", label: "GitHub" },
813
+ { value: "jira", label: "Jira" },
814
+ ]}
815
+ value=${config?.kanbanBackend || "github"}
816
+ onChange=${(v) => updateConfig("kanban", v)}
817
+ />
818
+ ${regions.length > 1 && html`
819
+ <div class="card-subtitle mt-sm">Region</div>
820
+ <${SegmentedControl}
821
+ options=${regionOptions}
822
+ value=${regions[0]}
823
+ onChange=${(v) => updateConfig("region", v)}
824
+ />
825
+ `}
826
+ <//>
827
+
828
+ <!-- ── Quick Commands ── -->
829
+ <${Card} title="Quick Commands">
830
+ <div class="form-label">Command</div>
831
+ <div class="input-row mb-sm">
832
+ <select
833
+ class="input"
834
+ style="flex:0 0 auto;width:80px"
835
+ value=${quickCmdPrefix}
836
+ onChange=${(e) => setQuickCmdPrefix(e.target.value)}
837
+ >
838
+ <option value="shell">Shell</option>
839
+ <option value="git">Git</option>
840
+ </select>
841
+ <input
842
+ class="input"
843
+ placeholder=${quickCmdPrefix === "shell" ? "ls -la" : "status --short"}
844
+ value=${quickCmdInput}
845
+ onInput=${(e) => {
846
+ setQuickCmdInput(e.target.value);
847
+ if (quickCmdFeedbackTone === "error") setQuickCmdFeedback("");
848
+ }}
849
+ onKeyDown=${(e) => {
850
+ if (e.key === "Enter") handleQuickCmd();
851
+ }}
852
+ style="flex:1"
853
+ />
854
+ <button class="btn btn-secondary btn-sm" onClick=${handleQuickCmd}>
855
+ β–Ά Run
856
+ </button>
857
+ </div>
858
+ ${quickCmdFeedback && html`
859
+ <div class="form-hint ${quickCmdFeedbackTone === "error" ? "error" : "success"} mb-sm">
860
+ ${quickCmdFeedback}
861
+ </div>
862
+ `}
863
+ <div class="meta-text">
864
+ Output appears in agent logs. ${""}
865
+ <a
866
+ href="#"
867
+ style="color:var(--tg-theme-link-color,#4ea8d6);text-decoration:underline;cursor:pointer"
868
+ onClick=${(e) => {
869
+ e.preventDefault();
870
+ import("../modules/router.js").then(({ navigateTo }) => navigateTo("logs"));
871
+ }}
872
+ >Open Logs tab β†’</a>
873
+ </div>
874
+ <//>
875
+
876
+ <!-- Inline styles for new elements -->
877
+ <style>
878
+ .cmd-dropdown { position: absolute; bottom: 100%; left: 0; right: 0; background: var(--glass-bg, rgba(15,23,42,0.9)); border: 1px solid var(--glass-border, rgba(255,255,255,0.08)); border-radius: 12px; max-height: 240px; overflow-y: auto; z-index: 50; backdrop-filter: blur(12px); }
879
+ .cmd-dropdown-item { padding: 8px 12px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
880
+ .cmd-dropdown-item.selected { background: rgba(99,102,241,0.2); }
881
+ .cmd-dropdown-item:hover { background: rgba(99,102,241,0.15); }
882
+ .cmd-output-panel { margin-top: 0; background: rgba(0,0,0,0.4); border-radius: 0 0 8px 8px; padding: 8px 12px; font-family: monospace; font-size: 0.8rem; color: #4ade80; max-height: 200px; overflow-y: auto; white-space: pre-wrap; }
883
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
884
+ </style>
885
+
886
+ `;
887
+ }