@femtomc/mu-agent 26.2.94 → 26.2.96

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
@@ -46,7 +46,7 @@ Current stack:
46
46
  - `brandingExtension` — mu compact header/footer branding + default theme
47
47
  - `eventLogExtension` — event tail + watch widget
48
48
  - `planningUiExtension` — planning mode: compact HUD for next-step/approval flow in widget/status surfaces while keeping the branding footer unchanged (`/mu plan ...`)
49
- - `subagentsUiExtension` — subagents mode: compact HUD with activity sentences from issue/forum events plus footer-ready queue/health metadata (`/mu subagents ...`)
49
+ - `subagentsUiExtension` — subagents mode: compact HUD with activity sentences from issue/forum events, with state kept in widget/status surfaces while branding footer stays unchanged (`/mu subagents ...`)
50
50
 
51
51
  Default operator UI theme is `mu-gruvbox-dark`.
52
52
 
@@ -56,7 +56,7 @@ Default operator UI theme is `mu-gruvbox-dark`.
56
56
  - `/mu events watch on|off` — toggle event watch widget
57
57
  - `/mu brand on|off|toggle` — enable/disable UI branding
58
58
  - `/mu plan ...` — planning HUD (phases, checklist editing, communication state, snapshots); does not inject planning metadata into branding footer
59
- - `/mu subagents ...` — tmux + issue queue monitor/spawner (profiles, spawn pause, stale/refresh controls, snapshots)
59
+ - `/mu subagents ...` — tmux + issue queue monitor/spawner (profiles, spawn pause, stale/refresh controls, snapshots); does not inject subagents metadata into branding footer
60
60
  - `/mu help` — dispatcher catalog of registered `/mu` subcommands
61
61
 
62
62
  ## Tooling model (CLI-first)
@@ -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;AAyEpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QA6OjD;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;AAwEpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QAoOjD;AAED,eAAe,iBAAiB,CAAC"}
@@ -8,7 +8,6 @@
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";
12
11
  import { registerMuSubcommand } from "./mu-command-dispatcher.js";
13
12
  import { fetchMuStatus, muServerUrl } from "./shared.js";
14
13
  const EMPTY_SNAPSHOT = {
@@ -128,15 +127,6 @@ export function brandingExtension(pi) {
128
127
  const barColor = pct >= 80 ? "warning" : pct >= 60 ? "muted" : "dim";
129
128
  parts.push(theme.fg("muted", "·"), theme.fg(barColor, `ctx ${pct}%`), theme.fg(barColor, contextBar(pct, 10)));
130
129
  }
131
- const activeHudMode = getActiveHudMode();
132
- if (activeHudMode === "subagents") {
133
- const extensionStatuses = footerData.getExtensionStatuses();
134
- const modeMeta = extensionStatuses.get("mu-subagents-meta") ?? "";
135
- parts.push(theme.fg("muted", "·"), theme.fg("accent", "hud:subagents"));
136
- if (modeMeta.length > 0) {
137
- parts.push(theme.fg("muted", "·"), theme.fg("dim", truncateToWidth(modeMeta, 42)));
138
- }
139
- }
140
130
  if (snapshot.openCount > 0 || snapshot.readyCount > 0) {
141
131
  parts.push(theme.fg("muted", "·"), theme.fg("dim", `open ${snapshot.openCount} ready ${snapshot.readyCount}`));
142
132
  }
@@ -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;AAi6BpF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,QAkoBpD;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;AA2gCpF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,QAkoBpD;AAED,eAAe,oBAAoB,CAAC"}
@@ -18,6 +18,7 @@ const WIDGET_SUMMARY_MAX = 76;
18
18
  const WIDGET_ERROR_MAX = 72;
19
19
  const ACTIVITY_EVENT_LIMIT = 180;
20
20
  const ACTIVITY_LINE_LIMIT = 4;
21
+ const FORUM_ACTIVITY_ISSUE_LIMIT = 8;
21
22
  function shellQuote(value) {
22
23
  return `'${value.replaceAll("'", "'\"'\"'")}'`;
23
24
  }
@@ -177,6 +178,46 @@ function parseIssueArray(label, jsonText) {
177
178
  });
178
179
  return { issues, error: null };
