@virtengine/openfleet 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,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
|
+
}
|