bosun 0.26.3

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