179
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
+ }
180
221
  async function runMuCli(args) {
181
222
  let proc = null;
182
223
  try {
@@ -408,52 +449,85 @@ function isActivityEndpointUnavailable(errorMessage) {
408
449
  const normalized = errorMessage.toLowerCase();
409
450
  return normalized.includes("mu server 404") && normalized.includes("not found");
410
451
  }
411
- async function fetchRecentActivity(opts) {
412
- if (!muServerUrl()) {
413
- return { lines: [], error: null };
414
- }
415
- let events;
416
- try {
417
- events = await fetchMuJson(`/api/control-plane/events?limit=${ACTIVITY_EVENT_LIMIT}`, {
418
- timeoutMs: 4_000,
419
- });
420
- }
421
- catch (err) {
422
- const message = err instanceof Error ? err.message : String(err);
423
- if (isActivityEndpointUnavailable(message)) {
424
- return { lines: [], error: null };
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;
425
461
  }
426
- return { lines: [], error: `activity refresh failed: ${truncateOneLine(message, 60)}` };
427
- }
428
- if (!Array.isArray(events) || events.length === 0) {
429
- return { lines: [], error: null };
430
- }
431
- const tracked = new Set(opts.issueIds.map((issueId) => issueId.trim()).filter((issueId) => issueId.length > 0));
432
- const seenIssueIds = new Set();
433
- const lines = [];
434
- const sorted = [...events].sort((left, right) => {
435
- const leftTs = typeof left.ts_ms === "number" ? left.ts_ms : 0;
436
- const rightTs = typeof right.ts_ms === "number" ? right.ts_ms : 0;
437
- return rightTs - leftTs;
438
- });
439
- for (const event of sorted) {
440
- const rendered = renderActivitySentence(event);
441
- if (!rendered) {
442
- continue;
462
+ const parsed = parseForumReadLatest(`forum:${issueId}`, outcome.stdout);
463
+ if (parsed.error || !parsed.message) {
464
+ return null;
443
465
  }
444
- if (tracked.size > 0 && !tracked.has(rendered.issueId)) {
445
- continue;
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
+ });
446
487
  }
447
- if (seenIssueIds.has(rendered.issueId)) {
448
- continue;
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 = [];
449
494
  }
450
- seenIssueIds.add(rendered.issueId);
451
- lines.push(`${eventAgeLabel(event.ts_ms)} ${rendered.sentence}`);
452
- if (lines.length >= ACTIVITY_LINE_LIMIT) {
453
- break;
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
+ }
454
524
  }
455
525
  }
456
- return { lines, error: null };
526
+ const forumLines = await fetchRecentForumActivity(opts.issueIds);
527
+ if (forumLines.length > 0) {
528
+ return { lines: forumLines, error: null };
529
+ }
530
+ return { lines: [], error: endpointError };
457
531
  }
458
532
  function computeQueueDrift(sessions, activeIssues) {
459
533
  const activeWithoutSessionIds = activeIssues
@@ -465,6 +539,30 @@ function computeQueueDrift(sessions, activeIssues) {
465
539
  orphanSessions,
466
540
  };
467
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
+ }
468
566
  function normalizeIssueTag(raw) {
469
567
  const trimmed = raw.trim();
470
568
  if (!trimmed || trimmed.toLowerCase() === "clear") {
@@ -597,17 +695,7 @@ function renderSubagentsUi(ctx, state) {
597
695
  statusParts.push(ctx.ui.theme.fg("muted", truncateOneLine(issueScope, 18)));
598
696
  }
599
697
  ctx.ui.setStatus("mu-subagents", statusParts.join(` ${ctx.ui.theme.fg("muted", "·")} `));
600
- const footerMetaParts = [`q:${state.readyIssues.length}/${state.activeIssues.length}`, `tmux:${state.sessions.length}`];
601
- if (staleCount > 0) {
602
- footerMetaParts.push(`drift:${staleCount}`);
603
- }
604
- if (refreshStale) {
605
- footerMetaParts.push("refresh:stale");
606
- }
607
- if (state.issueError || state.sessionError || state.activityError) {
608
- footerMetaParts.push("err");
609
- }
610
- ctx.ui.setStatus("mu-subagents-meta", footerMetaParts.join(" "));
698
+ ctx.ui.setStatus("mu-subagents-meta", undefined);
611
699
  const titleParts = [
612
700
  ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Subagents")),
613
701
  ctx.ui.theme.fg("muted", "·"),
@@ -668,8 +756,11 @@ function renderSubagentsUi(ctx, state) {
668
756
  lines.push(ctx.ui.theme.fg("warning", truncateOneLine(state.activityError, WIDGET_ERROR_MAX)));
669
757
  }
670
758
  else if (activityLines.length === 0) {
671
- if (state.activeIssues.length > 0) {
672
- lines.push(ctx.ui.theme.fg("muted", "(no recent subagent updates yet)"));
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
+ }
673
764
  }
674
765
  else {
675
766
  lines.push(ctx.ui.theme.fg("muted", "(no active operators)"));
@@ -750,7 +841,7 @@ export function subagentsUiExtension(pi) {
750
841
  state.readyIssues = issues.ready;
751
842
  state.activeIssues = issues.active;
752
843
  state.issueError = issues.error;
753
- const trackedIssueIds = (state.activeIssues.length > 0 ? state.activeIssues : state.readyIssues)
844
+ const trackedIssueIds = [...state.activeIssues, ...state.readyIssues]
754
845
  .slice(0, 8)
755
846
  .map((issue) => issue.id);
756
847
  const activity = await fetchRecentActivity({ issueIds: trackedIssueIds });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.94",
3
+ "version": "26.2.96",
4
4
  "description": "Shared operator runtime for mu assistant sessions 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.94",
27
+ "@femtomc/mu-core": "26.2.96",
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",