@femtomc/mu-agent 26.2.89 → 26.2.90

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 CHANGED
@@ -48,8 +48,8 @@ Current stack:
48
48
 
49
49
  - `brandingExtension` — mu compact header/footer branding + default theme
50
50
  - `eventLogExtension` — event tail + watch widget
51
- - `planningUiExtension` — planning HUD with phase/checklist state plus user-facing communication metadata (`/mu plan ...`)
52
- - `subagentsUiExtension` — tmux + issue queue HUD with spawn profiles, pause controls, and queue/session health signals (`/mu subagents ...`)
51
+ - `planningUiExtension` — planning mode: compact HUD for next-step/approval flow plus footer-ready incidental status metadata (`/mu plan ...`)
52
+ - `subagentsUiExtension` — subagents mode: compact HUD with activity sentences from issue/forum events plus footer-ready queue/health metadata (`/mu subagents ...`)
53
53
 
54
54
  Default operator UI theme is `mu-gruvbox-dark`.
55
55
 
@@ -1 +1 @@
1
- {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/extensions/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAwEpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QAmOjD;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/extensions/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAyEpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QA8OjD;AAED,eAAe,iBAAiB,CAAC"}
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { basename } from "node:path";
10
10
  import { MU_DEFAULT_THEME_NAME, MU_VERSION } from "../ui_defaults.js";
11
+ import { getActiveHudMode } from "./hud-mode.js";
11
12
  import { registerMuSubcommand } from "./mu-command-dispatcher.js";
12
13
  import { fetchMuStatus, muServerUrl } from "./shared.js";
13
14
  const EMPTY_SNAPSHOT = {
@@ -127,6 +128,16 @@ export function brandingExtension(pi) {
127
128
  const barColor = pct >= 80 ? "warning" : pct >= 60 ? "muted" : "dim";
128
129
  parts.push(theme.fg("muted", "·"), theme.fg(barColor, `ctx ${pct}%`), theme.fg(barColor, contextBar(pct, 10)));
129
130
  }
131
+ const activeHudMode = getActiveHudMode();
132
+ if (activeHudMode) {
133
+ const extensionStatuses = footerData.getExtensionStatuses();
134
+ const modeMetaKey = activeHudMode === "planning" ? "mu-planning-meta" : "mu-subagents-meta";
135
+ const modeMeta = extensionStatuses.get(modeMetaKey) ?? "";
136
+ parts.push(theme.fg("muted", "·"), theme.fg("accent", `hud:${activeHudMode}`));
137
+ if (modeMeta.length > 0) {
138
+ parts.push(theme.fg("muted", "·"), theme.fg("dim", truncateToWidth(modeMeta, 42)));
139
+ }
140
+ }
130
141
  if (snapshot.openCount > 0 || snapshot.readyCount > 0) {
131
142
  parts.push(theme.fg("muted", "·"), theme.fg("dim", `open ${snapshot.openCount} ready ${snapshot.readyCount}`));
132
143
  }
@@ -0,0 +1,8 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ export type MuHudMode = "planning" | "subagents";
3
+ export declare function getActiveHudMode(): MuHudMode | null;
4
+ export declare function resetHudMode(): void;
5
+ export declare function setActiveHudMode(mode: MuHudMode | null): void;
6
+ export declare function clearHudMode(mode: MuHudMode): void;
7
+ export declare function syncHudModeStatus(ctx: ExtensionContext): void;
8
+ //# sourceMappingURL=hud-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hud-mode.d.ts","sourceRoot":"","sources":["../../src/extensions/hud-mode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;AAIjD,wBAAgB,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAEnD;AAED,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAE7D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAIlD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAK7D"}
@@ -0,0 +1,21 @@
1
+ let activeHudMode = null;
2
+ export function getActiveHudMode() {
3
+ return activeHudMode;
4
+ }
5
+ export function resetHudMode() {
6
+ activeHudMode = null;
7
+ }
8
+ export function setActiveHudMode(mode) {
9
+ activeHudMode = mode;
10
+ }
11
+ export function clearHudMode(mode) {
12
+ if (activeHudMode === mode) {
13
+ activeHudMode = null;
14
+ }
15
+ }
16
+ export function syncHudModeStatus(ctx) {
17
+ if (!ctx.hasUI) {
18
+ return;
19
+ }
20
+ ctx.ui.setStatus("mu-hud-mode", activeHudMode ? `hud:${activeHudMode}` : undefined);
21
+ }
@@ -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;AA2fpF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAydnD;AAED,eAAe,mBAAmB,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;AAigBpF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAuenD;AAED,eAAe,mBAAmB,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { clearHudMode, setActiveHudMode, syncHudModeStatus } from "./hud-mode.js";
1
2
  import { registerMuSubcommand } from "./mu-command-dispatcher.js";
2
3
  const DEFAULT_STEPS = [
3
4
  "Investigate relevant code/docs/state",
@@ -252,6 +253,7 @@ function renderPlanningUi(ctx, state) {
252
253
  }
253
254
  if (!state.enabled) {
254
255
  ctx.ui.setStatus("mu-planning", undefined);
256
+ ctx.ui.setStatus("mu-planning-meta", undefined);
255
257
  ctx.ui.setWidget("mu-planning", undefined);
256
258
  return;
257
259
  }
@@ -276,6 +278,7 @@ function renderPlanningUi(ctx, state) {
276
278
  ctx.ui.theme.fg(waitingColor, `wait:${waitingLabel}`),
277
279
  ctx.ui.theme.fg("muted", `root:${rootCompact}`),
278
280
  ].join(` ${ctx.ui.theme.fg("muted", "·")} `));
281
+ ctx.ui.setStatus("mu-planning-meta", `phase:${phase} steps:${done}/${total} wait:${waitingLabel} conf:${state.confidence}`);
279
282
  const lines = [
280
283
  [
281
284
  ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Planning")),
@@ -387,6 +390,16 @@ export function planningUiExtension(pi) {
387
390
  const refresh = (ctx) => {
388
391
  renderPlanningUi(ctx, state);
389
392
  };
393
+ const syncPlanningMode = (ctx, action) => {
394
+ const passiveAction = action === "status" || action === "snapshot";
395
+ if (!state.enabled) {
396
+ clearHudMode("planning");
397
+ }
398
+ else if (!passiveAction) {
399
+ setActiveHudMode("planning");
400
+ }
401
+ syncHudModeStatus(ctx);
402
+ };
390
403
  const applyPlanningAction = (params) => {
391
404
  switch (params.action) {
392
405
  case "status":
@@ -647,9 +660,11 @@ export function planningUiExtension(pi) {
647
660
  };
648
661
  pi.on("session_start", async (_event, ctx) => {
649
662
  refresh(ctx);
663
+ syncHudModeStatus(ctx);
650
664
  });
651
665
  pi.on("session_switch", async (_event, ctx) => {
652
666
  refresh(ctx);
667
+ syncHudModeStatus(ctx);
653
668
  });
654
669
  registerMuSubcommand(pi, {
655
670
  subcommand: "plan",
@@ -733,6 +748,7 @@ export function planningUiExtension(pi) {
733
748
  notify(ctx, result.message, result.level ?? "error");
734
749
  return;
735
750
  }
751
+ syncPlanningMode(ctx, params.action);
736
752
  ctx.ui.notify(result.message, result.level ?? "info");
737
753
  },
738
754
  });
@@ -823,6 +839,7 @@ export function planningUiExtension(pi) {
823
839
  if (!result.ok) {
824
840
  return planningToolError(result.message);
825
841
  }
842
+ syncPlanningMode(ctx, params.action);
826
843
  return {
827
844
  content: [{ type: "text", text: `${result.message}\n\n${planningStatusSummary(state)}` }],
828
845
  details: {
@@ -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;AA2uBpF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,QAwmBpD;AAED,eAAe,oBAAoB,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;AAw4BpF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,QAkoBpD;AAED,eAAe,oBAAoB,CAAC"}
@@ -1,3 +1,5 @@
1
+ import { fetchMuJson, muServerUrl } from "./shared.js";
2
+ import { clearHudMode, setActiveHudMode, syncHudModeStatus } from "./hud-mode.js";
1
3
  import { registerMuSubcommand } from "./mu-command-dispatcher.js";
2
4
  const DEFAULT_PREFIX = "mu-sub-";
3
5
  const DEFAULT_ROLE_TAG = "role:worker";
@@ -10,6 +12,12 @@ const MAX_REFRESH_SECONDS = 120;
10
12
  const DEFAULT_STALE_AFTER_MS = 60_000;
11
13
  const MIN_STALE_SECONDS = 10;
12
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;
13
21
  function shellQuote(value) {
14
22
  return `'${value.replaceAll("'", "'\"'\"'")}'`;
15
23
  }
@@ -99,6 +107,8 @@ function createDefaultState() {
99
107
  staleAfterMs: DEFAULT_STALE_AFTER_MS,
100
108
  spawnPaused: false,
101
109
  spawnMode: DEFAULT_SPAWN_MODE,
110
+ activityLines: [],
111
+ activityError: null,
102
112
  };
103
113
  }
104
114
  function truncateOneLine(input, maxLen = 68) {
@@ -295,14 +305,6 @@ async function listIssueSlices(rootId, roleTag) {
295
305
  error: null,
296
306
  };
297
307
  }
298
- function formatIssueLine(ctx, issue, opts = {}) {
299
- const marker = opts.marker ?? "•";
300
- const tone = opts.tone ?? "accent";
301
- const id = ctx.ui.theme.fg("dim", issue.id);
302
- const priority = ctx.ui.theme.fg("muted", `p${issue.priority}`);
303
- const title = ctx.ui.theme.fg("text", truncateOneLine(issue.title));
304
- return ` ${ctx.ui.theme.fg(tone, marker)} ${id} ${priority} ${title}`;
305
- }
306
308
  function queueMeter(value, total, width = 10) {
307
309
  if (width <= 0 || total <= 0) {
308
310
  return "";
@@ -333,6 +335,127 @@ function isRefreshStale(lastUpdatedMs, staleAfterMs) {
333
335
  }
334
336
  return Date.now() - lastUpdatedMs > staleAfterMs;
335
337
  }
338
+ function asRecord(value) {
339
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
340
+ return null;
341
+ }
342
+ return value;
343
+ }
344
+ function issueIdFromEvent(event) {
345
+ const issueId = typeof event.issue_id === "string" ? event.issue_id.trim() : "";
346
+ return issueId.length > 0 ? issueId : null;
347
+ }
348
+ function eventAgeLabel(tsMs) {
349
+ if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) {
350
+ return "now";
351
+ }
352
+ const ageSeconds = Math.max(0, Math.round((Date.now() - tsMs) / 1_000));
353
+ if (ageSeconds < 60) {
354
+ return `${ageSeconds}s`;
355
+ }
356
+ const mins = Math.floor(ageSeconds / 60);
357
+ if (mins < 60) {
358
+ return `${mins}m`;
359
+ }
360
+ const hours = Math.floor(mins / 60);
361
+ return `${hours}h`;
362
+ }
363
+ function renderActivitySentence(event) {
364
+ const issueId = issueIdFromEvent(event);
365
+ if (!issueId) {
366
+ return null;
367
+ }
368
+ const eventType = typeof event.type === "string" ? event.type : "";
369
+ const payload = asRecord(event.payload);
370
+ if (eventType === "forum.post") {
371
+ const message = asRecord(payload?.message);
372
+ const body = typeof message?.body === "string" ? message.body.trim() : "";
373
+ const author = typeof message?.author === "string" ? message.author.trim() : "worker";
374
+ if (body.length === 0) {
375
+ return null;
376
+ }
377
+ return {
378
+ issueId,
379
+ sentence: `${issueId} ${author}: ${truncateOneLine(body, 54)}`,
380
+ };
381
+ }
382
+ if (eventType === "issue.claim") {
383
+ const ok = payload?.ok === true;
384
+ if (ok) {
385
+ return { issueId, sentence: `${issueId} claimed and started work` };
386
+ }
387
+ const reason = typeof payload?.reason === "string" ? payload.reason : "claim failed";
388
+ return { issueId, sentence: `${issueId} claim failed (${truncateOneLine(reason, 36)})` };
389
+ }
390
+ if (eventType === "issue.close") {
391
+ const outcome = typeof payload?.outcome === "string" ? payload.outcome : "closed";
392
+ return { issueId, sentence: `${issueId} closed (${outcome})` };
393
+ }
394
+ if (eventType === "issue.update") {
395
+ const changed = asRecord(payload?.changed);
396
+ const changedKeys = changed ? Object.keys(changed) : [];
397
+ if (changedKeys.includes("status")) {
398
+ const statusChange = asRecord(changed?.status);
399
+ const from = typeof statusChange?.from === "string" ? statusChange.from : "?";
400
+ const to = typeof statusChange?.to === "string" ? statusChange.to : "?";
401
+ return { issueId, sentence: `${issueId} status ${from} → ${to}` };
402
+ }
403
+ if (changedKeys.length > 0) {
404
+ return {
405
+ issueId,
406
+ sentence: `${issueId} updated ${truncateOneLine(changedKeys.join(","), 28)}`,
407
+ };
408
+ }
409
+ }
410
+ if (eventType === "issue.open") {
411
+ return { issueId, sentence: `${issueId} reopened` };
412
+ }
413
+ return null;
414
+ }
415
+ async function fetchRecentActivity(opts) {
416
+ if (!muServerUrl()) {
417
+ return { lines: [], error: null };
418
+ }
419
+ let events;
420
+ try {
421
+ events = await fetchMuJson(`/api/control-plane/events?limit=${ACTIVITY_EVENT_LIMIT}`, {
422
+ timeoutMs: 4_000,
423
+ });
424
+ }
425
+ catch (err) {
426
+ const message = err instanceof Error ? err.message : String(err);
427
+ return { lines: [], error: `activity refresh failed: ${truncateOneLine(message, 60)}` };
428
+ }
429
+ if (!Array.isArray(events) || events.length === 0) {
430
+ return { lines: [], error: null };
431
+ }
432
+ const tracked = new Set(opts.issueIds.map((issueId) => issueId.trim()).filter((issueId) => issueId.length > 0));
433
+ const seenIssueIds = new Set();
434
+ const lines = [];
435
+ const sorted = [...events].sort((left, right) => {
436
+ const leftTs = typeof left.ts_ms === "number" ? left.ts_ms : 0;
437
+ const rightTs = typeof right.ts_ms === "number" ? right.ts_ms : 0;
438
+ return rightTs - leftTs;
439
+ });
440
+ for (const event of sorted) {
441
+ const rendered = renderActivitySentence(event);
442
+ if (!rendered) {
443
+ continue;
444
+ }
445
+ if (tracked.size > 0 && !tracked.has(rendered.issueId)) {
446
+ continue;
447
+ }
448
+ if (seenIssueIds.has(rendered.issueId)) {
449
+ continue;
450
+ }
451
+ seenIssueIds.add(rendered.issueId);
452
+ lines.push(`${eventAgeLabel(event.ts_ms)} ${rendered.sentence}`);
453
+ if (lines.length >= ACTIVITY_LINE_LIMIT) {
454
+ break;
455
+ }
456
+ }
457
+ return { lines, error: null };
458
+ }
336
459
  function computeQueueDrift(sessions, activeIssues) {
337
460
  const activeWithoutSessionIds = activeIssues
338
461
  .filter((issue) => !issueHasSession(sessions, issue.id))
@@ -406,6 +529,7 @@ function subagentsSnapshot(state, format) {
406
529
  `spawn_paused: ${paused}`,
407
530
  `queues: ${state.readyIssues.length} ready / ${state.activeIssues.length} active`,
408
531
  `sessions: ${state.sessions.length}`,
532
+ `activity_lines: ${state.activityLines.length}`,
409
533
  `drift_active_without_session: ${drift.activeWithoutSessionIds.length}`,
410
534
  `drift_orphan_sessions: ${drift.orphanSessions.length}`,
411
535
  `refresh_age: ${refreshAge}`,
@@ -425,6 +549,7 @@ function subagentsSnapshot(state, format) {
425
549
  `active=${state.activeIssues.length}`,
426
550
  `sessions=${state.sessions.length}`,
427
551
  `drift=${staleCount}`,
552
+ `activity=${state.activityLines.length}`,
428
553
  `refresh=${refreshAge}`,
429
554
  ].join(" · ");
430
555
  }
@@ -434,11 +559,14 @@ function renderSubagentsUi(ctx, state) {
434
559
  }
435
560
  if (!state.enabled) {
436
561
  ctx.ui.setStatus("mu-subagents", undefined);
562
+ ctx.ui.setStatus("mu-subagents-meta", undefined);
437
563
  ctx.ui.setWidget("mu-subagents", undefined);
438
564
  return;
439
565
  }
440
566
  const issueScope = state.issueRootId ? `root:${state.issueRootId}` : "all-roots";
441
567
  const roleScope = state.issueRoleTag ? state.issueRoleTag : "(all roles)";
568
+ const scopeCompact = truncateOneLine(`${issueScope} · ${roleScope}`, WIDGET_SCOPE_MAX);
569
+ const prefixCompact = truncateOneLine(state.prefix || "(all sessions)", WIDGET_PREFIX_MAX);
442
570
  const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
443
571
  const drift = computeQueueDrift(state.sessions, state.activeIssues);
444
572
  const staleCount = drift.activeWithoutSessionIds.length + drift.orphanSessions.length;
@@ -452,82 +580,84 @@ function renderSubagentsUi(ctx, state) {
452
580
  const pausedColor = state.spawnPaused ? "warning" : "dim";
453
581
  const refreshSeconds = Math.round(state.refreshIntervalMs / 1_000);
454
582
  const staleAfterSeconds = Math.round(state.staleAfterMs / 1_000);
583
+ const activityLines = state.activityLines.slice(0, ACTIVITY_LINE_LIMIT);
455
584
  ctx.ui.setStatus("mu-subagents", [
456
585
  ctx.ui.theme.fg("dim", "subagents"),
457
586
  ctx.ui.theme.fg(healthColor, healthLabel),
458
- ctx.ui.theme.fg("dim", `${state.spawnMode}`),
587
+ ctx.ui.theme.fg("dim", `mode:${state.spawnMode}`),
459
588
  ctx.ui.theme.fg(pausedColor, `paused:${pausedLabel}`),
460
- ctx.ui.theme.fg("dim", `${state.sessions.length} tmux`),
461
- ctx.ui.theme.fg("dim", `${state.readyIssues.length} ready/${state.activeIssues.length} active`),
462
- ctx.ui.theme.fg("muted", issueScope),
589
+ ctx.ui.theme.fg("dim", `q:${state.readyIssues.length}/${state.activeIssues.length}`),
590
+ ctx.ui.theme.fg("dim", `tmux:${state.sessions.length}`),
591
+ ctx.ui.theme.fg(staleCount > 0 ? "warning" : "dim", `drift:${staleCount}`),
592
+ ctx.ui.theme.fg("muted", truncateOneLine(issueScope, 18)),
463
593
  ].join(` ${ctx.ui.theme.fg("muted", "·")} `));
594
+ ctx.ui.setStatus("mu-subagents-meta", `health:${healthLabel} q:${state.readyIssues.length}/${state.activeIssues.length} tmux:${state.sessions.length} drift:${staleCount} refresh:${refreshAge}`);
464
595
  const lines = [
465
- ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Subagents board")),
466
- ` ${ctx.ui.theme.fg("muted", "health:")} ${ctx.ui.theme.fg(healthColor, healthLabel)}`,
467
- ` ${ctx.ui.theme.fg("muted", "scope:")} ${ctx.ui.theme.fg("dim", `${issueScope} · ${roleScope}`)}`,
468
- ` ${ctx.ui.theme.fg("muted", "tmux prefix:")} ${ctx.ui.theme.fg("dim", state.prefix || "(all sessions)")}`,
469
- ` ${ctx.ui.theme.fg("muted", "spawn mode:")} ${ctx.ui.theme.fg("accent", state.spawnMode)}`,
470
- ` ${ctx.ui.theme.fg("muted", "spawn paused:")} ${ctx.ui.theme.fg(pausedColor, pausedLabel)}`,
471
- ` ${ctx.ui.theme.fg("muted", "refresh:")} ${ctx.ui.theme.fg("dim", `${refreshSeconds}s`)} ${ctx.ui.theme.fg("muted", "| stale after:")} ${ctx.ui.theme.fg("dim", `${staleAfterSeconds}s`)}`,
472
- ` ${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)}`,
473
- ` ${ctx.ui.theme.fg("muted", "last refresh:")} ${ctx.ui.theme.fg(refreshStale ? "warning" : "dim", refreshAge)}`,
474
- ` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`,
475
- ctx.ui.theme.fg("accent", `tmux sessions (${state.sessions.length})`),
596
+ [
597
+ ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Subagents")),
598
+ ctx.ui.theme.fg("muted", "·"),
599
+ ctx.ui.theme.fg(healthColor, healthLabel),
600
+ ctx.ui.theme.fg("muted", "·"),
601
+ ctx.ui.theme.fg("accent", `mode:${state.spawnMode}`),
602
+ ctx.ui.theme.fg("muted", "·"),
603
+ ctx.ui.theme.fg(pausedColor, `paused:${pausedLabel}`),
604
+ ].join(" "),
605
+ `${ctx.ui.theme.fg("muted", "scope:")} ${ctx.ui.theme.fg("dim", scopeCompact)} ${ctx.ui.theme.fg("muted", "· prefix:")} ${ctx.ui.theme.fg("dim", prefixCompact)}`,
606
+ [
607
+ ctx.ui.theme.fg("muted", "queues:"),
608
+ ctx.ui.theme.fg("accent", `${state.readyIssues.length}r`),
609
+ ctx.ui.theme.fg("muted", "/"),
610
+ ctx.ui.theme.fg("warning", `${state.activeIssues.length}a`),
611
+ ctx.ui.theme.fg("dim", queueBar),
612
+ ctx.ui.theme.fg("muted", "·"),
613
+ ctx.ui.theme.fg("muted", "tmux:"),
614
+ ctx.ui.theme.fg("dim", `${state.sessions.length}`),
615
+ ctx.ui.theme.fg("muted", "·"),
616
+ ctx.ui.theme.fg(staleCount > 0 ? "warning" : "dim", `drift:${staleCount}`),
617
+ ].join(" "),
618
+ [
619
+ ctx.ui.theme.fg("muted", "refresh:"),
620
+ ctx.ui.theme.fg(refreshStale ? "warning" : "dim", refreshAge),
621
+ ctx.ui.theme.fg("muted", "·"),
622
+ ctx.ui.theme.fg("muted", "every:"),
623
+ ctx.ui.theme.fg("dim", `${refreshSeconds}s`),
624
+ ctx.ui.theme.fg("muted", "·"),
625
+ ctx.ui.theme.fg("muted", "stale:"),
626
+ ctx.ui.theme.fg("dim", `${staleAfterSeconds}s`),
627
+ ].join(" "),
476
628
  ];
629
+ if (state.issueError) {
630
+ lines.push(ctx.ui.theme.fg("warning", `issue_error: ${truncateOneLine(state.issueError, WIDGET_ERROR_MAX)}`));
631
+ }
477
632
  if (state.sessionError) {
478
- lines.push(ctx.ui.theme.fg("warning", ` tmux error: ${state.sessionError}`));
633
+ lines.push(ctx.ui.theme.fg("warning", `tmux_error: ${truncateOneLine(state.sessionError, WIDGET_ERROR_MAX)}`));
479
634
  }
480
- else if (state.sessions.length === 0) {
481
- lines.push(ctx.ui.theme.fg("muted", " (no matching sessions)"));
635
+ if (refreshStale) {
636
+ lines.push(ctx.ui.theme.fg("warning", `warning: refresh stale (>${staleAfterSeconds}s since last successful refresh)`));
482
637
  }
483
- else {
484
- for (const name of state.sessions.slice(0, 8)) {
485
- lines.push(` ${ctx.ui.theme.fg("success", "●")} ${ctx.ui.theme.fg("text", name)}`);
486
- }
487
- if (state.sessions.length > 8) {
488
- lines.push(ctx.ui.theme.fg("muted", ` ... +${state.sessions.length - 8} more tmux sessions`));
489
- }
638
+ if (drift.activeWithoutSessionIds.length > 0) {
639
+ lines.push(ctx.ui.theme.fg("warning", truncateOneLine(`drift_missing: ${drift.activeWithoutSessionIds.slice(0, 4).join(", ")}${drift.activeWithoutSessionIds.length > 4 ? " ..." : ""}`, WIDGET_ERROR_MAX)));
490
640
  }
491
- lines.push(` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`);
492
- if (state.issueError) {
493
- lines.push(ctx.ui.theme.fg("warning", `issue error: ${state.issueError}`));
641
+ if (drift.orphanSessions.length > 0) {
642
+ lines.push(ctx.ui.theme.fg("warning", truncateOneLine(`drift_orphan: ${drift.orphanSessions.slice(0, 4).join(", ")}${drift.orphanSessions.length > 4 ? " ..." : ""}`, WIDGET_ERROR_MAX)));
494
643
  }
495
- else {
496
- lines.push(ctx.ui.theme.fg("accent", `ready queue (${state.readyIssues.length})`));
497
- if (state.readyIssues.length === 0) {
498
- lines.push(ctx.ui.theme.fg("muted", " (no ready issues)"));
499
- }
500
- else {
501
- for (const issue of state.readyIssues.slice(0, 6)) {
502
- lines.push(formatIssueLine(ctx, issue, { marker: "→", tone: "accent" }));
503
- }
504
- if (state.readyIssues.length > 6) {
505
- lines.push(ctx.ui.theme.fg("muted", ` ... +${state.readyIssues.length - 6} more ready issues`));
506
- }
507
- }
508
- lines.push(ctx.ui.theme.fg("accent", `active queue (${state.activeIssues.length})`));
509
- if (state.activeIssues.length === 0) {
510
- lines.push(ctx.ui.theme.fg("muted", " (no in-progress issues)"));
644
+ lines.push(ctx.ui.theme.fg("dim", "────────────────────────────"));
645
+ lines.push(ctx.ui.theme.fg("accent", "activity"));
646
+ if (state.activityError) {
647
+ lines.push(ctx.ui.theme.fg("warning", truncateOneLine(state.activityError, WIDGET_ERROR_MAX)));
648
+ }
649
+ else if (activityLines.length === 0) {
650
+ if (state.activeIssues.length > 0) {
651
+ lines.push(ctx.ui.theme.fg("muted", "(no recent subagent updates yet)"));
511
652
  }
512
653
  else {
513
- for (const issue of state.activeIssues.slice(0, 6)) {
514
- lines.push(formatIssueLine(ctx, issue, { marker: "●", tone: "warning" }));
515
- }
516
- if (state.activeIssues.length > 6) {
517
- lines.push(ctx.ui.theme.fg("muted", ` ... +${state.activeIssues.length - 6} more active issues`));
518
- }
654
+ lines.push(ctx.ui.theme.fg("muted", "(no active workers)"));
519
655
  }
520
656
  }
521
- if (refreshStale) {
522
- lines.push(ctx.ui.theme.fg("warning", `refresh warning: last successful refresh is stale (>${staleAfterSeconds}s)`));
523
- }
524
- if (drift.activeWithoutSessionIds.length > 0) {
525
- lines.push(ctx.ui.theme.fg("warning", `drift warning: active issues without tmux sessions (${drift.activeWithoutSessionIds.length})`));
526
- lines.push(ctx.ui.theme.fg("warning", ` missing sessions for: ${drift.activeWithoutSessionIds.slice(0, 8).join(", ")}${drift.activeWithoutSessionIds.length > 8 ? " ..." : ""}`));
527
- }
528
- if (drift.orphanSessions.length > 0) {
529
- lines.push(ctx.ui.theme.fg("warning", `drift warning: tmux sessions without active issues (${drift.orphanSessions.length})`));
530
- lines.push(ctx.ui.theme.fg("warning", ` orphan sessions: ${drift.orphanSessions.slice(0, 8).join(", ")}${drift.orphanSessions.length > 8 ? " ..." : ""}`));
657
+ else {
658
+ for (const line of activityLines) {
659
+ lines.push(`${ctx.ui.theme.fg("muted", "•")} ${ctx.ui.theme.fg("text", truncateOneLine(line, WIDGET_SUMMARY_MAX))}`);
660
+ }
531
661
  }
532
662
  ctx.ui.setWidget("mu-subagents", lines, { placement: "belowEditor" });
533
663
  }
@@ -564,6 +694,8 @@ function subagentsDetails(state) {
564
694
  refresh_stale: isRefreshStale(state.lastUpdatedMs, state.staleAfterMs),
565
695
  issue_error: state.issueError,
566
696
  session_error: state.sessionError,
697
+ activity_lines: [...state.activityLines],
698
+ activity_error: state.activityError,
567
699
  last_updated_ms: state.lastUpdatedMs,
568
700
  snapshot_compact: subagentsSnapshot(state, "compact"),
569
701
  snapshot_multiline: subagentsSnapshot(state, "multiline"),
@@ -597,6 +729,12 @@ export function subagentsUiExtension(pi) {
597
729
  state.readyIssues = issues.ready;
598
730
  state.activeIssues = issues.active;
599
731
  state.issueError = issues.error;
732
+ const trackedIssueIds = (state.activeIssues.length > 0 ? state.activeIssues : state.readyIssues)
733
+ .slice(0, 8)
734
+ .map((issue) => issue.id);
735
+ const activity = await fetchRecentActivity({ issueIds: trackedIssueIds });
736
+ state.activityLines = activity.lines;
737
+ state.activityError = activity.error;
600
738
  state.lastUpdatedMs = Date.now();
601
739
  renderSubagentsUi(ctx, state);
602
740
  };
@@ -628,6 +766,16 @@ export function subagentsUiExtension(pi) {
628
766
  const notify = (ctx, message, level = "info") => {
629
767
  ctx.ui.notify(`${message}\n\n${subagentsUsageText()}`, level);
630
768
  };
769
+ const syncSubagentsMode = (ctx, action) => {
770
+ const passiveAction = action === "status" || action === "snapshot";
771
+ if (!state.enabled) {
772
+ clearHudMode("subagents");
773
+ }
774
+ else if (!passiveAction) {
775
+ setActiveHudMode("subagents");
776
+ }
777
+ syncHudModeStatus(ctx);
778
+ };
631
779
  const statusSummary = () => {
632
780
  const when = state.lastUpdatedMs == null ? "never" : new Date(state.lastUpdatedMs).toLocaleTimeString();
633
781
  const status = state.enabled ? "enabled" : "disabled";
@@ -637,6 +785,7 @@ export function subagentsUiExtension(pi) {
637
785
  const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
638
786
  const issueError = state.issueError ? `\nissue_error: ${state.issueError}` : "";
639
787
  const tmuxError = state.sessionError ? `\ntmux_error: ${state.sessionError}` : "";
788
+ const activityError = state.activityError ? `\nactivity_error: ${state.activityError}` : "";
640
789
  const driftInfo = drift.activeWithoutSessionIds.length > 0 || drift.orphanSessions.length > 0
641
790
  ? `\ndrift_active_without_session: ${drift.activeWithoutSessionIds.length}\ndrift_orphan_sessions: ${drift.orphanSessions.length}`
642
791
  : "";
@@ -644,6 +793,7 @@ export function subagentsUiExtension(pi) {
644
793
  return {
645
794
  level: state.issueError ||
646
795
  state.sessionError ||
796
+ state.activityError ||
647
797
  refreshStale ||
648
798
  drift.activeWithoutSessionIds.length > 0 ||
649
799
  drift.orphanSessions.length > 0
@@ -661,10 +811,12 @@ export function subagentsUiExtension(pi) {
661
811
  `sessions: ${state.sessions.length}`,
662
812
  `ready_issues: ${state.readyIssues.length}`,
663
813
  `active_issues: ${state.activeIssues.length}`,
814
+ `activity_updates: ${state.activityLines.length}`,
664
815
  `last refresh: ${when}`,
665
816
  ].join("\n") +
666
817
  issueError +
667
818
  tmuxError +
819
+ activityError +
668
820
  driftInfo +
669
821
  staleInfo,
670
822
  };
@@ -984,6 +1136,7 @@ export function subagentsUiExtension(pi) {
984
1136
  ensurePolling();
985
1137
  }
986
1138
  await refresh(ctx);
1139
+ syncHudModeStatus(ctx);
987
1140
  });
988
1141
  pi.on("session_switch", async (_event, ctx) => {
989
1142
  activeCtx = ctx;
@@ -991,6 +1144,7 @@ export function subagentsUiExtension(pi) {
991
1144
  ensurePolling();
992
1145
  }
993
1146
  await refresh(ctx);
1147
+ syncHudModeStatus(ctx);
994
1148
  });
995
1149
  pi.on("session_shutdown", async () => {
996
1150
  stopPolling();
@@ -1072,6 +1226,7 @@ export function subagentsUiExtension(pi) {
1072
1226
  notify(ctx, result.message, result.level);
1073
1227
  return;
1074
1228
  }
1229
+ syncSubagentsMode(ctx, params.action);
1075
1230
  ctx.ui.notify(result.message, result.level);
1076
1231
  },
1077
1232
  });
@@ -1127,6 +1282,7 @@ export function subagentsUiExtension(pi) {
1127
1282
  if (!result.ok) {
1128
1283
  return subagentsToolError(result.message, state);
1129
1284
  }
1285
+ syncSubagentsMode(ctx, params.action);
1130
1286
  return {
1131
1287
  content: [{ type: "text", text: result.message }],
1132
1288
  details: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.89",
3
+ "version": "26.2.90",
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.89",
27
+ "@femtomc/mu-core": "26.2.90",
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",