@femtomc/mu-agent 26.2.86 → 26.2.87
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/dist/extensions/planning-ui.d.ts.map +1 -1
- package/dist/extensions/planning-ui.js +205 -82
- package/dist/extensions/subagents-ui.d.ts.map +1 -1
- package/dist/extensions/subagents-ui.js +339 -196
- package/package.json +2 -2
- package/prompts/skills/planning/SKILL.md +14 -0
- package/prompts/skills/subagents/SKILL.md +17 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planning-ui.d.ts","sourceRoot":"","sources":["../../src/extensions/planning-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"planning-ui.d.ts","sourceRoot":"","sources":["../../src/extensions/planning-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AA4LpF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QA8KnD;AAED,eAAe,mBAAmB,CAAC"}
|
|
@@ -5,6 +5,33 @@ const DEFAULT_STEPS = [
|
|
|
5
5
|
"Present plan with IDs, ordering, risks",
|
|
6
6
|
"Refine with user feedback until approved",
|
|
7
7
|
];
|
|
8
|
+
const BAR_CHARS = ["░", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
9
|
+
function phaseTone(phase) {
|
|
10
|
+
switch (phase) {
|
|
11
|
+
case "investigating":
|
|
12
|
+
return "dim";
|
|
13
|
+
case "drafting":
|
|
14
|
+
return "accent";
|
|
15
|
+
case "reviewing":
|
|
16
|
+
return "warning";
|
|
17
|
+
case "approved":
|
|
18
|
+
return "success";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function progressBar(done, total, width = 10) {
|
|
22
|
+
if (width <= 0 || total <= 0) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
const clampedDone = Math.max(0, Math.min(total, done));
|
|
26
|
+
const filled = (clampedDone / total) * width;
|
|
27
|
+
const full = Math.floor(filled);
|
|
28
|
+
const frac = filled - full;
|
|
29
|
+
const fracIdx = Math.round(frac * (BAR_CHARS.length - 1));
|
|
30
|
+
const empty = width - full - (fracIdx > 0 ? 1 : 0);
|
|
31
|
+
return (BAR_CHARS[BAR_CHARS.length - 1].repeat(full) +
|
|
32
|
+
(fracIdx > 0 ? BAR_CHARS[fracIdx] : "") +
|
|
33
|
+
BAR_CHARS[0].repeat(Math.max(0, empty)));
|
|
34
|
+
}
|
|
8
35
|
function createDefaultState() {
|
|
9
36
|
return {
|
|
10
37
|
enabled: false,
|
|
@@ -37,17 +64,28 @@ function renderPlanningUi(ctx, state) {
|
|
|
37
64
|
const done = state.steps.filter((step) => step.done).length;
|
|
38
65
|
const total = state.steps.length;
|
|
39
66
|
const phase = summarizePhase(state.phase);
|
|
40
|
-
const
|
|
41
|
-
|
|
67
|
+
const phaseColor = phaseTone(state.phase);
|
|
68
|
+
const rootLabel = state.rootIssueId ?? "(unset)";
|
|
69
|
+
const meter = progressBar(done, total, 10);
|
|
70
|
+
ctx.ui.setStatus("mu-planning", [
|
|
71
|
+
ctx.ui.theme.fg("dim", "plan"),
|
|
72
|
+
ctx.ui.theme.fg(phaseColor, phase),
|
|
73
|
+
ctx.ui.theme.fg("dim", `${done}/${total}`),
|
|
74
|
+
ctx.ui.theme.fg(phaseColor, meter),
|
|
75
|
+
ctx.ui.theme.fg("muted", `root:${rootLabel}`),
|
|
76
|
+
].join(` ${ctx.ui.theme.fg("muted", "·")} `));
|
|
42
77
|
const lines = [
|
|
43
|
-
ctx.ui.theme.fg("accent",
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Planning board")),
|
|
79
|
+
` ${ctx.ui.theme.fg("muted", "phase:")} ${ctx.ui.theme.fg(phaseColor, phase)}`,
|
|
80
|
+
` ${ctx.ui.theme.fg("muted", "progress:")} ${ctx.ui.theme.fg("dim", `${done}/${total}`)} ${ctx.ui.theme.fg(phaseColor, meter)}`,
|
|
81
|
+
` ${ctx.ui.theme.fg("muted", "root:")} ${ctx.ui.theme.fg("dim", rootLabel)}`,
|
|
82
|
+
` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`,
|
|
47
83
|
...state.steps.map((step, index) => {
|
|
48
84
|
const mark = step.done ? ctx.ui.theme.fg("success", "☑") : ctx.ui.theme.fg("muted", "☐");
|
|
49
|
-
|
|
85
|
+
const label = step.done ? ctx.ui.theme.fg("dim", step.label) : ctx.ui.theme.fg("text", step.label);
|
|
86
|
+
return `${mark} ${ctx.ui.theme.fg("muted", `${index + 1}.`)} ${label}`;
|
|
50
87
|
}),
|
|
88
|
+
ctx.ui.theme.fg("muted", " /mu plan status · /mu plan phase <...>"),
|
|
51
89
|
];
|
|
52
90
|
ctx.ui.setWidget("mu-planning", lines, { placement: "belowEditor" });
|
|
53
91
|
}
|
|
@@ -67,6 +105,32 @@ function parsePlanningPhase(raw) {
|
|
|
67
105
|
}
|
|
68
106
|
return null;
|
|
69
107
|
}
|
|
108
|
+
function planningDetails(state) {
|
|
109
|
+
return {
|
|
110
|
+
enabled: state.enabled,
|
|
111
|
+
phase: state.phase,
|
|
112
|
+
root_issue_id: state.rootIssueId,
|
|
113
|
+
steps: state.steps.map((step, index) => ({
|
|
114
|
+
index: index + 1,
|
|
115
|
+
label: step.label,
|
|
116
|
+
done: step.done,
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function planningStatusSummary(state) {
|
|
121
|
+
const done = state.steps.filter((step) => step.done).length;
|
|
122
|
+
const root = state.rootIssueId ?? "(unset)";
|
|
123
|
+
return `Planning HUD: ${state.enabled ? "enabled" : "disabled"}\nphase: ${state.phase}\nroot: ${root}\nsteps: ${done}/${state.steps.length}`;
|
|
124
|
+
}
|
|
125
|
+
function planningToolError(message) {
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: message }],
|
|
128
|
+
details: {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: message,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
70
134
|
export function planningUiExtension(pi) {
|
|
71
135
|
let state = createDefaultState();
|
|
72
136
|
const notify = (ctx, message, level = "info") => {
|
|
@@ -75,6 +139,66 @@ export function planningUiExtension(pi) {
|
|
|
75
139
|
const refresh = (ctx) => {
|
|
76
140
|
renderPlanningUi(ctx, state);
|
|
77
141
|
};
|
|
142
|
+
const applyPlanningAction = (params) => {
|
|
143
|
+
switch (params.action) {
|
|
144
|
+
case "status":
|
|
145
|
+
return { ok: true, message: planningStatusSummary(state), level: "info" };
|
|
146
|
+
case "on":
|
|
147
|
+
state.enabled = true;
|
|
148
|
+
return { ok: true, message: "Planning HUD enabled.", level: "info" };
|
|
149
|
+
case "off":
|
|
150
|
+
state.enabled = false;
|
|
151
|
+
return { ok: true, message: "Planning HUD disabled.", level: "info" };
|
|
152
|
+
case "toggle":
|
|
153
|
+
state.enabled = !state.enabled;
|
|
154
|
+
return { ok: true, message: `Planning HUD ${state.enabled ? "enabled" : "disabled"}.`, level: "info" };
|
|
155
|
+
case "reset":
|
|
156
|
+
state = createDefaultState();
|
|
157
|
+
return { ok: true, message: "Planning HUD state reset.", level: "info" };
|
|
158
|
+
case "phase": {
|
|
159
|
+
const phase = parsePlanningPhase(params.phase ?? "");
|
|
160
|
+
if (!phase) {
|
|
161
|
+
return { ok: false, message: "Invalid phase.", level: "error" };
|
|
162
|
+
}
|
|
163
|
+
state.phase = phase;
|
|
164
|
+
state.enabled = true;
|
|
165
|
+
return { ok: true, message: `Planning phase set to ${phase}.`, level: "info" };
|
|
166
|
+
}
|
|
167
|
+
case "root": {
|
|
168
|
+
const root = params.root_issue_id?.trim();
|
|
169
|
+
if (!root) {
|
|
170
|
+
return { ok: false, message: "Missing root issue id.", level: "error" };
|
|
171
|
+
}
|
|
172
|
+
state.rootIssueId = root.toLowerCase() === "clear" ? null : root;
|
|
173
|
+
state.enabled = true;
|
|
174
|
+
return { ok: true, message: `Planning root set to ${state.rootIssueId ?? "(unset)"}.`, level: "info" };
|
|
175
|
+
}
|
|
176
|
+
case "check":
|
|
177
|
+
case "uncheck":
|
|
178
|
+
case "toggle_step": {
|
|
179
|
+
const step = params.step;
|
|
180
|
+
if (typeof step !== "number" || !Number.isFinite(step)) {
|
|
181
|
+
return { ok: false, message: "Step index must be a number.", level: "error" };
|
|
182
|
+
}
|
|
183
|
+
const parsed = Math.trunc(step);
|
|
184
|
+
if (parsed < 1 || parsed > state.steps.length) {
|
|
185
|
+
return { ok: false, message: `Step index out of range (1-${state.steps.length}).`, level: "error" };
|
|
186
|
+
}
|
|
187
|
+
const index = parsed - 1;
|
|
188
|
+
if (params.action === "check") {
|
|
189
|
+
state.steps[index].done = true;
|
|
190
|
+
}
|
|
191
|
+
else if (params.action === "uncheck") {
|
|
192
|
+
state.steps[index].done = false;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
state.steps[index].done = !state.steps[index].done;
|
|
196
|
+
}
|
|
197
|
+
state.enabled = true;
|
|
198
|
+
return { ok: true, message: `Planning step ${index + 1} updated.`, level: "info" };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
78
202
|
pi.on("session_start", async (_event, ctx) => {
|
|
79
203
|
refresh(ctx);
|
|
80
204
|
});
|
|
@@ -90,89 +214,88 @@ export function planningUiExtension(pi) {
|
|
|
90
214
|
.trim()
|
|
91
215
|
.split(/\s+/)
|
|
92
216
|
.filter((token) => token.length > 0);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ctx.ui.notify(`Planning HUD: ${state.enabled ? "enabled" : "disabled"}\nphase: ${state.phase}\nroot: ${root}\nsteps: ${done}/${state.steps.length}`, "info");
|
|
97
|
-
refresh(ctx);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
switch (tokens[0]) {
|
|
217
|
+
const command = tokens[0] ?? "status";
|
|
218
|
+
let params;
|
|
219
|
+
switch (command) {
|
|
101
220
|
case "on":
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
ctx.ui.notify("Planning HUD enabled.", "info");
|
|
105
|
-
return;
|
|
221
|
+
params = { action: "on" };
|
|
222
|
+
break;
|
|
106
223
|
case "off":
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
ctx.ui.notify("Planning HUD disabled.", "info");
|
|
110
|
-
return;
|
|
224
|
+
params = { action: "off" };
|
|
225
|
+
break;
|
|
111
226
|
case "toggle":
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
227
|
+
params = { action: "toggle" };
|
|
228
|
+
break;
|
|
229
|
+
case "status":
|
|
230
|
+
params = { action: "status" };
|
|
231
|
+
break;
|
|
116
232
|
case "reset":
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
state.phase = phase;
|
|
128
|
-
state.enabled = true;
|
|
129
|
-
refresh(ctx);
|
|
130
|
-
ctx.ui.notify(`Planning phase set to ${phase}.`, "info");
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
case "root": {
|
|
134
|
-
const value = (tokens[1] ?? "").trim();
|
|
135
|
-
if (!value) {
|
|
136
|
-
notify(ctx, "Missing root issue id.", "error");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
state.rootIssueId = value.toLowerCase() === "clear" ? null : value;
|
|
140
|
-
state.enabled = true;
|
|
141
|
-
refresh(ctx);
|
|
142
|
-
ctx.ui.notify(`Planning root set to ${state.rootIssueId ?? "(unset)"}.`, "info");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
233
|
+
params = { action: "reset" };
|
|
234
|
+
break;
|
|
235
|
+
case "phase":
|
|
236
|
+
params = { action: "phase", phase: tokens[1] };
|
|
237
|
+
break;
|
|
238
|
+
case "root":
|
|
239
|
+
params = { action: "root", root_issue_id: tokens[1] };
|
|
240
|
+
break;
|
|
145
241
|
case "check":
|
|
242
|
+
params = { action: "check", step: Number.parseInt(tokens[1] ?? "", 10) };
|
|
243
|
+
break;
|
|
146
244
|
case "uncheck":
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (parsed < 1 || parsed > state.steps.length) {
|
|
155
|
-
notify(ctx, `Step index out of range (1-${state.steps.length}).`, "error");
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const index = parsed - 1;
|
|
159
|
-
if (tokens[0] === "check") {
|
|
160
|
-
state.steps[index].done = true;
|
|
161
|
-
}
|
|
162
|
-
else if (tokens[0] === "uncheck") {
|
|
163
|
-
state.steps[index].done = false;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
state.steps[index].done = !state.steps[index].done;
|
|
167
|
-
}
|
|
168
|
-
state.enabled = true;
|
|
169
|
-
refresh(ctx);
|
|
170
|
-
ctx.ui.notify(`Planning step ${index + 1} updated.`, "info");
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
245
|
+
params = { action: "uncheck", step: Number.parseInt(tokens[1] ?? "", 10) };
|
|
246
|
+
break;
|
|
247
|
+
case "toggle-step":
|
|
248
|
+
params = { action: "toggle_step", step: Number.parseInt(tokens[1] ?? "", 10) };
|
|
249
|
+
break;
|
|
173
250
|
default:
|
|
174
|
-
notify(ctx, `Unknown plan command: ${
|
|
251
|
+
notify(ctx, `Unknown plan command: ${command}`, "error");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const result = applyPlanningAction(params);
|
|
255
|
+
refresh(ctx);
|
|
256
|
+
if (!result.ok) {
|
|
257
|
+
notify(ctx, result.message, result.level ?? "error");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
ctx.ui.notify(result.message, result.level ?? "info");
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
pi.registerTool({
|
|
264
|
+
name: "mu_planning_hud",
|
|
265
|
+
label: "mu planning HUD",
|
|
266
|
+
description: "Control or inspect planning HUD state (phase, root issue, checklist progress).",
|
|
267
|
+
parameters: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
action: {
|
|
271
|
+
type: "string",
|
|
272
|
+
enum: ["status", "on", "off", "toggle", "reset", "phase", "root", "check", "uncheck", "toggle_step"],
|
|
273
|
+
},
|
|
274
|
+
phase: {
|
|
275
|
+
type: "string",
|
|
276
|
+
enum: ["investigating", "drafting", "reviewing", "approved"],
|
|
277
|
+
},
|
|
278
|
+
root_issue_id: { type: "string" },
|
|
279
|
+
step: { type: "integer", minimum: 1 },
|
|
280
|
+
},
|
|
281
|
+
required: ["action"],
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
},
|
|
284
|
+
execute: async (_toolCallId, paramsRaw, _signal, _onUpdate, ctx) => {
|
|
285
|
+
const params = paramsRaw;
|
|
286
|
+
const result = applyPlanningAction(params);
|
|
287
|
+
refresh(ctx);
|
|
288
|
+
if (!result.ok) {
|
|
289
|
+
return planningToolError(result.message);
|
|
175
290
|
}
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: "text", text: `${result.message}\n\n${planningStatusSummary(state)}` }],
|
|
293
|
+
details: {
|
|
294
|
+
ok: true,
|
|
295
|
+
action: params.action,
|
|
296
|
+
...planningDetails(state),
|
|
297
|
+
},
|
|
298
|
+
};
|
|
176
299
|
},
|
|
177
300
|
});
|
|
178
301
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagents-ui.d.ts","sourceRoot":"","sources":["../../src/extensions/subagents-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"subagents-ui.d.ts","sourceRoot":"","sources":["../../src/extensions/subagents-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AA+fpF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,QAwWpD;AAED,eAAe,oBAAoB,CAAC"}
|
|
@@ -251,10 +251,37 @@ async function listIssueSlices(rootId, roleTag) {
|
|
|
251
251
|
error: null,
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
|
-
function formatIssueLine(ctx, issue) {
|
|
254
|
+
function formatIssueLine(ctx, issue, opts = {}) {
|
|
255
|
+
const marker = opts.marker ?? "•";
|
|
256
|
+
const tone = opts.tone ?? "accent";
|
|
255
257
|
const id = ctx.ui.theme.fg("dim", issue.id);
|
|
256
258
|
const priority = ctx.ui.theme.fg("muted", `p${issue.priority}`);
|
|
257
|
-
|
|
259
|
+
const title = ctx.ui.theme.fg("text", truncateOneLine(issue.title));
|
|
260
|
+
return ` ${ctx.ui.theme.fg(tone, marker)} ${id} ${priority} ${title}`;
|
|
261
|
+
}
|
|
262
|
+
function queueMeter(value, total, width = 10) {
|
|
263
|
+
if (width <= 0 || total <= 0) {
|
|
264
|
+
return "";
|
|
265
|
+
}
|
|
266
|
+
const clamped = Math.max(0, Math.min(total, value));
|
|
267
|
+
const full = Math.floor((clamped / total) * width);
|
|
268
|
+
const empty = Math.max(0, width - full);
|
|
269
|
+
return "█".repeat(full) + "░".repeat(empty);
|
|
270
|
+
}
|
|
271
|
+
function formatRefreshAge(lastUpdatedMs) {
|
|
272
|
+
if (lastUpdatedMs == null) {
|
|
273
|
+
return "never";
|
|
274
|
+
}
|
|
275
|
+
const deltaSec = Math.max(0, Math.round((Date.now() - lastUpdatedMs) / 1000));
|
|
276
|
+
if (deltaSec < 60) {
|
|
277
|
+
return `${deltaSec}s ago`;
|
|
278
|
+
}
|
|
279
|
+
const mins = Math.floor(deltaSec / 60);
|
|
280
|
+
if (mins < 60) {
|
|
281
|
+
return `${mins}m ago`;
|
|
282
|
+
}
|
|
283
|
+
const hours = Math.floor(mins / 60);
|
|
284
|
+
return `${hours}h ago`;
|
|
258
285
|
}
|
|
259
286
|
function renderSubagentsUi(ctx, state) {
|
|
260
287
|
if (!ctx.hasUI) {
|
|
@@ -265,57 +292,69 @@ function renderSubagentsUi(ctx, state) {
|
|
|
265
292
|
ctx.ui.setWidget("mu-subagents", undefined);
|
|
266
293
|
return;
|
|
267
294
|
}
|
|
268
|
-
const issueScope = state.issueRootId ? `root
|
|
295
|
+
const issueScope = state.issueRootId ? `root:${state.issueRootId}` : "all-roots";
|
|
269
296
|
const roleScope = state.issueRoleTag ? state.issueRoleTag : "(all roles)";
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
297
|
+
const hasError = Boolean(state.sessionError || state.issueError);
|
|
298
|
+
const healthColor = hasError ? "warning" : "success";
|
|
299
|
+
const healthLabel = hasError ? "degraded" : "healthy";
|
|
300
|
+
const queueTotal = state.readyIssues.length + state.activeIssues.length;
|
|
301
|
+
const queueBar = queueMeter(state.activeIssues.length, Math.max(1, queueTotal), 10);
|
|
302
|
+
const refreshAge = formatRefreshAge(state.lastUpdatedMs);
|
|
303
|
+
ctx.ui.setStatus("mu-subagents", [
|
|
304
|
+
ctx.ui.theme.fg("dim", "subagents"),
|
|
305
|
+
ctx.ui.theme.fg(healthColor, healthLabel),
|
|
306
|
+
ctx.ui.theme.fg("dim", `${state.sessions.length} tmux`),
|
|
307
|
+
ctx.ui.theme.fg("dim", `${state.readyIssues.length} ready/${state.activeIssues.length} active`),
|
|
308
|
+
ctx.ui.theme.fg("muted", issueScope),
|
|
309
|
+
].join(` ${ctx.ui.theme.fg("muted", "·")} `));
|
|
277
310
|
const lines = [
|
|
278
|
-
ctx.ui.theme.fg("accent", "Subagents
|
|
279
|
-
ctx.ui.theme.fg("
|
|
280
|
-
ctx.ui.theme.fg("
|
|
311
|
+
ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Subagents board")),
|
|
312
|
+
` ${ctx.ui.theme.fg("muted", "health:")} ${ctx.ui.theme.fg(healthColor, healthLabel)}`,
|
|
313
|
+
` ${ctx.ui.theme.fg("muted", "scope:")} ${ctx.ui.theme.fg("dim", `${issueScope} · ${roleScope}`)}`,
|
|
314
|
+
` ${ctx.ui.theme.fg("muted", "tmux prefix:")} ${ctx.ui.theme.fg("dim", state.prefix || "(all sessions)")}`,
|
|
315
|
+
` ${ctx.ui.theme.fg("muted", "queues:")} ${ctx.ui.theme.fg("accent", `${state.readyIssues.length} ready`)} ${ctx.ui.theme.fg("muted", "| ")} ${ctx.ui.theme.fg("warning", `${state.activeIssues.length} active`)} ${ctx.ui.theme.fg("dim", queueBar)}`,
|
|
316
|
+
` ${ctx.ui.theme.fg("muted", "last refresh:")} ${ctx.ui.theme.fg("dim", refreshAge)}`,
|
|
317
|
+
` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`,
|
|
318
|
+
ctx.ui.theme.fg("accent", `tmux sessions (${state.sessions.length})`),
|
|
281
319
|
];
|
|
282
320
|
if (state.sessionError) {
|
|
283
321
|
lines.push(ctx.ui.theme.fg("warning", ` tmux error: ${state.sessionError}`));
|
|
284
322
|
}
|
|
285
323
|
else if (state.sessions.length === 0) {
|
|
286
|
-
lines.push(ctx.ui.theme.fg("muted", "
|
|
324
|
+
lines.push(ctx.ui.theme.fg("muted", " (no matching sessions)"));
|
|
287
325
|
}
|
|
288
326
|
else {
|
|
289
327
|
for (const name of state.sessions.slice(0, 8)) {
|
|
290
|
-
lines.push(` ${ctx.ui.theme.fg("success", "●")} ${name}`);
|
|
328
|
+
lines.push(` ${ctx.ui.theme.fg("success", "●")} ${ctx.ui.theme.fg("text", name)}`);
|
|
291
329
|
}
|
|
292
330
|
if (state.sessions.length > 8) {
|
|
293
331
|
lines.push(ctx.ui.theme.fg("muted", ` ... +${state.sessions.length - 8} more tmux sessions`));
|
|
294
332
|
}
|
|
295
333
|
}
|
|
334
|
+
lines.push(` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`);
|
|
296
335
|
if (state.issueError) {
|
|
297
|
-
lines.push(ctx.ui.theme.fg("warning", `
|
|
336
|
+
lines.push(ctx.ui.theme.fg("warning", `issue error: ${state.issueError}`));
|
|
298
337
|
}
|
|
299
338
|
else {
|
|
300
|
-
lines.push(ctx.ui.theme.fg("accent",
|
|
339
|
+
lines.push(ctx.ui.theme.fg("accent", `ready queue (${state.readyIssues.length})`));
|
|
301
340
|
if (state.readyIssues.length === 0) {
|
|
302
341
|
lines.push(ctx.ui.theme.fg("muted", " (no ready issues)"));
|
|
303
342
|
}
|
|
304
343
|
else {
|
|
305
344
|
for (const issue of state.readyIssues.slice(0, 6)) {
|
|
306
|
-
lines.push(formatIssueLine(ctx, issue));
|
|
345
|
+
lines.push(formatIssueLine(ctx, issue, { marker: "→", tone: "accent" }));
|
|
307
346
|
}
|
|
308
347
|
if (state.readyIssues.length > 6) {
|
|
309
348
|
lines.push(ctx.ui.theme.fg("muted", ` ... +${state.readyIssues.length - 6} more ready issues`));
|
|
310
349
|
}
|
|
311
350
|
}
|
|
312
|
-
lines.push(ctx.ui.theme.fg("accent",
|
|
351
|
+
lines.push(ctx.ui.theme.fg("accent", `active queue (${state.activeIssues.length})`));
|
|
313
352
|
if (state.activeIssues.length === 0) {
|
|
314
353
|
lines.push(ctx.ui.theme.fg("muted", " (no in-progress issues)"));
|
|
315
354
|
}
|
|
316
355
|
else {
|
|
317
356
|
for (const issue of state.activeIssues.slice(0, 6)) {
|
|
318
|
-
lines.push(formatIssueLine(ctx, issue));
|
|
357
|
+
lines.push(formatIssueLine(ctx, issue, { marker: "●", tone: "warning" }));
|
|
319
358
|
}
|
|
320
359
|
if (state.activeIssues.length > 6) {
|
|
321
360
|
lines.push(ctx.ui.theme.fg("muted", ` ... +${state.activeIssues.length - 6} more active issues`));
|
|
@@ -344,6 +383,30 @@ function normalizeRoleTag(raw) {
|
|
|
344
383
|
}
|
|
345
384
|
return trimmed;
|
|
346
385
|
}
|
|
386
|
+
function subagentsDetails(state) {
|
|
387
|
+
return {
|
|
388
|
+
enabled: state.enabled,
|
|
389
|
+
prefix: state.prefix,
|
|
390
|
+
issue_root_id: state.issueRootId,
|
|
391
|
+
issue_role_tag: state.issueRoleTag,
|
|
392
|
+
sessions: [...state.sessions],
|
|
393
|
+
ready_issue_ids: state.readyIssues.map((issue) => issue.id),
|
|
394
|
+
active_issue_ids: state.activeIssues.map((issue) => issue.id),
|
|
395
|
+
issue_error: state.issueError,
|
|
396
|
+
session_error: state.sessionError,
|
|
397
|
+
last_updated_ms: state.lastUpdatedMs,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function subagentsToolError(message, state) {
|
|
401
|
+
return {
|
|
402
|
+
content: [{ type: "text", text: message }],
|
|
403
|
+
details: {
|
|
404
|
+
ok: false,
|
|
405
|
+
error: message,
|
|
406
|
+
...subagentsDetails(state),
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
347
410
|
export function subagentsUiExtension(pi) {
|
|
348
411
|
let activeCtx = null;
|
|
349
412
|
let pollTimer = null;
|
|
@@ -386,6 +449,180 @@ export function subagentsUiExtension(pi) {
|
|
|
386
449
|
const notify = (ctx, message, level = "info") => {
|
|
387
450
|
ctx.ui.notify(`${message}\n\n${subagentsUsageText()}`, level);
|
|
388
451
|
};
|
|
452
|
+
const statusSummary = () => {
|
|
453
|
+
const when = state.lastUpdatedMs == null ? "never" : new Date(state.lastUpdatedMs).toLocaleTimeString();
|
|
454
|
+
const status = state.enabled ? "enabled" : "disabled";
|
|
455
|
+
const issueScope = state.issueRootId ?? "(all roots)";
|
|
456
|
+
const issueRole = state.issueRoleTag ?? "(all roles)";
|
|
457
|
+
const issueError = state.issueError ? `\nissue_error: ${state.issueError}` : "";
|
|
458
|
+
const tmuxError = state.sessionError ? `\ntmux_error: ${state.sessionError}` : "";
|
|
459
|
+
return {
|
|
460
|
+
level: state.issueError || state.sessionError ? "warning" : "info",
|
|
461
|
+
text: [
|
|
462
|
+
`Subagents monitor ${status}`,
|
|
463
|
+
`prefix: ${state.prefix || "(all sessions)"}`,
|
|
464
|
+
`issue_root: ${issueScope}`,
|
|
465
|
+
`issue_role: ${issueRole}`,
|
|
466
|
+
`sessions: ${state.sessions.length}`,
|
|
467
|
+
`ready_issues: ${state.readyIssues.length}`,
|
|
468
|
+
`active_issues: ${state.activeIssues.length}`,
|
|
469
|
+
`last refresh: ${when}`,
|
|
470
|
+
].join("\n") + issueError + tmuxError,
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
const applySubagentsAction = async (params, ctx) => {
|
|
474
|
+
switch (params.action) {
|
|
475
|
+
case "status": {
|
|
476
|
+
const summary = statusSummary();
|
|
477
|
+
return { ok: true, message: summary.text, level: summary.level };
|
|
478
|
+
}
|
|
479
|
+
case "on":
|
|
480
|
+
state.enabled = true;
|
|
481
|
+
ensurePolling();
|
|
482
|
+
await refresh(ctx);
|
|
483
|
+
return { ok: true, message: "Subagents monitor enabled.", level: "info" };
|
|
484
|
+
case "off":
|
|
485
|
+
state.enabled = false;
|
|
486
|
+
stopPolling();
|
|
487
|
+
renderSubagentsUi(ctx, state);
|
|
488
|
+
return { ok: true, message: "Subagents monitor disabled.", level: "info" };
|
|
489
|
+
case "toggle":
|
|
490
|
+
state.enabled = !state.enabled;
|
|
491
|
+
if (state.enabled) {
|
|
492
|
+
ensurePolling();
|
|
493
|
+
await refresh(ctx);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
stopPolling();
|
|
497
|
+
renderSubagentsUi(ctx, state);
|
|
498
|
+
}
|
|
499
|
+
return { ok: true, message: `Subagents monitor ${state.enabled ? "enabled" : "disabled"}.`, level: "info" };
|
|
500
|
+
case "refresh":
|
|
501
|
+
await refresh(ctx);
|
|
502
|
+
return { ok: true, message: "Subagents monitor refreshed.", level: "info" };
|
|
503
|
+
case "set_prefix": {
|
|
504
|
+
const value = params.prefix?.trim();
|
|
505
|
+
if (!value) {
|
|
506
|
+
return { ok: false, message: "Missing prefix value.", level: "error" };
|
|
507
|
+
}
|
|
508
|
+
state.prefix = value.toLowerCase() === "clear" ? "" : value;
|
|
509
|
+
state.enabled = true;
|
|
510
|
+
ensurePolling();
|
|
511
|
+
await refresh(ctx);
|
|
512
|
+
return { ok: true, message: `Subagents prefix set to ${state.prefix || "(all sessions)"}.`, level: "info" };
|
|
513
|
+
}
|
|
514
|
+
case "set_root": {
|
|
515
|
+
const value = params.root_issue_id?.trim();
|
|
516
|
+
if (!value) {
|
|
517
|
+
return { ok: false, message: "Missing root issue id.", level: "error" };
|
|
518
|
+
}
|
|
519
|
+
state.issueRootId = value.toLowerCase() === "clear" ? null : value;
|
|
520
|
+
state.enabled = true;
|
|
521
|
+
ensurePolling();
|
|
522
|
+
await refresh(ctx);
|
|
523
|
+
return { ok: true, message: `Subagents root set to ${state.issueRootId ?? "(all roots)"}.`, level: "info" };
|
|
524
|
+
}
|
|
525
|
+
case "set_role": {
|
|
526
|
+
const value = params.role_tag?.trim();
|
|
527
|
+
if (!value) {
|
|
528
|
+
return { ok: false, message: "Missing role/tag value.", level: "error" };
|
|
529
|
+
}
|
|
530
|
+
state.issueRoleTag = normalizeRoleTag(value);
|
|
531
|
+
state.enabled = true;
|
|
532
|
+
ensurePolling();
|
|
533
|
+
await refresh(ctx);
|
|
534
|
+
return { ok: true, message: `Subagents issue tag filter set to ${state.issueRoleTag ?? "(all roles)"}.`, level: "info" };
|
|
535
|
+
}
|
|
536
|
+
case "spawn": {
|
|
537
|
+
if (!state.issueRootId) {
|
|
538
|
+
return {
|
|
539
|
+
ok: false,
|
|
540
|
+
message: "Set a root first (`/mu subagents root <root-id>`) before spawning.",
|
|
541
|
+
level: "error",
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
let spawnLimit = null;
|
|
545
|
+
if (params.count != null && params.count !== "all") {
|
|
546
|
+
const countNum = typeof params.count === "number" ? params.count : Number.parseInt(String(params.count), 10);
|
|
547
|
+
const parsed = Math.trunc(countNum);
|
|
548
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > ISSUE_LIST_LIMIT) {
|
|
549
|
+
return { ok: false, message: `Spawn count must be 1-${ISSUE_LIST_LIMIT} or 'all'.`, level: "error" };
|
|
550
|
+
}
|
|
551
|
+
spawnLimit = parsed;
|
|
552
|
+
}
|
|
553
|
+
const issueSlices = await listIssueSlices(state.issueRootId, state.issueRoleTag);
|
|
554
|
+
state.readyIssues = issueSlices.ready;
|
|
555
|
+
state.activeIssues = issueSlices.active;
|
|
556
|
+
state.issueError = issueSlices.error;
|
|
557
|
+
if (issueSlices.error) {
|
|
558
|
+
state.enabled = true;
|
|
559
|
+
ensurePolling();
|
|
560
|
+
renderSubagentsUi(ctx, state);
|
|
561
|
+
return { ok: false, message: `Cannot spawn: ${issueSlices.error}`, level: "error" };
|
|
562
|
+
}
|
|
563
|
+
const candidates = spawnLimit == null ? issueSlices.ready : issueSlices.ready.slice(0, spawnLimit);
|
|
564
|
+
if (candidates.length === 0) {
|
|
565
|
+
state.enabled = true;
|
|
566
|
+
ensurePolling();
|
|
567
|
+
await refresh(ctx);
|
|
568
|
+
return { ok: true, message: "No ready issues to spawn for current root/tag filter.", level: "info" };
|
|
569
|
+
}
|
|
570
|
+
const spawnPrefix = state.prefix.length > 0 ? state.prefix : DEFAULT_PREFIX;
|
|
571
|
+
const tmux = await listTmuxSessions(spawnPrefix);
|
|
572
|
+
if (tmux.error) {
|
|
573
|
+
state.sessionError = tmux.error;
|
|
574
|
+
state.enabled = true;
|
|
575
|
+
ensurePolling();
|
|
576
|
+
renderSubagentsUi(ctx, state);
|
|
577
|
+
return { ok: false, message: `Cannot spawn: ${tmux.error}`, level: "error" };
|
|
578
|
+
}
|
|
579
|
+
const existingSessions = [...tmux.sessions];
|
|
580
|
+
const runId = spawnRunId();
|
|
581
|
+
const launched = [];
|
|
582
|
+
const skipped = [];
|
|
583
|
+
const failed = [];
|
|
584
|
+
for (const issue of candidates) {
|
|
585
|
+
if (issueHasSession(existingSessions, issue.id)) {
|
|
586
|
+
skipped.push(`${issue.id} (session exists)`);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
let sessionName = `${spawnPrefix}${runId}-${issue.id}`;
|
|
590
|
+
if (existingSessions.includes(sessionName)) {
|
|
591
|
+
let suffix = 1;
|
|
592
|
+
while (existingSessions.includes(`${sessionName}-${suffix}`)) {
|
|
593
|
+
suffix += 1;
|
|
594
|
+
}
|
|
595
|
+
sessionName = `${sessionName}-${suffix}`;
|
|
596
|
+
}
|
|
597
|
+
const spawned = await spawnIssueTmuxSession({
|
|
598
|
+
cwd: ctx.cwd,
|
|
599
|
+
sessionName,
|
|
600
|
+
issue,
|
|
601
|
+
});
|
|
602
|
+
if (spawned.ok) {
|
|
603
|
+
existingSessions.push(sessionName);
|
|
604
|
+
launched.push(`${issue.id} -> ${sessionName}`);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
failed.push(`${issue.id} (${spawned.error ?? "unknown error"})`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
state.enabled = true;
|
|
611
|
+
ensurePolling();
|
|
612
|
+
await refresh(ctx);
|
|
613
|
+
const summary = [
|
|
614
|
+
`Spawned ${launched.length}/${candidates.length} ready issue sessions.`,
|
|
615
|
+
launched.length > 0 ? `launched: ${launched.join(", ")}` : "launched: (none)",
|
|
616
|
+
`skipped: ${skipped.length}`,
|
|
617
|
+
`failed: ${failed.length}`,
|
|
618
|
+
];
|
|
619
|
+
if (failed.length > 0) {
|
|
620
|
+
summary.push(`failures: ${failed.join("; ")}`);
|
|
621
|
+
}
|
|
622
|
+
return { ok: true, message: summary.join("\n"), level: failed.length > 0 ? "warning" : "info" };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
};
|
|
389
626
|
pi.on("session_start", async (_event, ctx) => {
|
|
390
627
|
activeCtx = ctx;
|
|
391
628
|
if (state.enabled) {
|
|
@@ -414,188 +651,94 @@ export function subagentsUiExtension(pi) {
|
|
|
414
651
|
.trim()
|
|
415
652
|
.split(/\s+/)
|
|
416
653
|
.filter((token) => token.length > 0);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const tmuxError = state.sessionError ? `\ntmux_error: ${state.sessionError}` : "";
|
|
424
|
-
ctx.ui.notify([
|
|
425
|
-
`Subagents monitor ${status}`,
|
|
426
|
-
`prefix: ${state.prefix || "(all sessions)"}`,
|
|
427
|
-
`issue_root: ${issueScope}`,
|
|
428
|
-
`issue_role: ${issueRole}`,
|
|
429
|
-
`sessions: ${state.sessions.length}`,
|
|
430
|
-
`ready_issues: ${state.readyIssues.length}`,
|
|
431
|
-
`active_issues: ${state.activeIssues.length}`,
|
|
432
|
-
`last refresh: ${when}`,
|
|
433
|
-
].join("\n") + issueError + tmuxError, state.issueError || state.sessionError ? "warning" : "info");
|
|
434
|
-
renderSubagentsUi(ctx, state);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
switch (tokens[0]) {
|
|
654
|
+
const command = tokens[0] ?? "status";
|
|
655
|
+
let params;
|
|
656
|
+
switch (command) {
|
|
657
|
+
case "status":
|
|
658
|
+
params = { action: "status" };
|
|
659
|
+
break;
|
|
438
660
|
case "on":
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
await refresh(ctx);
|
|
442
|
-
ctx.ui.notify("Subagents monitor enabled.", "info");
|
|
443
|
-
return;
|
|
661
|
+
params = { action: "on" };
|
|
662
|
+
break;
|
|
444
663
|
case "off":
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
renderSubagentsUi(ctx, state);
|
|
448
|
-
ctx.ui.notify("Subagents monitor disabled.", "info");
|
|
449
|
-
return;
|
|
664
|
+
params = { action: "off" };
|
|
665
|
+
break;
|
|
450
666
|
case "toggle":
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
case "
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
state.prefix = value.toLowerCase() === "clear" ? "" : value;
|
|
474
|
-
state.enabled = true;
|
|
475
|
-
ensurePolling();
|
|
476
|
-
await refresh(ctx);
|
|
477
|
-
ctx.ui.notify(`Subagents prefix set to ${state.prefix || "(all sessions)"}.`, "info");
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
case "root": {
|
|
481
|
-
const value = tokens.slice(1).join(" ").trim();
|
|
482
|
-
if (!value) {
|
|
483
|
-
notify(ctx, "Missing root issue id.", "error");
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
state.issueRootId = value.toLowerCase() === "clear" ? null : value;
|
|
487
|
-
state.enabled = true;
|
|
488
|
-
ensurePolling();
|
|
489
|
-
await refresh(ctx);
|
|
490
|
-
ctx.ui.notify(`Subagents root set to ${state.issueRootId ?? "(all roots)"}.`, "info");
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
case "role": {
|
|
494
|
-
const value = tokens.slice(1).join(" ").trim();
|
|
495
|
-
if (!value) {
|
|
496
|
-
notify(ctx, "Missing role/tag value.", "error");
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
state.issueRoleTag = normalizeRoleTag(value);
|
|
500
|
-
state.enabled = true;
|
|
501
|
-
ensurePolling();
|
|
502
|
-
await refresh(ctx);
|
|
503
|
-
ctx.ui.notify(`Subagents issue tag filter set to ${state.issueRoleTag ?? "(all roles)"}.`, "info");
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
case "spawn": {
|
|
507
|
-
if (!state.issueRootId) {
|
|
508
|
-
notify(ctx, "Set a root first (`/mu subagents root <root-id>`) before spawning.", "error");
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
let spawnLimit = null;
|
|
512
|
-
const limitToken = tokens[1]?.trim();
|
|
513
|
-
if (limitToken && limitToken.toLowerCase() !== "all") {
|
|
514
|
-
const parsed = Number.parseInt(limitToken, 10);
|
|
515
|
-
if (!Number.isFinite(parsed) || parsed < 1 || parsed > ISSUE_LIST_LIMIT) {
|
|
516
|
-
notify(ctx, `Spawn count must be 1-${ISSUE_LIST_LIMIT} or 'all'.`, "error");
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
spawnLimit = parsed;
|
|
520
|
-
}
|
|
521
|
-
const issueSlices = await listIssueSlices(state.issueRootId, state.issueRoleTag);
|
|
522
|
-
state.readyIssues = issueSlices.ready;
|
|
523
|
-
state.activeIssues = issueSlices.active;
|
|
524
|
-
state.issueError = issueSlices.error;
|
|
525
|
-
if (issueSlices.error) {
|
|
526
|
-
state.enabled = true;
|
|
527
|
-
ensurePolling();
|
|
528
|
-
renderSubagentsUi(ctx, state);
|
|
529
|
-
notify(ctx, `Cannot spawn: ${issueSlices.error}`, "error");
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
const candidates = spawnLimit == null ? issueSlices.ready : issueSlices.ready.slice(0, spawnLimit);
|
|
533
|
-
if (candidates.length === 0) {
|
|
534
|
-
state.enabled = true;
|
|
535
|
-
ensurePolling();
|
|
536
|
-
await refresh(ctx);
|
|
537
|
-
ctx.ui.notify("No ready issues to spawn for current root/tag filter.", "info");
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
const spawnPrefix = state.prefix.length > 0 ? state.prefix : DEFAULT_PREFIX;
|
|
541
|
-
const tmux = await listTmuxSessions(spawnPrefix);
|
|
542
|
-
if (tmux.error) {
|
|
543
|
-
state.sessionError = tmux.error;
|
|
544
|
-
state.enabled = true;
|
|
545
|
-
ensurePolling();
|
|
546
|
-
renderSubagentsUi(ctx, state);
|
|
547
|
-
notify(ctx, `Cannot spawn: ${tmux.error}`, "error");
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
const existingSessions = [...tmux.sessions];
|
|
551
|
-
const runId = spawnRunId();
|
|
552
|
-
const launched = [];
|
|
553
|
-
const skipped = [];
|
|
554
|
-
const failed = [];
|
|
555
|
-
for (const issue of candidates) {
|
|
556
|
-
if (issueHasSession(existingSessions, issue.id)) {
|
|
557
|
-
skipped.push(`${issue.id} (session exists)`);
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
let sessionName = `${spawnPrefix}${runId}-${issue.id}`;
|
|
561
|
-
if (existingSessions.includes(sessionName)) {
|
|
562
|
-
let suffix = 1;
|
|
563
|
-
while (existingSessions.includes(`${sessionName}-${suffix}`)) {
|
|
564
|
-
suffix += 1;
|
|
667
|
+
params = { action: "toggle" };
|
|
668
|
+
break;
|
|
669
|
+
case "refresh":
|
|
670
|
+
params = { action: "refresh" };
|
|
671
|
+
break;
|
|
672
|
+
case "prefix":
|
|
673
|
+
params = { action: "set_prefix", prefix: tokens.slice(1).join(" ") };
|
|
674
|
+
break;
|
|
675
|
+
case "root":
|
|
676
|
+
params = { action: "set_root", root_issue_id: tokens.slice(1).join(" ") };
|
|
677
|
+
break;
|
|
678
|
+
case "role":
|
|
679
|
+
params = { action: "set_role", role_tag: tokens.slice(1).join(" ") };
|
|
680
|
+
break;
|
|
681
|
+
case "spawn":
|
|
682
|
+
params = {
|
|
683
|
+
action: "spawn",
|
|
684
|
+
count: (() => {
|
|
685
|
+
const token = tokens[1]?.trim();
|
|
686
|
+
if (!token || token.toLowerCase() === "all") {
|
|
687
|
+
return "all";
|
|
565
688
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
issue,
|
|
572
|
-
});
|
|
573
|
-
if (spawned.ok) {
|
|
574
|
-
existingSessions.push(sessionName);
|
|
575
|
-
launched.push(`${issue.id} -> ${sessionName}`);
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
failed.push(`${issue.id} (${spawned.error ?? "unknown error"})`);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
state.enabled = true;
|
|
582
|
-
ensurePolling();
|
|
583
|
-
await refresh(ctx);
|
|
584
|
-
const summary = [
|
|
585
|
-
`Spawned ${launched.length}/${candidates.length} ready issue sessions.`,
|
|
586
|
-
launched.length > 0 ? `launched: ${launched.join(", ")}` : "launched: (none)",
|
|
587
|
-
`skipped: ${skipped.length}`,
|
|
588
|
-
`failed: ${failed.length}`,
|
|
589
|
-
];
|
|
590
|
-
if (failed.length > 0) {
|
|
591
|
-
summary.push(`failures: ${failed.join("; ")}`);
|
|
592
|
-
}
|
|
593
|
-
ctx.ui.notify(summary.join("\n"), failed.length > 0 ? "warning" : "info");
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
689
|
+
const parsed = Number.parseInt(token, 10);
|
|
690
|
+
return Number.isFinite(parsed) ? parsed : Number.NaN;
|
|
691
|
+
})(),
|
|
692
|
+
};
|
|
693
|
+
break;
|
|
596
694
|
default:
|
|
597
|
-
notify(ctx, `Unknown subagents command: ${
|
|
695
|
+
notify(ctx, `Unknown subagents command: ${command}`, "error");
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const result = await applySubagentsAction(params, ctx);
|
|
699
|
+
if (!result.ok) {
|
|
700
|
+
notify(ctx, result.message, result.level);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
ctx.ui.notify(result.message, result.level);
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
pi.registerTool({
|
|
707
|
+
name: "mu_subagents_hud",
|
|
708
|
+
label: "mu subagents HUD",
|
|
709
|
+
description: "Control or inspect subagents HUD state, including tmux scope, issue queue filters, and ready-queue spawning.",
|
|
710
|
+
parameters: {
|
|
711
|
+
type: "object",
|
|
712
|
+
properties: {
|
|
713
|
+
action: {
|
|
714
|
+
type: "string",
|
|
715
|
+
enum: ["status", "on", "off", "toggle", "refresh", "set_prefix", "set_root", "set_role", "spawn"],
|
|
716
|
+
},
|
|
717
|
+
prefix: { type: "string" },
|
|
718
|
+
root_issue_id: { type: "string" },
|
|
719
|
+
role_tag: { type: "string" },
|
|
720
|
+
count: {
|
|
721
|
+
anyOf: [{ type: "integer", minimum: 1, maximum: ISSUE_LIST_LIMIT }, { type: "string", enum: ["all"] }],
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
required: ["action"],
|
|
725
|
+
additionalProperties: false,
|
|
726
|
+
},
|
|
727
|
+
execute: async (_toolCallId, paramsRaw, _signal, _onUpdate, ctx) => {
|
|
728
|
+
activeCtx = ctx;
|
|
729
|
+
const params = paramsRaw;
|
|
730
|
+
const result = await applySubagentsAction(params, ctx);
|
|
731
|
+
if (!result.ok) {
|
|
732
|
+
return subagentsToolError(result.message, state);
|
|
598
733
|
}
|
|
734
|
+
return {
|
|
735
|
+
content: [{ type: "text", text: result.message }],
|
|
736
|
+
details: {
|
|
737
|
+
ok: true,
|
|
738
|
+
action: params.action,
|
|
739
|
+
...subagentsDetails(state),
|
|
740
|
+
},
|
|
741
|
+
};
|
|
599
742
|
},
|
|
600
743
|
});
|
|
601
744
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-agent",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.87",
|
|
4
4
|
"description": "Shared agent runtime for mu assistant sessions, orchestration roles, and serve extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"themes/**"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@femtomc/mu-core": "26.2.
|
|
27
|
+
"@femtomc/mu-core": "26.2.87",
|
|
28
28
|
"@mariozechner/pi-agent-core": "^0.53.0",
|
|
29
29
|
"@mariozechner/pi-ai": "^0.53.0",
|
|
30
30
|
"@mariozechner/pi-coding-agent": "^0.53.0",
|
|
@@ -45,6 +45,20 @@ Optional planning HUD (interactive operator session):
|
|
|
45
45
|
/mu plan phase investigating
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
Tool contract (preferred when tools are available):
|
|
49
|
+
|
|
50
|
+
- Tool: `mu_planning_hud`
|
|
51
|
+
- Actions: `status`, `on`, `off`, `toggle`, `reset`, `phase`, `root`, `check`, `uncheck`, `toggle_step`
|
|
52
|
+
- Parameters:
|
|
53
|
+
- `phase`: `investigating|drafting|reviewing|approved`
|
|
54
|
+
- `root_issue_id`: issue ID or `clear`
|
|
55
|
+
- `step`: 1-based checklist index
|
|
56
|
+
|
|
57
|
+
Example tool calls:
|
|
58
|
+
- Set phase: `{"action":"phase","phase":"drafting"}`
|
|
59
|
+
- Bind root: `{"action":"root","root_issue_id":"<root-id>"}`
|
|
60
|
+
- Update checklist: `{"action":"check","step":2}`
|
|
61
|
+
|
|
48
62
|
Also inspect repo files directly (read/bash) for implementation constraints.
|
|
49
63
|
|
|
50
64
|
### B) Draft DAG in mu-issue
|
|
@@ -85,6 +85,23 @@ The widget picks up tracker decomposition by reading `mu issues ready` and
|
|
|
85
85
|
Use `spawn` to launch tmux sessions directly from the ready queue for the
|
|
86
86
|
current root/tag filter.
|
|
87
87
|
|
|
88
|
+
Tool contract (preferred when tools are available):
|
|
89
|
+
|
|
90
|
+
- Tool: `mu_subagents_hud`
|
|
91
|
+
- Actions: `status`, `on`, `off`, `toggle`, `refresh`, `set_prefix`, `set_root`, `set_role`, `spawn`
|
|
92
|
+
- Parameters:
|
|
93
|
+
- `prefix`: tmux prefix or `clear`
|
|
94
|
+
- `root_issue_id`: issue root ID or `clear`
|
|
95
|
+
- `role_tag`: issue tag filter (for example `role:worker`) or `clear`
|
|
96
|
+
- `count`: integer 1..40 or `"all"` for spawn
|
|
97
|
+
|
|
98
|
+
Example tool calls:
|
|
99
|
+
- Configure root + role:
|
|
100
|
+
- `{"action":"set_root","root_issue_id":"<root-id>"}`
|
|
101
|
+
- `{"action":"set_role","role_tag":"role:worker"}`
|
|
102
|
+
- Refresh status: `{"action":"refresh"}`
|
|
103
|
+
- Spawn from ready queue: `{"action":"spawn","count":"all"}`
|
|
104
|
+
|
|
88
105
|
## Handoffs and follow-up turns
|
|
89
106
|
|
|
90
107
|
With `mu exec`, follow up by issuing another `mu exec` command in the same tmux pane
|