@femtomc/mu-agent 26.2.99 → 26.2.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/extensions/hud.d.ts +4 -0
- package/dist/extensions/hud.d.ts.map +1 -0
- package/dist/extensions/hud.js +282 -0
- package/dist/extensions/index.d.ts +1 -2
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +1 -2
- package/dist/extensions/mu-operator.d.ts.map +1 -1
- package/dist/extensions/mu-operator.js +2 -4
- package/dist/extensions/mu-serve.d.ts.map +1 -1
- package/dist/extensions/mu-serve.js +2 -4
- package/dist/operator.d.ts +155 -0
- package/dist/operator.d.ts.map +1 -1
- package/dist/operator.js +66 -2
- package/package.json +2 -2
- package/prompts/skills/heartbeats/SKILL.md +30 -5
- package/prompts/skills/hierarchical-work-protocol/SKILL.md +2 -1
- package/prompts/skills/hud/SKILL.md +169 -0
- package/prompts/skills/mu/SKILL.md +1 -1
- package/prompts/skills/planning/SKILL.md +42 -45
- package/prompts/skills/subagents/SKILL.md +39 -16
- package/dist/extensions/hud-mode.d.ts +0 -8
- package/dist/extensions/hud-mode.d.ts.map +0 -1
- package/dist/extensions/hud-mode.js +0 -21
- package/dist/extensions/planning-ui.d.ts +0 -4
- package/dist/extensions/planning-ui.d.ts.map +0 -1
- package/dist/extensions/planning-ui.js +0 -866
- package/dist/extensions/subagents-ui.d.ts +0 -4
- package/dist/extensions/subagents-ui.d.ts.map +0 -1
- package/dist/extensions/subagents-ui.js +0 -1409
|
@@ -1,1409 +0,0 @@
|
|
|
1
|
-
import { fetchMuJson, muServerUrl } from "./shared.js";
|
|
2
|
-
import { clearHudMode, setActiveHudMode, syncHudModeStatus } from "./hud-mode.js";
|
|
3
|
-
import { registerMuSubcommand } from "./mu-command-dispatcher.js";
|
|
4
|
-
const DEFAULT_PREFIX = "mu-sub-";
|
|
5
|
-
const DEFAULT_ISSUE_TAG_FILTER = null;
|
|
6
|
-
const DEFAULT_SPAWN_MODE = "operator";
|
|
7
|
-
const ISSUE_LIST_LIMIT = 40;
|
|
8
|
-
const MU_CLI_TIMEOUT_MS = 12_000;
|
|
9
|
-
const DEFAULT_REFRESH_INTERVAL_MS = 8_000;
|
|
10
|
-
const MIN_REFRESH_SECONDS = 2;
|
|
11
|
-
const MAX_REFRESH_SECONDS = 120;
|
|
12
|
-
const DEFAULT_STALE_AFTER_MS = 60_000;
|
|
13
|
-
const MIN_STALE_SECONDS = 10;
|
|
14
|
-
const MAX_STALE_SECONDS = 3_600;
|
|
15
|
-
const WIDGET_SCOPE_MAX = 52;
|
|
16
|
-
const WIDGET_PREFIX_MAX = 20;
|
|
17
|
-
const WIDGET_SUMMARY_MAX = 76;
|
|
18
|
-
const WIDGET_ERROR_MAX = 72;
|
|
19
|
-
const ACTIVITY_EVENT_LIMIT = 180;
|
|
20
|
-
const ACTIVITY_LINE_LIMIT = 4;
|
|
21
|
-
const FORUM_ACTIVITY_ISSUE_LIMIT = 8;
|
|
22
|
-
function shellQuote(value) {
|
|
23
|
-
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
24
|
-
}
|
|
25
|
-
function spawnRunId(now = new Date()) {
|
|
26
|
-
return now
|
|
27
|
-
.toISOString()
|
|
28
|
-
.replaceAll(/[-:TZ.]/g, "")
|
|
29
|
-
.slice(0, 14);
|
|
30
|
-
}
|
|
31
|
-
function sessionMatchesIssue(sessionName, issueId) {
|
|
32
|
-
return (sessionName === issueId ||
|
|
33
|
-
sessionName.endsWith(`-${issueId}`) ||
|
|
34
|
-
sessionName.includes(`-${issueId}-`) ||
|
|
35
|
-
sessionName.includes(`_${issueId}`));
|
|
36
|
-
}
|
|
37
|
-
function issueHasSession(sessions, issueId) {
|
|
38
|
-
return sessions.some((sessionName) => sessionMatchesIssue(sessionName, issueId));
|
|
39
|
-
}
|
|
40
|
-
function buildSubagentPrompt(issue, mode) {
|
|
41
|
-
switch (mode) {
|
|
42
|
-
case "operator":
|
|
43
|
-
return [
|
|
44
|
-
`Work issue ${issue.id} (${truncateOneLine(issue.title, 80)}).`,
|
|
45
|
-
`First run: mu issues claim ${issue.id}.`,
|
|
46
|
-
`Keep forum updates in topic issue:${issue.id}.`,
|
|
47
|
-
"When done, close with an explicit outcome and summary.",
|
|
48
|
-
].join(" ");
|
|
49
|
-
case "researcher":
|
|
50
|
-
return [
|
|
51
|
-
`Research issue ${issue.id} (${truncateOneLine(issue.title, 80)}).`,
|
|
52
|
-
`First run: mu issues claim ${issue.id}.`,
|
|
53
|
-
"Collect concrete evidence and options; call out uncertainty explicitly.",
|
|
54
|
-
`Keep findings in topic issue:${issue.id}.`,
|
|
55
|
-
"Close the issue with a concise recommendation and rationale.",
|
|
56
|
-
].join(" ");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
async function spawnIssueTmuxSession(opts) {
|
|
60
|
-
const shellCommand = `cd ${shellQuote(opts.cwd)} && mu exec ${shellQuote(buildSubagentPrompt(opts.issue, opts.mode))} ; rc=$?; echo __MU_DONE__:$rc`;
|
|
61
|
-
let proc = null;
|
|
62
|
-
try {
|
|
63
|
-
proc = Bun.spawn({
|
|
64
|
-
cmd: ["tmux", "new-session", "-d", "-s", opts.sessionName, shellCommand],
|
|
65
|
-
stdin: "ignore",
|
|
66
|
-
stdout: "pipe",
|
|
67
|
-
stderr: "pipe",
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
-
return { ok: false, error: `failed to launch tmux session: ${message}` };
|
|
73
|
-
}
|
|
74
|
-
const [exitCode, stderr] = await Promise.all([proc.exited, readableText(proc.stderr)]);
|
|
75
|
-
if (exitCode !== 0) {
|
|
76
|
-
const detail = stderr.trim();
|
|
77
|
-
return { ok: false, error: detail.length > 0 ? detail : `tmux exited ${exitCode}` };
|
|
78
|
-
}
|
|
79
|
-
return { ok: true, error: null };
|
|
80
|
-
}
|
|
81
|
-
function readableText(stream) {
|
|
82
|
-
if (stream && typeof stream === "object" && "getReader" in stream) {
|
|
83
|
-
return new Response(stream).text().catch(() => "");
|
|
84
|
-
}
|
|
85
|
-
return Promise.resolve("");
|
|
86
|
-
}
|
|
87
|
-
function createDefaultState() {
|
|
88
|
-
return {
|
|
89
|
-
enabled: false,
|
|
90
|
-
prefix: DEFAULT_PREFIX,
|
|
91
|
-
sessions: [],
|
|
92
|
-
sessionError: null,
|
|
93
|
-
issueRootId: null,
|
|
94
|
-
issueTagFilter: DEFAULT_ISSUE_TAG_FILTER,
|
|
95
|
-
readyIssues: [],
|
|
96
|
-
activeIssues: [],
|
|
97
|
-
issueError: null,
|
|
98
|
-
lastUpdatedMs: null,
|
|
99
|
-
refreshIntervalMs: DEFAULT_REFRESH_INTERVAL_MS,
|
|
100
|
-
staleAfterMs: DEFAULT_STALE_AFTER_MS,
|
|
101
|
-
spawnPaused: false,
|
|
102
|
-
spawnMode: DEFAULT_SPAWN_MODE,
|
|
103
|
-
activityLines: [],
|
|
104
|
-
activityError: null,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
function truncateOneLine(input, maxLen = 68) {
|
|
108
|
-
const compact = input.replace(/\s+/g, " ").trim();
|
|
109
|
-
if (compact.length <= maxLen) {
|
|
110
|
-
return compact;
|
|
111
|
-
}
|
|
112
|
-
return `${compact.slice(0, Math.max(0, maxLen - 1))}…`;
|
|
113
|
-
}
|
|
114
|
-
function summarizeFailure(label, outcome) {
|
|
115
|
-
if (outcome.error) {
|
|
116
|
-
return `${label}: ${outcome.error}`;
|
|
117
|
-
}
|
|
118
|
-
if (outcome.timedOut) {
|
|
119
|
-
return `${label}: timed out after ${MU_CLI_TIMEOUT_MS}ms`;
|
|
120
|
-
}
|
|
121
|
-
const stderr = outcome.stderr.trim();
|
|
122
|
-
if (stderr.length > 0) {
|
|
123
|
-
return `${label}: ${stderr}`;
|
|
124
|
-
}
|
|
125
|
-
const stdout = outcome.stdout.trim();
|
|
126
|
-
if (stdout.length > 0) {
|
|
127
|
-
return `${label}: ${truncateOneLine(stdout, 120)}`;
|
|
128
|
-
}
|
|
129
|
-
return `${label}: exit ${outcome.exitCode}`;
|
|
130
|
-
}
|
|
131
|
-
function normalizeIssueDigest(row) {
|
|
132
|
-
if (!row || typeof row !== "object") {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
const value = row;
|
|
136
|
-
const id = typeof value.id === "string" ? value.id.trim() : "";
|
|
137
|
-
const title = typeof value.title === "string" ? value.title : "";
|
|
138
|
-
const status = value.status;
|
|
139
|
-
const priorityRaw = value.priority;
|
|
140
|
-
const tagsRaw = value.tags;
|
|
141
|
-
if (!id || !title) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
if (status !== "open" && status !== "in_progress" && status !== "closed") {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
const priority = typeof priorityRaw === "number" && Number.isFinite(priorityRaw) ? Math.trunc(priorityRaw) : 3;
|
|
148
|
-
const tags = Array.isArray(tagsRaw) ? tagsRaw.filter((tag) => typeof tag === "string") : [];
|
|
149
|
-
return {
|
|
150
|
-
id,
|
|
151
|
-
title,
|
|
152
|
-
status,
|
|
153
|
-
priority,
|
|
154
|
-
tags,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function parseIssueArray(label, jsonText) {
|
|
158
|
-
const trimmed = jsonText.trim();
|
|
159
|
-
if (trimmed.length === 0) {
|
|
160
|
-
return { issues: [], error: null };
|
|
161
|
-
}
|
|
162
|
-
let parsed = null;
|
|
163
|
-
try {
|
|
164
|
-
parsed = JSON.parse(trimmed);
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
return { issues: [], error: `${label}: invalid JSON output from mu issues command` };
|
|
168
|
-
}
|
|
169
|
-
if (!Array.isArray(parsed)) {
|
|
170
|
-
return { issues: [], error: `${label}: expected JSON array output from mu issues command` };
|
|
171
|
-
}
|
|
172
|
-
const issues = parsed.map(normalizeIssueDigest).filter((issue) => issue !== null);
|
|
173
|
-
issues.sort((left, right) => {
|
|
174
|
-
if (left.priority !== right.priority) {
|
|
175
|
-
return left.priority - right.priority;
|
|
176
|
-
}
|
|
177
|
-
return left.id.localeCompare(right.id);
|
|
178
|
-
});
|
|
179
|
-
return { issues, error: null };
|
|
180
|
-
}
|
|
181
|
-
function parseForumReadLatest(label, jsonText) {
|
|
182
|
-
const trimmed = jsonText.trim();
|
|
183
|
-
if (trimmed.length === 0) {
|
|
184
|
-
return { message: null, error: null };
|
|
185
|
-
}
|
|
186
|
-
let parsed = null;
|
|
187
|
-
try {
|
|
188
|
-
parsed = JSON.parse(trimmed);
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return { message: null, error: `${label}: invalid JSON output from mu forum read` };
|
|
192
|
-
}
|
|
193
|
-
if (!Array.isArray(parsed)) {
|
|
194
|
-
return { message: null, error: `${label}: expected JSON array output from mu forum read` };
|
|
195
|
-
}
|
|
196
|
-
const first = parsed[0];
|
|
197
|
-
if (!first || typeof first !== "object" || Array.isArray(first)) {
|
|
198
|
-
return { message: null, error: null };
|
|
199
|
-
}
|
|
200
|
-
const row = first;
|
|
201
|
-
const body = typeof row.body === "string" ? row.body.trim() : "";
|
|
202
|
-
if (body.length === 0) {
|
|
203
|
-
return { message: null, error: null };
|
|
204
|
-
}
|
|
205
|
-
const authorRaw = typeof row.author === "string" ? row.author.trim() : "";
|
|
206
|
-
const createdAtRaw = row.created_at;
|
|
207
|
-
const createdAtSec = typeof createdAtRaw === "number" && Number.isFinite(createdAtRaw)
|
|
208
|
-
? Math.trunc(createdAtRaw)
|
|
209
|
-
: typeof createdAtRaw === "string" && /^\d+$/.test(createdAtRaw.trim())
|
|
210
|
-
? Number.parseInt(createdAtRaw.trim(), 10)
|
|
211
|
-
: null;
|
|
212
|
-
return {
|
|
213
|
-
message: {
|
|
214
|
-
author: authorRaw.length > 0 ? authorRaw : "operator",
|
|
215
|
-
body,
|
|
216
|
-
createdAtMs: createdAtSec != null && Number.isFinite(createdAtSec) ? createdAtSec * 1_000 : null,
|
|
217
|
-
},
|
|
218
|
-
error: null,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
async function runMuCli(args) {
|
|
222
|
-
let proc = null;
|
|
223
|
-
try {
|
|
224
|
-
proc = Bun.spawn({
|
|
225
|
-
cmd: ["mu", ...args],
|
|
226
|
-
stdin: "ignore",
|
|
227
|
-
stdout: "pipe",
|
|
228
|
-
stderr: "pipe",
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
catch (err) {
|
|
232
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
233
|
-
return {
|
|
234
|
-
exitCode: 1,
|
|
235
|
-
stdout: "",
|
|
236
|
-
stderr: "",
|
|
237
|
-
timedOut: false,
|
|
238
|
-
error: `failed to launch mu CLI (${message})`,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
let timedOut = false;
|
|
242
|
-
const timeout = setTimeout(() => {
|
|
243
|
-
timedOut = true;
|
|
244
|
-
proc?.kill();
|
|
245
|
-
}, MU_CLI_TIMEOUT_MS);
|
|
246
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
247
|
-
proc.exited,
|
|
248
|
-
readableText(proc.stdout),
|
|
249
|
-
readableText(proc.stderr),
|
|
250
|
-
]);
|
|
251
|
-
clearTimeout(timeout);
|
|
252
|
-
return {
|
|
253
|
-
exitCode,
|
|
254
|
-
stdout,
|
|
255
|
-
stderr,
|
|
256
|
-
timedOut,
|
|
257
|
-
error: null,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
async function listTmuxSessions(prefix) {
|
|
261
|
-
let proc = null;
|
|
262
|
-
try {
|
|
263
|
-
proc = Bun.spawn({
|
|
264
|
-
cmd: ["tmux", "ls"],
|
|
265
|
-
stdin: "ignore",
|
|
266
|
-
stdout: "pipe",
|
|
267
|
-
stderr: "pipe",
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
272
|
-
return { sessions: [], error: `failed to launch tmux: ${message}` };
|
|
273
|
-
}
|
|
274
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
275
|
-
proc.exited,
|
|
276
|
-
readableText(proc.stdout),
|
|
277
|
-
readableText(proc.stderr),
|
|
278
|
-
]);
|
|
279
|
-
const stderrTrimmed = stderr.trim();
|
|
280
|
-
if (exitCode !== 0) {
|
|
281
|
-
const lowered = stderrTrimmed.toLowerCase();
|
|
282
|
-
if (lowered.includes("no server running") || lowered.includes("failed to connect to server")) {
|
|
283
|
-
return { sessions: [], error: null };
|
|
284
|
-
}
|
|
285
|
-
const detail = stderrTrimmed.length > 0 ? stderrTrimmed : `tmux exited ${exitCode}`;
|
|
286
|
-
return { sessions: [], error: detail };
|
|
287
|
-
}
|
|
288
|
-
const sessions = stdout
|
|
289
|
-
.split("\n")
|
|
290
|
-
.map((line) => line.trim())
|
|
291
|
-
.filter((line) => line.length > 0)
|
|
292
|
-
.map((line) => {
|
|
293
|
-
const colon = line.indexOf(":");
|
|
294
|
-
return colon >= 0 ? line.slice(0, colon).trim() : line;
|
|
295
|
-
})
|
|
296
|
-
.filter((name) => name.length > 0)
|
|
297
|
-
.filter((name) => (prefix.length > 0 ? name.startsWith(prefix) : true))
|
|
298
|
-
.sort((left, right) => left.localeCompare(right));
|
|
299
|
-
return { sessions, error: null };
|
|
300
|
-
}
|
|
301
|
-
async function listIssueSlices(rootId, tagFilter) {
|
|
302
|
-
const readyArgs = ["issues", "ready", "--json", "--limit", String(ISSUE_LIST_LIMIT)];
|
|
303
|
-
const activeArgs = ["issues", "list", "--status", "in_progress", "--json", "--limit", String(ISSUE_LIST_LIMIT)];
|
|
304
|
-
if (rootId) {
|
|
305
|
-
readyArgs.push("--root", rootId);
|
|
306
|
-
activeArgs.push("--root", rootId);
|
|
307
|
-
}
|
|
308
|
-
if (tagFilter) {
|
|
309
|
-
readyArgs.push("--tag", tagFilter);
|
|
310
|
-
activeArgs.push("--tag", tagFilter);
|
|
311
|
-
}
|
|
312
|
-
const [readyOutcome, activeOutcome] = await Promise.all([runMuCli(readyArgs), runMuCli(activeArgs)]);
|
|
313
|
-
if (readyOutcome.exitCode !== 0 || readyOutcome.error || readyOutcome.timedOut) {
|
|
314
|
-
return {
|
|
315
|
-
ready: [],
|
|
316
|
-
active: [],
|
|
317
|
-
error: summarizeFailure("ready", readyOutcome),
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
if (activeOutcome.exitCode !== 0 || activeOutcome.error || activeOutcome.timedOut) {
|
|
321
|
-
return {
|
|
322
|
-
ready: [],
|
|
323
|
-
active: [],
|
|
324
|
-
error: summarizeFailure("in-progress", activeOutcome),
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
const readyParsed = parseIssueArray("ready", readyOutcome.stdout);
|
|
328
|
-
if (readyParsed.error) {
|
|
329
|
-
return { ready: [], active: [], error: readyParsed.error };
|
|
330
|
-
}
|
|
331
|
-
const activeParsed = parseIssueArray("in-progress", activeOutcome.stdout);
|
|
332
|
-
if (activeParsed.error) {
|
|
333
|
-
return { ready: [], active: [], error: activeParsed.error };
|
|
334
|
-
}
|
|
335
|
-
return {
|
|
336
|
-
ready: readyParsed.issues,
|
|
337
|
-
active: activeParsed.issues,
|
|
338
|
-
error: null,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function queueMeter(value, total, width = 10) {
|
|
342
|
-
if (width <= 0 || total <= 0) {
|
|
343
|
-
return "";
|
|
344
|
-
}
|
|
345
|
-
const clamped = Math.max(0, Math.min(total, value));
|
|
346
|
-
const full = Math.floor((clamped / total) * width);
|
|
347
|
-
const empty = Math.max(0, width - full);
|
|
348
|
-
return "█".repeat(full) + "░".repeat(empty);
|
|
349
|
-
}
|
|
350
|
-
function formatRefreshAge(lastUpdatedMs) {
|
|
351
|
-
if (lastUpdatedMs == null) {
|
|
352
|
-
return "never";
|
|
353
|
-
}
|
|
354
|
-
const deltaSec = Math.max(0, Math.round((Date.now() - lastUpdatedMs) / 1000));
|
|
355
|
-
if (deltaSec < 60) {
|
|
356
|
-
return `${deltaSec}s ago`;
|
|
357
|
-
}
|
|
358
|
-
const mins = Math.floor(deltaSec / 60);
|
|
359
|
-
if (mins < 60) {
|
|
360
|
-
return `${mins}m ago`;
|
|
361
|
-
}
|
|
362
|
-
const hours = Math.floor(mins / 60);
|
|
363
|
-
return `${hours}h ago`;
|
|
364
|
-
}
|
|
365
|
-
function isRefreshStale(lastUpdatedMs, staleAfterMs) {
|
|
366
|
-
if (lastUpdatedMs == null) {
|
|
367
|
-
return false;
|
|
368
|
-
}
|
|
369
|
-
return Date.now() - lastUpdatedMs > staleAfterMs;
|
|
370
|
-
}
|
|
371
|
-
function asRecord(value) {
|
|
372
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
return value;
|
|
376
|
-
}
|
|
377
|
-
function issueIdFromEvent(event) {
|
|
378
|
-
const issueId = typeof event.issue_id === "string" ? event.issue_id.trim() : "";
|
|
379
|
-
return issueId.length > 0 ? issueId : null;
|
|
380
|
-
}
|
|
381
|
-
function eventAgeLabel(tsMs) {
|
|
382
|
-
if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) {
|
|
383
|
-
return "now";
|
|
384
|
-
}
|
|
385
|
-
const ageSeconds = Math.max(0, Math.round((Date.now() - tsMs) / 1_000));
|
|
386
|
-
if (ageSeconds < 60) {
|
|
387
|
-
return `${ageSeconds}s`;
|
|
388
|
-
}
|
|
389
|
-
const mins = Math.floor(ageSeconds / 60);
|
|
390
|
-
if (mins < 60) {
|
|
391
|
-
return `${mins}m`;
|
|
392
|
-
}
|
|
393
|
-
const hours = Math.floor(mins / 60);
|
|
394
|
-
return `${hours}h`;
|
|
395
|
-
}
|
|
396
|
-
function renderActivitySentence(event) {
|
|
397
|
-
const issueId = issueIdFromEvent(event);
|
|
398
|
-
if (!issueId) {
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
const eventType = typeof event.type === "string" ? event.type : "";
|
|
402
|
-
const payload = asRecord(event.payload);
|
|
403
|
-
if (eventType === "forum.post") {
|
|
404
|
-
const message = asRecord(payload?.message);
|
|
405
|
-
const body = typeof message?.body === "string" ? message.body.trim() : "";
|
|
406
|
-
const author = typeof message?.author === "string" ? message.author.trim() : "operator";
|
|
407
|
-
if (body.length === 0) {
|
|
408
|
-
return null;
|
|
409
|
-
}
|
|
410
|
-
return {
|
|
411
|
-
issueId,
|
|
412
|
-
sentence: `${issueId} ${author}: ${truncateOneLine(body, 54)}`,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
if (eventType === "issue.claim") {
|
|
416
|
-
const ok = payload?.ok === true;
|
|
417
|
-
if (ok) {
|
|
418
|
-
return { issueId, sentence: `${issueId} claimed and started work` };
|
|
419
|
-
}
|
|
420
|
-
const reason = typeof payload?.reason === "string" ? payload.reason : "claim failed";
|
|
421
|
-
return { issueId, sentence: `${issueId} claim failed (${truncateOneLine(reason, 36)})` };
|
|
422
|
-
}
|
|
423
|
-
if (eventType === "issue.close") {
|
|
424
|
-
const outcome = typeof payload?.outcome === "string" ? payload.outcome : "closed";
|
|
425
|
-
return { issueId, sentence: `${issueId} closed (${outcome})` };
|
|
426
|
-
}
|
|
427
|
-
if (eventType === "issue.update") {
|
|
428
|
-
const changed = asRecord(payload?.changed);
|
|
429
|
-
const changedKeys = changed ? Object.keys(changed) : [];
|
|
430
|
-
if (changedKeys.includes("status")) {
|
|
431
|
-
const statusChange = asRecord(changed?.status);
|
|
432
|
-
const from = typeof statusChange?.from === "string" ? statusChange.from : "?";
|
|
433
|
-
const to = typeof statusChange?.to === "string" ? statusChange.to : "?";
|
|
434
|
-
return { issueId, sentence: `${issueId} status ${from} → ${to}` };
|
|
435
|
-
}
|
|
436
|
-
if (changedKeys.length > 0) {
|
|
437
|
-
return {
|
|
438
|
-
issueId,
|
|
439
|
-
sentence: `${issueId} updated ${truncateOneLine(changedKeys.join(","), 28)}`,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
if (eventType === "issue.open") {
|
|
444
|
-
return { issueId, sentence: `${issueId} reopened` };
|
|
445
|
-
}
|
|
446
|
-
return null;
|
|
447
|
-
}
|
|
448
|
-
function isActivityEndpointUnavailable(errorMessage) {
|
|
449
|
-
const normalized = errorMessage.toLowerCase();
|
|
450
|
-
return normalized.includes("mu server 404") && normalized.includes("not found");
|
|
451
|
-
}
|
|
452
|
-
async function fetchRecentForumActivity(issueIds) {
|
|
453
|
-
const uniqueIssueIds = Array.from(new Set(issueIds.map((issueId) => issueId.trim()).filter((issueId) => issueId.length > 0))).slice(0, FORUM_ACTIVITY_ISSUE_LIMIT);
|
|
454
|
-
if (uniqueIssueIds.length === 0) {
|
|
455
|
-
return [];
|
|
456
|
-
}
|
|
457
|
-
const reads = await Promise.all(uniqueIssueIds.map(async (issueId) => {
|
|
458
|
-
const outcome = await runMuCli(["forum", "read", `issue:${issueId}`, "--limit", "1", "--json"]);
|
|
459
|
-
if (outcome.exitCode !== 0 || outcome.error || outcome.timedOut) {
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
const parsed = parseForumReadLatest(`forum:${issueId}`, outcome.stdout);
|
|
463
|
-
if (parsed.error || !parsed.message) {
|
|
464
|
-
return null;
|
|
465
|
-
}
|
|
466
|
-
return {
|
|
467
|
-
issueId,
|
|
468
|
-
tsMs: parsed.message.createdAtMs ?? 0,
|
|
469
|
-
line: `${eventAgeLabel(parsed.message.createdAtMs ?? undefined)} ${issueId} ${parsed.message.author}: ${truncateOneLine(parsed.message.body, 54)}`,
|
|
470
|
-
};
|
|
471
|
-
}));
|
|
472
|
-
return reads
|
|
473
|
-
.filter((row) => row != null)
|
|
474
|
-
.sort((left, right) => right.tsMs - left.tsMs)
|
|
475
|
-
.slice(0, ACTIVITY_LINE_LIMIT)
|
|
476
|
-
.map((row) => row.line);
|
|
477
|
-
}
|
|
478
|
-
async function fetchRecentActivity(opts) {
|
|
479
|
-
let endpointError = null;
|
|
480
|
-
const hasServerUrl = Boolean(muServerUrl());
|
|
481
|
-
if (hasServerUrl) {
|
|
482
|
-
let events;
|
|
483
|
-
try {
|
|
484
|
-
events = await fetchMuJson(`/api/control-plane/events?limit=${ACTIVITY_EVENT_LIMIT}`, {
|
|
485
|
-
timeoutMs: 4_000,
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
catch (err) {
|
|
489
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
490
|
-
if (!isActivityEndpointUnavailable(message)) {
|
|
491
|
-
endpointError = `activity refresh failed: ${truncateOneLine(message, 60)}`;
|
|
492
|
-
}
|
|
493
|
-
events = [];
|
|
494
|
-
}
|
|
495
|
-
if (Array.isArray(events) && events.length > 0) {
|
|
496
|
-
const tracked = new Set(opts.issueIds.map((issueId) => issueId.trim()).filter((issueId) => issueId.length > 0));
|
|
497
|
-
const seenIssueIds = new Set();
|
|
498
|
-
const lines = [];
|
|
499
|
-
const sorted = [...events].sort((left, right) => {
|
|
500
|
-
const leftTs = typeof left.ts_ms === "number" ? left.ts_ms : 0;
|
|
501
|
-
const rightTs = typeof right.ts_ms === "number" ? right.ts_ms : 0;
|
|
502
|
-
return rightTs - leftTs;
|
|
503
|
-
});
|
|
504
|
-
for (const event of sorted) {
|
|
505
|
-
const rendered = renderActivitySentence(event);
|
|
506
|
-
if (!rendered) {
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
if (tracked.size > 0 && !tracked.has(rendered.issueId)) {
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
if (seenIssueIds.has(rendered.issueId)) {
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
seenIssueIds.add(rendered.issueId);
|
|
516
|
-
lines.push(`${eventAgeLabel(event.ts_ms)} ${rendered.sentence}`);
|
|
517
|
-
if (lines.length >= ACTIVITY_LINE_LIMIT) {
|
|
518
|
-
break;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
if (lines.length > 0) {
|
|
522
|
-
return { lines, error: null };
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const forumLines = await fetchRecentForumActivity(opts.issueIds);
|
|
527
|
-
if (forumLines.length > 0) {
|
|
528
|
-
return { lines: forumLines, error: null };
|
|
529
|
-
}
|
|
530
|
-
return { lines: [], error: endpointError };
|
|
531
|
-
}
|
|
532
|
-
function computeQueueDrift(sessions, activeIssues) {
|
|
533
|
-
const activeWithoutSessionIds = activeIssues
|
|
534
|
-
.filter((issue) => !issueHasSession(sessions, issue.id))
|
|
535
|
-
.map((issue) => issue.id);
|
|
536
|
-
const orphanSessions = sessions.filter((sessionName) => !activeIssues.some((issue) => sessionMatchesIssue(sessionName, issue.id)));
|
|
537
|
-
return {
|
|
538
|
-
activeWithoutSessionIds,
|
|
539
|
-
orphanSessions,
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
function queueFallbackActivityLines(state) {
|
|
543
|
-
const lines = [];
|
|
544
|
-
for (const issue of state.activeIssues) {
|
|
545
|
-
if (lines.length >= ACTIVITY_LINE_LIMIT) {
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
548
|
-
lines.push(`active ${issue.id}: ${truncateOneLine(issue.title, 52)}`);
|
|
549
|
-
}
|
|
550
|
-
for (const issue of state.readyIssues) {
|
|
551
|
-
if (lines.length >= ACTIVITY_LINE_LIMIT) {
|
|
552
|
-
break;
|
|
553
|
-
}
|
|
554
|
-
lines.push(`ready ${issue.id}: ${truncateOneLine(issue.title, 52)}`);
|
|
555
|
-
}
|
|
556
|
-
if (lines.length === 0 && state.sessions.length > 0) {
|
|
557
|
-
const sessionPreview = state.sessions
|
|
558
|
-
.slice(0, 2)
|
|
559
|
-
.map((sessionName) => truncateOneLine(sessionName, 28))
|
|
560
|
-
.join(", ");
|
|
561
|
-
const suffix = state.sessions.length > 2 ? ` +${state.sessions.length - 2} more` : "";
|
|
562
|
-
lines.push(`tmux active: ${sessionPreview}${suffix}`);
|
|
563
|
-
}
|
|
564
|
-
return lines;
|
|
565
|
-
}
|
|
566
|
-
function normalizeIssueTag(raw) {
|
|
567
|
-
const trimmed = raw.trim();
|
|
568
|
-
if (!trimmed || trimmed.toLowerCase() === "clear") {
|
|
569
|
-
return null;
|
|
570
|
-
}
|
|
571
|
-
return trimmed;
|
|
572
|
-
}
|
|
573
|
-
function parseSpawnMode(raw) {
|
|
574
|
-
const value = raw.trim().toLowerCase();
|
|
575
|
-
if (value === "operator" || value === "researcher") {
|
|
576
|
-
return value;
|
|
577
|
-
}
|
|
578
|
-
return null;
|
|
579
|
-
}
|
|
580
|
-
function parseOnOff(raw) {
|
|
581
|
-
const value = (raw ?? "").trim().toLowerCase();
|
|
582
|
-
if (value === "on" || value === "yes" || value === "true" || value === "1") {
|
|
583
|
-
return true;
|
|
584
|
-
}
|
|
585
|
-
if (value === "off" || value === "no" || value === "false" || value === "0") {
|
|
586
|
-
return false;
|
|
587
|
-
}
|
|
588
|
-
return null;
|
|
589
|
-
}
|
|
590
|
-
function parseSnapshotFormat(raw) {
|
|
591
|
-
const value = (raw ?? "compact").trim().toLowerCase();
|
|
592
|
-
return value === "multiline" ? "multiline" : "compact";
|
|
593
|
-
}
|
|
594
|
-
function parseSecondsBounded(secondsRaw, minSeconds, maxSeconds, field) {
|
|
595
|
-
if (typeof secondsRaw !== "number" || !Number.isFinite(secondsRaw)) {
|
|
596
|
-
return { ok: false, error: `${field} must be a number.` };
|
|
597
|
-
}
|
|
598
|
-
const rounded = Math.round(secondsRaw);
|
|
599
|
-
if (rounded < minSeconds || rounded > maxSeconds) {
|
|
600
|
-
return { ok: false, error: `${field} must be ${minSeconds}-${maxSeconds} seconds.` };
|
|
601
|
-
}
|
|
602
|
-
return { ok: true, ms: rounded * 1_000 };
|
|
603
|
-
}
|
|
604
|
-
function subagentsSnapshot(state, format) {
|
|
605
|
-
const issueScope = state.issueRootId ?? "(all roots)";
|
|
606
|
-
const tagScope = state.issueTagFilter ?? "(all tags)";
|
|
607
|
-
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
608
|
-
const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
|
|
609
|
-
const staleCount = drift.activeWithoutSessionIds.length + drift.orphanSessions.length;
|
|
610
|
-
const health = state.issueError || state.sessionError || state.activityError || refreshStale || staleCount > 0
|
|
611
|
-
? "degraded"
|
|
612
|
-
: "healthy";
|
|
613
|
-
const refreshAge = formatRefreshAge(state.lastUpdatedMs);
|
|
614
|
-
const paused = state.spawnPaused ? "yes" : "no";
|
|
615
|
-
const refreshSeconds = Math.round(state.refreshIntervalMs / 1_000);
|
|
616
|
-
const staleAfterSeconds = Math.round(state.staleAfterMs / 1_000);
|
|
617
|
-
if (format === "multiline") {
|
|
618
|
-
return [
|
|
619
|
-
"Subagents HUD snapshot",
|
|
620
|
-
`health: ${health}`,
|
|
621
|
-
`prefix: ${state.prefix || "(all sessions)"}`,
|
|
622
|
-
`issue_root: ${issueScope}`,
|
|
623
|
-
`issue_tag_filter: ${tagScope}`,
|
|
624
|
-
`spawn_mode: ${state.spawnMode}`,
|
|
625
|
-
`spawn_paused: ${paused}`,
|
|
626
|
-
`queues: ${state.readyIssues.length} ready / ${state.activeIssues.length} active`,
|
|
627
|
-
`sessions: ${state.sessions.length}`,
|
|
628
|
-
`activity_lines: ${state.activityLines.length}`,
|
|
629
|
-
`drift_active_without_session: ${drift.activeWithoutSessionIds.length}`,
|
|
630
|
-
`drift_orphan_sessions: ${drift.orphanSessions.length}`,
|
|
631
|
-
`refresh_age: ${refreshAge}`,
|
|
632
|
-
`refresh_stale: ${refreshStale ? "yes" : "no"}`,
|
|
633
|
-
`refresh_seconds: ${refreshSeconds}`,
|
|
634
|
-
`stale_after_seconds: ${staleAfterSeconds}`,
|
|
635
|
-
].join("\n");
|
|
636
|
-
}
|
|
637
|
-
return [
|
|
638
|
-
"HUD(subagents)",
|
|
639
|
-
`health=${health}`,
|
|
640
|
-
`root=${issueScope}`,
|
|
641
|
-
`tag=${tagScope}`,
|
|
642
|
-
`mode=${state.spawnMode}`,
|
|
643
|
-
`paused=${paused}`,
|
|
644
|
-
`ready=${state.readyIssues.length}`,
|
|
645
|
-
`active=${state.activeIssues.length}`,
|
|
646
|
-
`sessions=${state.sessions.length}`,
|
|
647
|
-
`drift=${staleCount}`,
|
|
648
|
-
`activity=${state.activityLines.length}`,
|
|
649
|
-
`refresh=${refreshAge}`,
|
|
650
|
-
].join(" · ");
|
|
651
|
-
}
|
|
652
|
-
function renderSubagentsUi(ctx, state) {
|
|
653
|
-
if (!ctx.hasUI) {
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
if (!state.enabled) {
|
|
657
|
-
ctx.ui.setStatus("mu-subagents", undefined);
|
|
658
|
-
ctx.ui.setStatus("mu-subagents-meta", undefined);
|
|
659
|
-
ctx.ui.setWidget("mu-subagents", undefined);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
const issueScope = state.issueRootId ? `root:${state.issueRootId}` : "all-roots";
|
|
663
|
-
const tagScope = state.issueTagFilter ? `tag:${state.issueTagFilter}` : null;
|
|
664
|
-
const scopeLabel = [issueScope, tagScope].filter((value) => value != null).join(" · ");
|
|
665
|
-
const scopeCompact = truncateOneLine(scopeLabel, WIDGET_SCOPE_MAX);
|
|
666
|
-
const prefixCompact = truncateOneLine(state.prefix || "(all sessions)", WIDGET_PREFIX_MAX);
|
|
667
|
-
const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
|
|
668
|
-
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
669
|
-
const staleCount = drift.activeWithoutSessionIds.length + drift.orphanSessions.length;
|
|
670
|
-
const hasError = Boolean(state.sessionError || state.issueError || state.activityError || refreshStale || staleCount > 0);
|
|
671
|
-
const healthColor = hasError ? "warning" : "success";
|
|
672
|
-
const healthLabel = hasError ? "degraded" : "healthy";
|
|
673
|
-
const queueTotal = state.readyIssues.length + state.activeIssues.length;
|
|
674
|
-
const queueBar = queueMeter(state.activeIssues.length, Math.max(1, queueTotal), 10);
|
|
675
|
-
const refreshAge = formatRefreshAge(state.lastUpdatedMs);
|
|
676
|
-
const pausedLabel = state.spawnPaused ? "yes" : "no";
|
|
677
|
-
const pausedColor = state.spawnPaused ? "warning" : "dim";
|
|
678
|
-
const refreshSeconds = Math.round(state.refreshIntervalMs / 1_000);
|
|
679
|
-
const staleAfterSeconds = Math.round(state.staleAfterMs / 1_000);
|
|
680
|
-
const activityLines = state.activityLines.slice(0, ACTIVITY_LINE_LIMIT);
|
|
681
|
-
const statusParts = [
|
|
682
|
-
ctx.ui.theme.fg("dim", "subagents"),
|
|
683
|
-
ctx.ui.theme.fg(healthColor, healthLabel),
|
|
684
|
-
ctx.ui.theme.fg("dim", `mode:${state.spawnMode}`),
|
|
685
|
-
ctx.ui.theme.fg("dim", `q:${state.readyIssues.length}/${state.activeIssues.length}`),
|
|
686
|
-
ctx.ui.theme.fg("dim", `tmux:${state.sessions.length}`),
|
|
687
|
-
];
|
|
688
|
-
if (state.spawnPaused) {
|
|
689
|
-
statusParts.push(ctx.ui.theme.fg(pausedColor, `paused:${pausedLabel}`));
|
|
690
|
-
}
|
|
691
|
-
if (staleCount > 0) {
|
|
692
|
-
statusParts.push(ctx.ui.theme.fg("warning", `drift:${staleCount}`));
|
|
693
|
-
}
|
|
694
|
-
if (state.issueRootId) {
|
|
695
|
-
statusParts.push(ctx.ui.theme.fg("muted", truncateOneLine(issueScope, 18)));
|
|
696
|
-
}
|
|
697
|
-
ctx.ui.setStatus("mu-subagents", statusParts.join(` ${ctx.ui.theme.fg("muted", "·")} `));
|
|
698
|
-
ctx.ui.setStatus("mu-subagents-meta", undefined);
|
|
699
|
-
const titleParts = [
|
|
700
|
-
ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Subagents")),
|
|
701
|
-
ctx.ui.theme.fg("muted", "·"),
|
|
702
|
-
ctx.ui.theme.fg(healthColor, healthLabel),
|
|
703
|
-
ctx.ui.theme.fg("muted", "·"),
|
|
704
|
-
ctx.ui.theme.fg("accent", `mode:${state.spawnMode}`),
|
|
705
|
-
];
|
|
706
|
-
if (state.spawnPaused) {
|
|
707
|
-
titleParts.push(ctx.ui.theme.fg("muted", "·"), ctx.ui.theme.fg(pausedColor, `paused:${pausedLabel}`));
|
|
708
|
-
}
|
|
709
|
-
const queueParts = [
|
|
710
|
-
ctx.ui.theme.fg("muted", "queues:"),
|
|
711
|
-
ctx.ui.theme.fg("accent", `${state.readyIssues.length}r`),
|
|
712
|
-
ctx.ui.theme.fg("muted", "/"),
|
|
713
|
-
ctx.ui.theme.fg("warning", `${state.activeIssues.length}a`),
|
|
714
|
-
ctx.ui.theme.fg("dim", queueBar),
|
|
715
|
-
ctx.ui.theme.fg("muted", "·"),
|
|
716
|
-
ctx.ui.theme.fg("muted", "tmux:"),
|
|
717
|
-
ctx.ui.theme.fg("dim", `${state.sessions.length}`),
|
|
718
|
-
];
|
|
719
|
-
if (staleCount > 0) {
|
|
720
|
-
queueParts.push(ctx.ui.theme.fg("muted", "·"), ctx.ui.theme.fg("warning", `drift:${staleCount}`));
|
|
721
|
-
}
|
|
722
|
-
const refreshParts = [
|
|
723
|
-
ctx.ui.theme.fg("muted", "refresh:"),
|
|
724
|
-
ctx.ui.theme.fg(refreshStale ? "warning" : "dim", refreshAge),
|
|
725
|
-
ctx.ui.theme.fg("muted", "·"),
|
|
726
|
-
ctx.ui.theme.fg("muted", "every:"),
|
|
727
|
-
ctx.ui.theme.fg("dim", `${refreshSeconds}s`),
|
|
728
|
-
];
|
|
729
|
-
if (refreshStale) {
|
|
730
|
-
refreshParts.push(ctx.ui.theme.fg("muted", "·"), ctx.ui.theme.fg("muted", "stale:"), ctx.ui.theme.fg("dim", `${staleAfterSeconds}s`));
|
|
731
|
-
}
|
|
732
|
-
const lines = [
|
|
733
|
-
titleParts.join(" "),
|
|
734
|
-
`${ctx.ui.theme.fg("muted", "scope:")} ${ctx.ui.theme.fg("dim", scopeCompact)} ${ctx.ui.theme.fg("muted", "· prefix:")} ${ctx.ui.theme.fg("dim", prefixCompact)}`,
|
|
735
|
-
queueParts.join(" "),
|
|
736
|
-
refreshParts.join(" "),
|
|
737
|
-
];
|
|
738
|
-
if (state.issueError) {
|
|
739
|
-
lines.push(ctx.ui.theme.fg("warning", `issue_error: ${truncateOneLine(state.issueError, WIDGET_ERROR_MAX)}`));
|
|
740
|
-
}
|
|
741
|
-
if (state.sessionError) {
|
|
742
|
-
lines.push(ctx.ui.theme.fg("warning", `tmux_error: ${truncateOneLine(state.sessionError, WIDGET_ERROR_MAX)}`));
|
|
743
|
-
}
|
|
744
|
-
if (refreshStale) {
|
|
745
|
-
lines.push(ctx.ui.theme.fg("warning", `warning: refresh stale (>${staleAfterSeconds}s since last successful refresh)`));
|
|
746
|
-
}
|
|
747
|
-
if (drift.activeWithoutSessionIds.length > 0) {
|
|
748
|
-
lines.push(ctx.ui.theme.fg("warning", truncateOneLine(`drift_missing: ${drift.activeWithoutSessionIds.slice(0, 4).join(", ")}${drift.activeWithoutSessionIds.length > 4 ? " ..." : ""}`, WIDGET_ERROR_MAX)));
|
|
749
|
-
}
|
|
750
|
-
if (drift.orphanSessions.length > 0) {
|
|
751
|
-
lines.push(ctx.ui.theme.fg("warning", truncateOneLine(`drift_orphan: ${drift.orphanSessions.slice(0, 4).join(", ")}${drift.orphanSessions.length > 4 ? " ..." : ""}`, WIDGET_ERROR_MAX)));
|
|
752
|
-
}
|
|
753
|
-
lines.push(ctx.ui.theme.fg("dim", "────────────────────────────"));
|
|
754
|
-
lines.push(ctx.ui.theme.fg("accent", "activity"));
|
|
755
|
-
if (state.activityError) {
|
|
756
|
-
lines.push(ctx.ui.theme.fg("warning", truncateOneLine(state.activityError, WIDGET_ERROR_MAX)));
|
|
757
|
-
}
|
|
758
|
-
else if (activityLines.length === 0) {
|
|
759
|
-
const fallbackLines = queueFallbackActivityLines(state);
|
|
760
|
-
if (fallbackLines.length > 0) {
|
|
761
|
-
for (const line of fallbackLines) {
|
|
762
|
-
lines.push(`${ctx.ui.theme.fg("muted", "•")} ${ctx.ui.theme.fg("text", truncateOneLine(line, WIDGET_SUMMARY_MAX))}`);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
else {
|
|
766
|
-
lines.push(ctx.ui.theme.fg("muted", "(no active operators)"));
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
for (const line of activityLines) {
|
|
771
|
-
lines.push(`${ctx.ui.theme.fg("muted", "•")} ${ctx.ui.theme.fg("text", truncateOneLine(line, WIDGET_SUMMARY_MAX))}`);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
ctx.ui.setWidget("mu-subagents", lines, { placement: "belowEditor" });
|
|
775
|
-
}
|
|
776
|
-
function subagentsUsageText() {
|
|
777
|
-
return [
|
|
778
|
-
"Usage:",
|
|
779
|
-
" /mu subagents on|off|toggle|status|refresh|snapshot",
|
|
780
|
-
" /mu subagents prefix <text|clear>",
|
|
781
|
-
" /mu subagents root <issue-id|clear>",
|
|
782
|
-
" /mu subagents tag <tag|clear>",
|
|
783
|
-
" /mu subagents mode <operator|researcher>",
|
|
784
|
-
" /mu subagents refresh-interval <seconds>",
|
|
785
|
-
" /mu subagents stale-after <seconds>",
|
|
786
|
-
" /mu subagents pause <on|off>",
|
|
787
|
-
" /mu subagents spawn [N|all]",
|
|
788
|
-
].join("\n");
|
|
789
|
-
}
|
|
790
|
-
function subagentsDetails(state) {
|
|
791
|
-
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
792
|
-
return {
|
|
793
|
-
enabled: state.enabled,
|
|
794
|
-
prefix: state.prefix,
|
|
795
|
-
issue_root_id: state.issueRootId,
|
|
796
|
-
issue_tag_filter: state.issueTagFilter,
|
|
797
|
-
spawn_mode: state.spawnMode,
|
|
798
|
-
spawn_paused: state.spawnPaused,
|
|
799
|
-
refresh_seconds: Math.round(state.refreshIntervalMs / 1_000),
|
|
800
|
-
stale_after_seconds: Math.round(state.staleAfterMs / 1_000),
|
|
801
|
-
sessions: [...state.sessions],
|
|
802
|
-
ready_issue_ids: state.readyIssues.map((issue) => issue.id),
|
|
803
|
-
active_issue_ids: state.activeIssues.map((issue) => issue.id),
|
|
804
|
-
active_without_session_ids: drift.activeWithoutSessionIds,
|
|
805
|
-
orphan_sessions: drift.orphanSessions,
|
|
806
|
-
refresh_stale: isRefreshStale(state.lastUpdatedMs, state.staleAfterMs),
|
|
807
|
-
issue_error: state.issueError,
|
|
808
|
-
session_error: state.sessionError,
|
|
809
|
-
activity_lines: [...state.activityLines],
|
|
810
|
-
activity_error: state.activityError,
|
|
811
|
-
last_updated_ms: state.lastUpdatedMs,
|
|
812
|
-
snapshot_compact: subagentsSnapshot(state, "compact"),
|
|
813
|
-
snapshot_multiline: subagentsSnapshot(state, "multiline"),
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
function subagentsToolError(message, state) {
|
|
817
|
-
return {
|
|
818
|
-
content: [{ type: "text", text: message }],
|
|
819
|
-
details: {
|
|
820
|
-
ok: false,
|
|
821
|
-
error: message,
|
|
822
|
-
...subagentsDetails(state),
|
|
823
|
-
},
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
export function subagentsUiExtension(pi) {
|
|
827
|
-
let activeCtx = null;
|
|
828
|
-
let pollTimer = null;
|
|
829
|
-
const state = createDefaultState();
|
|
830
|
-
const refresh = async (ctx) => {
|
|
831
|
-
if (!state.enabled) {
|
|
832
|
-
renderSubagentsUi(ctx, state);
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
const [tmux, issues] = await Promise.all([
|
|
836
|
-
listTmuxSessions(state.prefix),
|
|
837
|
-
listIssueSlices(state.issueRootId, state.issueTagFilter),
|
|
838
|
-
]);
|
|
839
|
-
state.sessions = tmux.sessions;
|
|
840
|
-
state.sessionError = tmux.error;
|
|
841
|
-
state.readyIssues = issues.ready;
|
|
842
|
-
state.activeIssues = issues.active;
|
|
843
|
-
state.issueError = issues.error;
|
|
844
|
-
const trackedIssueIds = [...state.activeIssues, ...state.readyIssues]
|
|
845
|
-
.slice(0, 8)
|
|
846
|
-
.map((issue) => issue.id);
|
|
847
|
-
const activity = await fetchRecentActivity({ issueIds: trackedIssueIds });
|
|
848
|
-
state.activityLines = activity.lines;
|
|
849
|
-
state.activityError = activity.error;
|
|
850
|
-
state.lastUpdatedMs = Date.now();
|
|
851
|
-
renderSubagentsUi(ctx, state);
|
|
852
|
-
};
|
|
853
|
-
const stopPolling = () => {
|
|
854
|
-
if (!pollTimer) {
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
clearInterval(pollTimer);
|
|
858
|
-
pollTimer = null;
|
|
859
|
-
};
|
|
860
|
-
const ensurePolling = () => {
|
|
861
|
-
if (pollTimer) {
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
pollTimer = setInterval(() => {
|
|
865
|
-
if (!activeCtx) {
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
void refresh(activeCtx);
|
|
869
|
-
}, state.refreshIntervalMs);
|
|
870
|
-
};
|
|
871
|
-
const restartPolling = () => {
|
|
872
|
-
if (!state.enabled) {
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
stopPolling();
|
|
876
|
-
ensurePolling();
|
|
877
|
-
};
|
|
878
|
-
const notify = (ctx, message, level = "info") => {
|
|
879
|
-
ctx.ui.notify(`${message}\n\n${subagentsUsageText()}`, level);
|
|
880
|
-
};
|
|
881
|
-
const syncSubagentsMode = (ctx, action) => {
|
|
882
|
-
const passiveAction = action === "status" || action === "snapshot";
|
|
883
|
-
if (!state.enabled) {
|
|
884
|
-
clearHudMode("subagents");
|
|
885
|
-
}
|
|
886
|
-
else if (!passiveAction) {
|
|
887
|
-
setActiveHudMode("subagents");
|
|
888
|
-
}
|
|
889
|
-
syncHudModeStatus(ctx);
|
|
890
|
-
};
|
|
891
|
-
const statusSummary = () => {
|
|
892
|
-
const when = state.lastUpdatedMs == null ? "never" : new Date(state.lastUpdatedMs).toLocaleTimeString();
|
|
893
|
-
const status = state.enabled ? "enabled" : "disabled";
|
|
894
|
-
const issueScope = state.issueRootId ?? "(all roots)";
|
|
895
|
-
const issueTag = state.issueTagFilter ?? "(all tags)";
|
|
896
|
-
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
897
|
-
const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
|
|
898
|
-
const issueError = state.issueError ? `\nissue_error: ${state.issueError}` : "";
|
|
899
|
-
const tmuxError = state.sessionError ? `\ntmux_error: ${state.sessionError}` : "";
|
|
900
|
-
const activityError = state.activityError ? `\nactivity_error: ${state.activityError}` : "";
|
|
901
|
-
const driftInfo = drift.activeWithoutSessionIds.length > 0 || drift.orphanSessions.length > 0
|
|
902
|
-
? `\ndrift_active_without_session: ${drift.activeWithoutSessionIds.length}\ndrift_orphan_sessions: ${drift.orphanSessions.length}`
|
|
903
|
-
: "";
|
|
904
|
-
const staleInfo = refreshStale ? "\nrefresh_stale: yes" : "\nrefresh_stale: no";
|
|
905
|
-
return {
|
|
906
|
-
level: state.issueError ||
|
|
907
|
-
state.sessionError ||
|
|
908
|
-
state.activityError ||
|
|
909
|
-
refreshStale ||
|
|
910
|
-
drift.activeWithoutSessionIds.length > 0 ||
|
|
911
|
-
drift.orphanSessions.length > 0
|
|
912
|
-
? "warning"
|
|
913
|
-
: "info",
|
|
914
|
-
text: [
|
|
915
|
-
`Subagents monitor ${status}`,
|
|
916
|
-
`prefix: ${state.prefix || "(all sessions)"}`,
|
|
917
|
-
`issue_root: ${issueScope}`,
|
|
918
|
-
`issue_tag_filter: ${issueTag}`,
|
|
919
|
-
`spawn_mode: ${state.spawnMode}`,
|
|
920
|
-
`spawn_paused: ${state.spawnPaused ? "yes" : "no"}`,
|
|
921
|
-
`refresh_seconds: ${Math.round(state.refreshIntervalMs / 1_000)}`,
|
|
922
|
-
`stale_after_seconds: ${Math.round(state.staleAfterMs / 1_000)}`,
|
|
923
|
-
`sessions: ${state.sessions.length}`,
|
|
924
|
-
`ready_issues: ${state.readyIssues.length}`,
|
|
925
|
-
`active_issues: ${state.activeIssues.length}`,
|
|
926
|
-
`activity_updates: ${state.activityLines.length}`,
|
|
927
|
-
`last refresh: ${when}`,
|
|
928
|
-
].join("\n") +
|
|
929
|
-
issueError +
|
|
930
|
-
tmuxError +
|
|
931
|
-
activityError +
|
|
932
|
-
driftInfo +
|
|
933
|
-
staleInfo,
|
|
934
|
-
};
|
|
935
|
-
};
|
|
936
|
-
const applySubagentsAction = async (params, ctx) => {
|
|
937
|
-
switch (params.action) {
|
|
938
|
-
case "status": {
|
|
939
|
-
const summary = statusSummary();
|
|
940
|
-
return { ok: true, message: summary.text, level: summary.level };
|
|
941
|
-
}
|
|
942
|
-
case "snapshot": {
|
|
943
|
-
const format = parseSnapshotFormat(params.snapshot_format);
|
|
944
|
-
return { ok: true, message: subagentsSnapshot(state, format), level: "info" };
|
|
945
|
-
}
|
|
946
|
-
case "on":
|
|
947
|
-
state.enabled = true;
|
|
948
|
-
ensurePolling();
|
|
949
|
-
await refresh(ctx);
|
|
950
|
-
return { ok: true, message: "Subagents monitor enabled.", level: "info" };
|
|
951
|
-
case "off":
|
|
952
|
-
state.enabled = false;
|
|
953
|
-
stopPolling();
|
|
954
|
-
renderSubagentsUi(ctx, state);
|
|
955
|
-
return { ok: true, message: "Subagents monitor disabled.", level: "info" };
|
|
956
|
-
case "toggle":
|
|
957
|
-
state.enabled = !state.enabled;
|
|
958
|
-
if (state.enabled) {
|
|
959
|
-
ensurePolling();
|
|
960
|
-
await refresh(ctx);
|
|
961
|
-
}
|
|
962
|
-
else {
|
|
963
|
-
stopPolling();
|
|
964
|
-
renderSubagentsUi(ctx, state);
|
|
965
|
-
}
|
|
966
|
-
return { ok: true, message: `Subagents monitor ${state.enabled ? "enabled" : "disabled"}.`, level: "info" };
|
|
967
|
-
case "refresh":
|
|
968
|
-
await refresh(ctx);
|
|
969
|
-
return { ok: true, message: "Subagents monitor refreshed.", level: "info" };
|
|
970
|
-
case "set_prefix": {
|
|
971
|
-
const value = params.prefix?.trim();
|
|
972
|
-
if (!value) {
|
|
973
|
-
return { ok: false, message: "Missing prefix value.", level: "error" };
|
|
974
|
-
}
|
|
975
|
-
state.prefix = value.toLowerCase() === "clear" ? "" : value;
|
|
976
|
-
state.enabled = true;
|
|
977
|
-
ensurePolling();
|
|
978
|
-
await refresh(ctx);
|
|
979
|
-
return { ok: true, message: `Subagents prefix set to ${state.prefix || "(all sessions)"}.`, level: "info" };
|
|
980
|
-
}
|
|
981
|
-
case "set_root": {
|
|
982
|
-
const value = params.root_issue_id?.trim();
|
|
983
|
-
if (!value) {
|
|
984
|
-
return { ok: false, message: "Missing root issue id.", level: "error" };
|
|
985
|
-
}
|
|
986
|
-
state.issueRootId = value.toLowerCase() === "clear" ? null : value;
|
|
987
|
-
state.enabled = true;
|
|
988
|
-
ensurePolling();
|
|
989
|
-
await refresh(ctx);
|
|
990
|
-
return { ok: true, message: `Subagents root set to ${state.issueRootId ?? "(all roots)"}.`, level: "info" };
|
|
991
|
-
}
|
|
992
|
-
case "set_tag": {
|
|
993
|
-
const value = params.issue_tag?.trim();
|
|
994
|
-
if (!value) {
|
|
995
|
-
return { ok: false, message: "Missing tag value.", level: "error" };
|
|
996
|
-
}
|
|
997
|
-
state.issueTagFilter = normalizeIssueTag(value);
|
|
998
|
-
state.enabled = true;
|
|
999
|
-
ensurePolling();
|
|
1000
|
-
await refresh(ctx);
|
|
1001
|
-
return {
|
|
1002
|
-
ok: true,
|
|
1003
|
-
message: `Subagents issue tag filter set to ${state.issueTagFilter ?? "(all tags)"}.`,
|
|
1004
|
-
level: "info",
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
case "set_mode": {
|
|
1008
|
-
const modeRaw = params.spawn_mode?.trim() ?? "";
|
|
1009
|
-
const mode = parseSpawnMode(modeRaw);
|
|
1010
|
-
if (!mode) {
|
|
1011
|
-
return { ok: false, message: "Invalid spawn mode.", level: "error" };
|
|
1012
|
-
}
|
|
1013
|
-
state.spawnMode = mode;
|
|
1014
|
-
state.enabled = true;
|
|
1015
|
-
ensurePolling();
|
|
1016
|
-
await refresh(ctx);
|
|
1017
|
-
return { ok: true, message: `Subagents spawn mode set to ${mode}.`, level: "info" };
|
|
1018
|
-
}
|
|
1019
|
-
case "set_refresh_interval": {
|
|
1020
|
-
const parsed = parseSecondsBounded(params.refresh_seconds, MIN_REFRESH_SECONDS, MAX_REFRESH_SECONDS, "refresh_seconds");
|
|
1021
|
-
if (!parsed.ok) {
|
|
1022
|
-
return { ok: false, message: parsed.error, level: "error" };
|
|
1023
|
-
}
|
|
1024
|
-
state.refreshIntervalMs = parsed.ms;
|
|
1025
|
-
state.enabled = true;
|
|
1026
|
-
restartPolling();
|
|
1027
|
-
await refresh(ctx);
|
|
1028
|
-
return {
|
|
1029
|
-
ok: true,
|
|
1030
|
-
message: `Subagents refresh interval set to ${Math.round(state.refreshIntervalMs / 1_000)}s.`,
|
|
1031
|
-
level: "info",
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
case "set_stale_after": {
|
|
1035
|
-
const parsed = parseSecondsBounded(params.stale_after_seconds, MIN_STALE_SECONDS, MAX_STALE_SECONDS, "stale_after_seconds");
|
|
1036
|
-
if (!parsed.ok) {
|
|
1037
|
-
return { ok: false, message: parsed.error, level: "error" };
|
|
1038
|
-
}
|
|
1039
|
-
state.staleAfterMs = parsed.ms;
|
|
1040
|
-
state.enabled = true;
|
|
1041
|
-
ensurePolling();
|
|
1042
|
-
await refresh(ctx);
|
|
1043
|
-
return {
|
|
1044
|
-
ok: true,
|
|
1045
|
-
message: `Subagents stale threshold set to ${Math.round(state.staleAfterMs / 1_000)}s.`,
|
|
1046
|
-
level: "info",
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
case "set_spawn_paused": {
|
|
1050
|
-
if (typeof params.spawn_paused !== "boolean") {
|
|
1051
|
-
return { ok: false, message: "spawn_paused must be a boolean.", level: "error" };
|
|
1052
|
-
}
|
|
1053
|
-
state.spawnPaused = params.spawn_paused;
|
|
1054
|
-
state.enabled = true;
|
|
1055
|
-
ensurePolling();
|
|
1056
|
-
await refresh(ctx);
|
|
1057
|
-
return {
|
|
1058
|
-
ok: true,
|
|
1059
|
-
message: `Subagents spawn pause set to ${state.spawnPaused ? "on" : "off"}.`,
|
|
1060
|
-
level: "info",
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
|
-
case "update": {
|
|
1064
|
-
const changed = [];
|
|
1065
|
-
let refreshIntervalChanged = false;
|
|
1066
|
-
if (params.prefix !== undefined) {
|
|
1067
|
-
if (typeof params.prefix !== "string") {
|
|
1068
|
-
return { ok: false, message: "prefix must be a string.", level: "error" };
|
|
1069
|
-
}
|
|
1070
|
-
const trimmed = params.prefix.trim();
|
|
1071
|
-
if (trimmed.length === 0) {
|
|
1072
|
-
return { ok: false, message: "prefix must not be empty.", level: "error" };
|
|
1073
|
-
}
|
|
1074
|
-
state.prefix = trimmed.toLowerCase() === "clear" ? "" : trimmed;
|
|
1075
|
-
changed.push("prefix");
|
|
1076
|
-
}
|
|
1077
|
-
if (params.root_issue_id !== undefined) {
|
|
1078
|
-
if (typeof params.root_issue_id !== "string") {
|
|
1079
|
-
return { ok: false, message: "root_issue_id must be a string.", level: "error" };
|
|
1080
|
-
}
|
|
1081
|
-
const trimmed = params.root_issue_id.trim();
|
|
1082
|
-
if (trimmed.length === 0) {
|
|
1083
|
-
return { ok: false, message: "root_issue_id must not be empty.", level: "error" };
|
|
1084
|
-
}
|
|
1085
|
-
state.issueRootId = trimmed.toLowerCase() === "clear" ? null : trimmed;
|
|
1086
|
-
changed.push("root_issue_id");
|
|
1087
|
-
}
|
|
1088
|
-
if (params.issue_tag !== undefined) {
|
|
1089
|
-
if (typeof params.issue_tag !== "string") {
|
|
1090
|
-
return { ok: false, message: "issue_tag must be a string.", level: "error" };
|
|
1091
|
-
}
|
|
1092
|
-
const trimmed = params.issue_tag.trim();
|
|
1093
|
-
if (trimmed.length === 0) {
|
|
1094
|
-
return { ok: false, message: "issue_tag must not be empty.", level: "error" };
|
|
1095
|
-
}
|
|
1096
|
-
state.issueTagFilter = normalizeIssueTag(trimmed);
|
|
1097
|
-
changed.push("issue_tag");
|
|
1098
|
-
}
|
|
1099
|
-
if (params.spawn_mode !== undefined) {
|
|
1100
|
-
if (typeof params.spawn_mode !== "string") {
|
|
1101
|
-
return { ok: false, message: "spawn_mode must be a string.", level: "error" };
|
|
1102
|
-
}
|
|
1103
|
-
const mode = parseSpawnMode(params.spawn_mode);
|
|
1104
|
-
if (!mode) {
|
|
1105
|
-
return { ok: false, message: "Invalid spawn mode.", level: "error" };
|
|
1106
|
-
}
|
|
1107
|
-
state.spawnMode = mode;
|
|
1108
|
-
changed.push("spawn_mode");
|
|
1109
|
-
}
|
|
1110
|
-
if (params.refresh_seconds !== undefined) {
|
|
1111
|
-
const parsed = parseSecondsBounded(params.refresh_seconds, MIN_REFRESH_SECONDS, MAX_REFRESH_SECONDS, "refresh_seconds");
|
|
1112
|
-
if (!parsed.ok) {
|
|
1113
|
-
return { ok: false, message: parsed.error, level: "error" };
|
|
1114
|
-
}
|
|
1115
|
-
state.refreshIntervalMs = parsed.ms;
|
|
1116
|
-
refreshIntervalChanged = true;
|
|
1117
|
-
changed.push("refresh_seconds");
|
|
1118
|
-
}
|
|
1119
|
-
if (params.stale_after_seconds !== undefined) {
|
|
1120
|
-
const parsed = parseSecondsBounded(params.stale_after_seconds, MIN_STALE_SECONDS, MAX_STALE_SECONDS, "stale_after_seconds");
|
|
1121
|
-
if (!parsed.ok) {
|
|
1122
|
-
return { ok: false, message: parsed.error, level: "error" };
|
|
1123
|
-
}
|
|
1124
|
-
state.staleAfterMs = parsed.ms;
|
|
1125
|
-
changed.push("stale_after_seconds");
|
|
1126
|
-
}
|
|
1127
|
-
if (params.spawn_paused !== undefined) {
|
|
1128
|
-
if (typeof params.spawn_paused !== "boolean") {
|
|
1129
|
-
return { ok: false, message: "spawn_paused must be a boolean.", level: "error" };
|
|
1130
|
-
}
|
|
1131
|
-
state.spawnPaused = params.spawn_paused;
|
|
1132
|
-
changed.push("spawn_paused");
|
|
1133
|
-
}
|
|
1134
|
-
if (changed.length === 0) {
|
|
1135
|
-
return { ok: false, message: "No update fields provided.", level: "error" };
|
|
1136
|
-
}
|
|
1137
|
-
state.enabled = true;
|
|
1138
|
-
if (refreshIntervalChanged) {
|
|
1139
|
-
restartPolling();
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
ensurePolling();
|
|
1143
|
-
}
|
|
1144
|
-
await refresh(ctx);
|
|
1145
|
-
return { ok: true, message: `Subagents monitor updated (${changed.join(", ")}).`, level: "info" };
|
|
1146
|
-
}
|
|
1147
|
-
case "spawn": {
|
|
1148
|
-
if (state.spawnPaused) {
|
|
1149
|
-
return {
|
|
1150
|
-
ok: false,
|
|
1151
|
-
message: "Spawn is paused. Use set_spawn_paused=false before spawning.",
|
|
1152
|
-
level: "error",
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
if (!state.issueRootId) {
|
|
1156
|
-
return {
|
|
1157
|
-
ok: false,
|
|
1158
|
-
message: "Set a root first (`/mu subagents root <root-id>`) before spawning.",
|
|
1159
|
-
level: "error",
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
let spawnLimit = null;
|
|
1163
|
-
if (params.count != null && params.count !== "all") {
|
|
1164
|
-
const countNum = typeof params.count === "number" ? params.count : Number.parseInt(String(params.count), 10);
|
|
1165
|
-
const parsed = Math.trunc(countNum);
|
|
1166
|
-
if (!Number.isFinite(parsed) || parsed < 1 || parsed > ISSUE_LIST_LIMIT) {
|
|
1167
|
-
return { ok: false, message: `Spawn count must be 1-${ISSUE_LIST_LIMIT} or 'all'.`, level: "error" };
|
|
1168
|
-
}
|
|
1169
|
-
spawnLimit = parsed;
|
|
1170
|
-
}
|
|
1171
|
-
const issueSlices = await listIssueSlices(state.issueRootId, state.issueTagFilter);
|
|
1172
|
-
state.readyIssues = issueSlices.ready;
|
|
1173
|
-
state.activeIssues = issueSlices.active;
|
|
1174
|
-
state.issueError = issueSlices.error;
|
|
1175
|
-
if (issueSlices.error) {
|
|
1176
|
-
state.enabled = true;
|
|
1177
|
-
ensurePolling();
|
|
1178
|
-
renderSubagentsUi(ctx, state);
|
|
1179
|
-
return { ok: false, message: `Cannot spawn: ${issueSlices.error}`, level: "error" };
|
|
1180
|
-
}
|
|
1181
|
-
const candidates = spawnLimit == null ? issueSlices.ready : issueSlices.ready.slice(0, spawnLimit);
|
|
1182
|
-
if (candidates.length === 0) {
|
|
1183
|
-
state.enabled = true;
|
|
1184
|
-
ensurePolling();
|
|
1185
|
-
await refresh(ctx);
|
|
1186
|
-
return { ok: true, message: "No ready issues to spawn for current root/tag filter.", level: "info" };
|
|
1187
|
-
}
|
|
1188
|
-
const spawnPrefix = state.prefix.length > 0 ? state.prefix : DEFAULT_PREFIX;
|
|
1189
|
-
const tmux = await listTmuxSessions(spawnPrefix);
|
|
1190
|
-
if (tmux.error) {
|
|
1191
|
-
state.sessionError = tmux.error;
|
|
1192
|
-
state.enabled = true;
|
|
1193
|
-
ensurePolling();
|
|
1194
|
-
renderSubagentsUi(ctx, state);
|
|
1195
|
-
return { ok: false, message: `Cannot spawn: ${tmux.error}`, level: "error" };
|
|
1196
|
-
}
|
|
1197
|
-
const existingSessions = [...tmux.sessions];
|
|
1198
|
-
const runId = spawnRunId();
|
|
1199
|
-
const launched = [];
|
|
1200
|
-
const skipped = [];
|
|
1201
|
-
const failed = [];
|
|
1202
|
-
for (const issue of candidates) {
|
|
1203
|
-
if (issueHasSession(existingSessions, issue.id)) {
|
|
1204
|
-
skipped.push(`${issue.id} (session exists)`);
|
|
1205
|
-
continue;
|
|
1206
|
-
}
|
|
1207
|
-
let sessionName = `${spawnPrefix}${runId}-${issue.id}`;
|
|
1208
|
-
if (existingSessions.includes(sessionName)) {
|
|
1209
|
-
let suffix = 1;
|
|
1210
|
-
while (existingSessions.includes(`${sessionName}-${suffix}`)) {
|
|
1211
|
-
suffix += 1;
|
|
1212
|
-
}
|
|
1213
|
-
sessionName = `${sessionName}-${suffix}`;
|
|
1214
|
-
}
|
|
1215
|
-
const spawned = await spawnIssueTmuxSession({
|
|
1216
|
-
cwd: ctx.cwd,
|
|
1217
|
-
sessionName,
|
|
1218
|
-
issue,
|
|
1219
|
-
mode: state.spawnMode,
|
|
1220
|
-
});
|
|
1221
|
-
if (spawned.ok) {
|
|
1222
|
-
existingSessions.push(sessionName);
|
|
1223
|
-
launched.push(`${issue.id} -> ${sessionName}`);
|
|
1224
|
-
}
|
|
1225
|
-
else {
|
|
1226
|
-
failed.push(`${issue.id} (${spawned.error ?? "unknown error"})`);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
state.enabled = true;
|
|
1230
|
-
ensurePolling();
|
|
1231
|
-
await refresh(ctx);
|
|
1232
|
-
const summary = [
|
|
1233
|
-
`Spawned ${launched.length}/${candidates.length} ready issue sessions (mode=${state.spawnMode}).`,
|
|
1234
|
-
launched.length > 0 ? `launched: ${launched.join(", ")}` : "launched: (none)",
|
|
1235
|
-
`skipped: ${skipped.length}`,
|
|
1236
|
-
`failed: ${failed.length}`,
|
|
1237
|
-
];
|
|
1238
|
-
if (failed.length > 0) {
|
|
1239
|
-
summary.push(`failures: ${failed.join("; ")}`);
|
|
1240
|
-
}
|
|
1241
|
-
return { ok: true, message: summary.join("\n"), level: failed.length > 0 ? "warning" : "info" };
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
};
|
|
1245
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
1246
|
-
activeCtx = ctx;
|
|
1247
|
-
if (state.enabled) {
|
|
1248
|
-
ensurePolling();
|
|
1249
|
-
}
|
|
1250
|
-
await refresh(ctx);
|
|
1251
|
-
syncHudModeStatus(ctx);
|
|
1252
|
-
});
|
|
1253
|
-
pi.on("session_switch", async (_event, ctx) => {
|
|
1254
|
-
activeCtx = ctx;
|
|
1255
|
-
if (state.enabled) {
|
|
1256
|
-
ensurePolling();
|
|
1257
|
-
}
|
|
1258
|
-
await refresh(ctx);
|
|
1259
|
-
syncHudModeStatus(ctx);
|
|
1260
|
-
});
|
|
1261
|
-
pi.on("session_shutdown", async () => {
|
|
1262
|
-
stopPolling();
|
|
1263
|
-
activeCtx = null;
|
|
1264
|
-
});
|
|
1265
|
-
registerMuSubcommand(pi, {
|
|
1266
|
-
subcommand: "subagents",
|
|
1267
|
-
summary: "Monitor tmux subagent sessions + issue queue, and spawn ready issue sessions",
|
|
1268
|
-
usage: "/mu subagents on|off|toggle|status|refresh|snapshot|prefix|root|tag|mode|refresh-interval|stale-after|pause|spawn",
|
|
1269
|
-
handler: async (args, ctx) => {
|
|
1270
|
-
activeCtx = ctx;
|
|
1271
|
-
const tokens = args
|
|
1272
|
-
.trim()
|
|
1273
|
-
.split(/\s+/)
|
|
1274
|
-
.filter((token) => token.length > 0);
|
|
1275
|
-
const command = tokens[0] ?? "status";
|
|
1276
|
-
let params;
|
|
1277
|
-
switch (command) {
|
|
1278
|
-
case "status":
|
|
1279
|
-
params = { action: "status" };
|
|
1280
|
-
break;
|
|
1281
|
-
case "snapshot":
|
|
1282
|
-
params = { action: "snapshot", snapshot_format: tokens[1] };
|
|
1283
|
-
break;
|
|
1284
|
-
case "on":
|
|
1285
|
-
params = { action: "on" };
|
|
1286
|
-
break;
|
|
1287
|
-
case "off":
|
|
1288
|
-
params = { action: "off" };
|
|
1289
|
-
break;
|
|
1290
|
-
case "toggle":
|
|
1291
|
-
params = { action: "toggle" };
|
|
1292
|
-
break;
|
|
1293
|
-
case "refresh":
|
|
1294
|
-
params = { action: "refresh" };
|
|
1295
|
-
break;
|
|
1296
|
-
case "prefix":
|
|
1297
|
-
params = { action: "set_prefix", prefix: tokens.slice(1).join(" ") };
|
|
1298
|
-
break;
|
|
1299
|
-
case "root":
|
|
1300
|
-
params = { action: "set_root", root_issue_id: tokens.slice(1).join(" ") };
|
|
1301
|
-
break;
|
|
1302
|
-
case "tag":
|
|
1303
|
-
params = { action: "set_tag", issue_tag: tokens.slice(1).join(" ") };
|
|
1304
|
-
break;
|
|
1305
|
-
case "mode":
|
|
1306
|
-
params = { action: "set_mode", spawn_mode: tokens[1] };
|
|
1307
|
-
break;
|
|
1308
|
-
case "refresh-interval":
|
|
1309
|
-
params = { action: "set_refresh_interval", refresh_seconds: Number.parseFloat(tokens[1] ?? "") };
|
|
1310
|
-
break;
|
|
1311
|
-
case "stale-after":
|
|
1312
|
-
params = { action: "set_stale_after", stale_after_seconds: Number.parseFloat(tokens[1] ?? "") };
|
|
1313
|
-
break;
|
|
1314
|
-
case "pause": {
|
|
1315
|
-
const parsed = parseOnOff(tokens[1]);
|
|
1316
|
-
params = { action: "set_spawn_paused", spawn_paused: parsed ?? undefined };
|
|
1317
|
-
break;
|
|
1318
|
-
}
|
|
1319
|
-
case "spawn":
|
|
1320
|
-
params = {
|
|
1321
|
-
action: "spawn",
|
|
1322
|
-
count: (() => {
|
|
1323
|
-
const token = tokens[1]?.trim();
|
|
1324
|
-
if (!token || token.toLowerCase() === "all") {
|
|
1325
|
-
return "all";
|
|
1326
|
-
}
|
|
1327
|
-
const parsed = Number.parseInt(token, 10);
|
|
1328
|
-
return Number.isFinite(parsed) ? parsed : Number.NaN;
|
|
1329
|
-
})(),
|
|
1330
|
-
};
|
|
1331
|
-
break;
|
|
1332
|
-
default:
|
|
1333
|
-
notify(ctx, `Unknown subagents command: ${command}`, "error");
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
const result = await applySubagentsAction(params, ctx);
|
|
1337
|
-
if (!result.ok) {
|
|
1338
|
-
notify(ctx, result.message, result.level);
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
syncSubagentsMode(ctx, params.action);
|
|
1342
|
-
ctx.ui.notify(result.message, result.level);
|
|
1343
|
-
},
|
|
1344
|
-
});
|
|
1345
|
-
pi.registerTool({
|
|
1346
|
-
name: "mu_subagents_hud",
|
|
1347
|
-
label: "mu subagents HUD",
|
|
1348
|
-
description: "Control or inspect subagents HUD state, including tmux scope, queue filters, spawn profile, and health policies.",
|
|
1349
|
-
parameters: {
|
|
1350
|
-
type: "object",
|
|
1351
|
-
properties: {
|
|
1352
|
-
action: {
|
|
1353
|
-
type: "string",
|
|
1354
|
-
enum: [
|
|
1355
|
-
"status",
|
|
1356
|
-
"snapshot",
|
|
1357
|
-
"on",
|
|
1358
|
-
"off",
|
|
1359
|
-
"toggle",
|
|
1360
|
-
"refresh",
|
|
1361
|
-
"set_prefix",
|
|
1362
|
-
"set_root",
|
|
1363
|
-
"set_tag",
|
|
1364
|
-
"set_mode",
|
|
1365
|
-
"set_refresh_interval",
|
|
1366
|
-
"set_stale_after",
|
|
1367
|
-
"set_spawn_paused",
|
|
1368
|
-
"update",
|
|
1369
|
-
"spawn",
|
|
1370
|
-
],
|
|
1371
|
-
},
|
|
1372
|
-
prefix: { type: "string" },
|
|
1373
|
-
root_issue_id: { type: "string" },
|
|
1374
|
-
issue_tag: { type: "string" },
|
|
1375
|
-
spawn_mode: { type: "string", enum: ["operator", "researcher"] },
|
|
1376
|
-
refresh_seconds: { type: "number", minimum: MIN_REFRESH_SECONDS, maximum: MAX_REFRESH_SECONDS },
|
|
1377
|
-
stale_after_seconds: { type: "number", minimum: MIN_STALE_SECONDS, maximum: MAX_STALE_SECONDS },
|
|
1378
|
-
spawn_paused: { type: "boolean" },
|
|
1379
|
-
snapshot_format: { type: "string", enum: ["compact", "multiline"] },
|
|
1380
|
-
count: {
|
|
1381
|
-
anyOf: [
|
|
1382
|
-
{ type: "integer", minimum: 1, maximum: ISSUE_LIST_LIMIT },
|
|
1383
|
-
{ type: "string", enum: ["all"] },
|
|
1384
|
-
],
|
|
1385
|
-
},
|
|
1386
|
-
},
|
|
1387
|
-
required: ["action"],
|
|
1388
|
-
additionalProperties: false,
|
|
1389
|
-
},
|
|
1390
|
-
execute: async (_toolCallId, paramsRaw, _signal, _onUpdate, ctx) => {
|
|
1391
|
-
activeCtx = ctx;
|
|
1392
|
-
const params = paramsRaw;
|
|
1393
|
-
const result = await applySubagentsAction(params, ctx);
|
|
1394
|
-
if (!result.ok) {
|
|
1395
|
-
return subagentsToolError(result.message, state);
|
|
1396
|
-
}
|
|
1397
|
-
syncSubagentsMode(ctx, params.action);
|
|
1398
|
-
return {
|
|
1399
|
-
content: [{ type: "text", text: result.message }],
|
|
1400
|
-
details: {
|
|
1401
|
-
ok: true,
|
|
1402
|
-
action: params.action,
|
|
1403
|
-
...subagentsDetails(state),
|
|
1404
|
-
},
|
|
1405
|
-
};
|
|
1406
|
-
},
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
export default subagentsUiExtension;
|