@femtomc/mu-agent 26.2.105 → 26.2.107

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.
Files changed (40) hide show
  1. package/README.md +39 -16
  2. package/assets/mu-tui-logo.png +0 -0
  3. package/dist/extensions/index.d.ts +1 -0
  4. package/dist/extensions/index.d.ts.map +1 -1
  5. package/dist/extensions/index.js +1 -0
  6. package/dist/extensions/mu-command-dispatcher.d.ts +0 -1
  7. package/dist/extensions/mu-command-dispatcher.d.ts.map +1 -1
  8. package/dist/extensions/mu-command-dispatcher.js +5 -42
  9. package/dist/extensions/mu-operator.d.ts.map +1 -1
  10. package/dist/extensions/mu-operator.js +2 -0
  11. package/dist/extensions/mu-serve.d.ts.map +1 -1
  12. package/dist/extensions/mu-serve.js +2 -0
  13. package/dist/extensions/ui.d.ts +4 -0
  14. package/dist/extensions/ui.d.ts.map +1 -0
  15. package/dist/extensions/ui.js +335 -0
  16. package/dist/operator.d.ts +169 -1
  17. package/dist/operator.d.ts.map +1 -1
  18. package/dist/operator.js +77 -7
  19. package/package.json +2 -2
  20. package/prompts/skills/automation/SKILL.md +25 -0
  21. package/prompts/skills/{crons → automation/crons}/SKILL.md +3 -2
  22. package/prompts/skills/{heartbeats → automation/heartbeats}/SKILL.md +3 -2
  23. package/prompts/skills/core/SKILL.md +28 -0
  24. package/prompts/skills/{code-mode → core/code-mode}/SKILL.md +1 -1
  25. package/prompts/skills/{mu → core/mu}/SKILL.md +38 -4
  26. package/prompts/skills/{tmux → core/tmux}/SKILL.md +1 -1
  27. package/prompts/skills/messaging/SKILL.md +27 -0
  28. package/prompts/skills/subagents/SKILL.md +21 -236
  29. package/prompts/skills/{control-flow → subagents/control-flow}/SKILL.md +5 -5
  30. package/prompts/skills/subagents/execution/SKILL.md +315 -0
  31. package/prompts/skills/{hud → subagents/hud}/SKILL.md +4 -3
  32. package/prompts/skills/subagents/model-routing/SKILL.md +363 -0
  33. package/prompts/skills/{planning → subagents/planning}/SKILL.md +13 -6
  34. package/prompts/skills/{orchestration → subagents/protocol}/SKILL.md +21 -19
  35. package/prompts/skills/writing/SKILL.md +1 -0
  36. /package/prompts/skills/{memory → core/memory}/SKILL.md +0 -0
  37. /package/prompts/skills/{setup-discord → messaging/setup-discord}/SKILL.md +0 -0
  38. /package/prompts/skills/{setup-neovim → messaging/setup-neovim}/SKILL.md +0 -0
  39. /package/prompts/skills/{setup-slack → messaging/setup-slack}/SKILL.md +0 -0
  40. /package/prompts/skills/{setup-telegram → messaging/setup-telegram}/SKILL.md +0 -0
package/README.md CHANGED
@@ -22,23 +22,29 @@ These are loaded by runtime code and are the single source of truth for default
22
22
  ## Bundled starter skills
23
23
 
24
24
  Bundled starter skills live under `packages/agent/prompts/skills/` and are bootstrapped
25
- into `~/.mu/skills/` (or `$MU_HOME/skills/`) by the CLI store-initialization path:
26
-
27
- - `mu`
28
- - `memory`
29
- - `planning`
30
- - `hud`
31
- - `orchestration`
32
- - `control-flow`
33
- - `code-mode`
34
- - `tmux`
25
+ into `~/.mu/skills/` (or `$MU_HOME/skills/`) by the CLI store-initialization path.
26
+ They are organized as category meta-skills plus subskills:
27
+
28
+ - `core`
29
+ - `mu`
30
+ - `memory`
31
+ - `tmux`
32
+ - `code-mode`
35
33
  - `subagents`
36
- - `heartbeats`
37
- - `crons`
38
- - `setup-slack`
39
- - `setup-discord`
40
- - `setup-telegram`
41
- - `setup-neovim`
34
+ - `planning`
35
+ - `protocol`
36
+ - `execution`
37
+ - `control-flow`
38
+ - `model-routing`
39
+ - `hud`
40
+ - `automation`
41
+ - `heartbeats`
42
+ - `crons`
43
+ - `messaging`
44
+ - `setup-slack`
45
+ - `setup-discord`
46
+ - `setup-telegram`
47
+ - `setup-neovim`
42
48
  - `writing`
43
49
 
44
50
  Starter skills are version-synced by CLI bootstrap. Initial bootstrap seeds missing
@@ -83,6 +89,23 @@ Default operator UI theme is `mu-gruvbox-dark`.
83
89
  - `/mu brand on|off|toggle` — enable/disable UI branding
84
90
  - `/mu hud ...` — HUD command for enabling/inspecting/clearing HUD docs; does not inject HUD metadata into branding footer
85
91
  - `/mu help` — dispatcher catalog of registered `/mu` subcommands
92
+ - `/mu ui ...` — inspect interactive `UiDoc`s (`status`/`snapshot`)
93
+
94
+ ## Programmable UI documents
95
+
96
+ Skills can publish interactive UI state via the `mu_ui` tool. Rendered `UiDoc`s survive session reconnects
97
+ (30 minute retention per session ID), respect revision/version bumps, and route action clicks/taps back to
98
+ plain command text via `metadata.command_text` (the `/answer` flow is the reference pattern).
99
+
100
+ Actions without `metadata.command_text` are treated as non-interactive and rendered as deterministic fallback rows.
101
+
102
+ Current runtime behavior is channel-specific:
103
+
104
+ - Slack renders rich blocks + interactive action buttons.
105
+ - Discord/Telegram/Neovim render text-first docs plus tokenized action callbacks.
106
+ - When interactive controls cannot be rendered, adapters append deterministic text fallback.
107
+
108
+ See the [Programmable UI substrate guide](../../docs/mu-ui.md) for the full support matrix and workflow.
86
109
 
87
110
  ## Tooling model (CLI-first)
88
111
 
Binary file
@@ -3,6 +3,7 @@ export { eventLogExtension } from "./event-log.js";
3
3
  export { hudExtension } from "./hud.js";
4
4
  export { muOperatorExtension } from "./mu-operator.js";
5
5
  export { muServeExtension } from "./mu-serve.js";
6
+ export { uiExtension } from "./ui.js";
6
7
  /**
7
8
  * Serve-mode extension — single facade that bundles all serve extensions.
8
9
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD;;GAEG;AACH,eAAO,MAAM,mBAAmB,UAA4C,CAAC;AAE7E;;GAEG;AACH,eAAO,MAAM,sBAAsB,UAA+C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAQtC;;GAEG;AACH,eAAO,MAAM,mBAAmB,UAA4C,CAAC;AAE7E;;GAEG;AACH,eAAO,MAAM,sBAAsB,UAA+C,CAAC"}
@@ -3,6 +3,7 @@ export { eventLogExtension } from "./event-log.js";
3
3
  export { hudExtension } from "./hud.js";
4
4
  export { muOperatorExtension } from "./mu-operator.js";
5
5
  export { muServeExtension } from "./mu-serve.js";
6
+ export { uiExtension } from "./ui.js";
6
7
  const RUNTIME_EXTENSION = import.meta.url.endsWith(".ts") ? "ts" : "js";
7
8
  function resolveBundledExtensionPath(moduleBasename) {
8
9
  return new URL(`./${moduleBasename}.${RUNTIME_EXTENSION}`, import.meta.url).pathname;
@@ -4,7 +4,6 @@ export type MuSubcommandRegistration = {
4
4
  subcommand: string;
5
5
  summary: string;
6
6
  usage: string;
7
- aliases?: string[];
8
7
  handler: MuSubcommandHandler;
9
8
  };
10
9
  export declare function resetMuCommandDispatcher(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"mu-command-dispatcher.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-command-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE3F,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEvG,MAAM,MAAM,wBAAwB,GAAG;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAcF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAoHD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAsDnG"}
1
+ {"version":3,"file":"mu-command-dispatcher.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-command-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE3F,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEvG,MAAM,MAAM,wBAAwB,GAAG;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAYF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA+GD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAmBnG"}
@@ -10,8 +10,7 @@ function isValidSubcommandToken(value) {
10
10
  return /^[a-z][a-z0-9_-]*$/.test(value);
11
11
  }
12
12
  function subcommandUsageSummary(entry) {
13
- const aliasSuffix = entry.aliases && entry.aliases.length > 0 ? ` (aliases: ${entry.aliases.join(", ")})` : "";
14
- return `- ${entry.usage} — ${entry.summary}${aliasSuffix}`;
13
+ return `- ${entry.usage} ${entry.summary}`;
15
14
  }
16
15
  function renderSubcommandCatalog(state) {
17
16
  if (state.entries.size === 0) {
@@ -26,11 +25,7 @@ function renderSubcommandCatalog(state) {
26
25
  return lines.join("\n");
27
26
  }
28
27
  function renderSubcommandHelp(entry) {
29
- const lines = [entry.summary, "", `Usage: ${entry.usage}`];
30
- if (entry.aliases && entry.aliases.length > 0) {
31
- lines.push(`Aliases: ${entry.aliases.map((alias) => `/mu ${alias}`).join(", ")}`);
32
- }
33
- return lines.join("\n");
28
+ return [entry.summary, "", `Usage: ${entry.usage}`].join("\n");
34
29
  }
35
30
  function parseInvocation(args) {
36
31
  const trimmed = args.trim();
@@ -48,10 +43,10 @@ function parseInvocation(args) {
48
43
  }
49
44
  function resolveEntry(state, token) {
50
45
  const normalized = normalizeSubcommand(token);
51
- if (!normalized)
46
+ if (!normalized) {
52
47
  return null;
53
- const canonical = state.aliases.get(normalized) ?? normalized;
54
- return state.entries.get(canonical) ?? null;
48
+ }
49
+ return state.entries.get(normalized) ?? null;
55
50
  }
56
51
  function ensureDispatcher(pi) {
57
52
  if (singletonState) {
@@ -59,7 +54,6 @@ function ensureDispatcher(pi) {
59
54
  }
60
55
  const state = {
61
56
  entries: new Map(),
62
- aliases: new Map(),
63
57
  };
64
58
  singletonState = state;
65
59
  pi.registerCommand("mu", {
@@ -106,40 +100,9 @@ export function registerMuSubcommand(pi, registration) {
106
100
  if (!registration.usage.startsWith("/mu ")) {
107
101
  throw new Error(`mu subcommand usage must start with '/mu ': ${registration.usage}`);
108
102
  }
109
- const normalizedAliases = (registration.aliases ?? [])
110
- .map((alias) => normalizeSubcommand(alias))
111
- .filter((alias) => alias.length > 0 && alias !== normalizedSubcommand);
112
- for (const alias of normalizedAliases) {
113
- if (!isValidSubcommandToken(alias)) {
114
- throw new Error(`Invalid mu subcommand alias: ${alias}`);
115
- }
116
- if (RESERVED_SUBCOMMANDS.has(alias)) {
117
- throw new Error(`Reserved mu subcommand alias: ${alias}`);
118
- }
119
- }
120
- const existing = state.entries.get(normalizedSubcommand);
121
- if (existing) {
122
- for (const alias of existing.normalizedAliases) {
123
- state.aliases.delete(alias);
124
- }
125
- }
126
- for (const alias of normalizedAliases) {
127
- const occupiedBy = state.aliases.get(alias);
128
- if (occupiedBy && occupiedBy !== normalizedSubcommand) {
129
- throw new Error(`mu subcommand alias '${alias}' is already registered by '${occupiedBy}'`);
130
- }
131
- if (state.entries.has(alias) && alias !== normalizedSubcommand) {
132
- throw new Error(`mu subcommand alias '${alias}' conflicts with existing subcommand`);
133
- }
134
- }
135
103
  const entry = {
136
104
  ...registration,
137
105
  normalizedSubcommand,
138
- normalizedAliases,
139
106
  };
140
107
  state.entries.set(normalizedSubcommand, entry);
141
- state.aliases.set(normalizedSubcommand, normalizedSubcommand);
142
- for (const alias of normalizedAliases) {
143
- state.aliases.set(alias, normalizedSubcommand);
144
- }
145
108
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mu-operator.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-operator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAKlE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAInD;AAED,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"mu-operator.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-operator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAMlE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAKnD;AAED,eAAe,mBAAmB,CAAC"}
@@ -7,8 +7,10 @@
7
7
  import { brandingExtension } from "./branding.js";
8
8
  import { eventLogExtension } from "./event-log.js";
9
9
  import { hudExtension } from "./hud.js";
10
+ import { uiExtension } from "./ui.js";
10
11
  export function muOperatorExtension(pi) {
11
12
  hudExtension(pi);
13
+ uiExtension(pi);
12
14
  brandingExtension(pi);
13
15
  eventLogExtension(pi);
14
16
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mu-serve.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-serve.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAKlE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,QAIhD;AAED,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"mu-serve.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-serve.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAMlE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,QAKhD;AAED,eAAe,gBAAgB,CAAC"}
@@ -7,8 +7,10 @@
7
7
  import { brandingExtension } from "./branding.js";
8
8
  import { eventLogExtension } from "./event-log.js";
9
9
  import { hudExtension } from "./hud.js";
10
+ import { uiExtension } from "./ui.js";
10
11
  export function muServeExtension(pi) {
11
12
  hudExtension(pi);
13
+ uiExtension(pi);
12
14
  brandingExtension(pi);
13
15
  eventLogExtension(pi);
14
16
  }
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export declare function uiExtension(pi: ExtensionAPI): void;
3
+ export default uiExtension;
4
+ //# sourceMappingURL=ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/extensions/ui.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AA4TpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QAiF3C;AAED,eAAe,WAAW,CAAC"}
@@ -0,0 +1,335 @@
1
+ import { normalizeUiDocs, parseUiDoc, } from "@femtomc/mu-core";
2
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
3
+ const UI_DISPLAY_DOCS_MAX = 16;
4
+ const UI_WIDGET_COMPONENTS_MAX = 6;
5
+ const UI_WIDGET_ACTIONS_MAX = 4;
6
+ const UI_SESSION_KEY_FALLBACK = "__mu_ui_active_session__";
7
+ const STATE_BY_SESSION = new Map();
8
+ const UI_STATE_TTL_MS = 30 * 60 * 1000; // keep session state for 30 minutes after last access
9
+ function createState() {
10
+ return { docsById: new Map() };
11
+ }
12
+ function pruneStaleStates(nowMs) {
13
+ for (const [key, entry] of STATE_BY_SESSION.entries()) {
14
+ if (nowMs - entry.lastAccessMs > UI_STATE_TTL_MS) {
15
+ STATE_BY_SESSION.delete(key);
16
+ }
17
+ }
18
+ }
19
+ function sessionKey(ctx) {
20
+ const manager = ctx.sessionManager;
21
+ if (!manager) {
22
+ return UI_SESSION_KEY_FALLBACK;
23
+ }
24
+ const sessionId = manager.getSessionId();
25
+ return sessionId ?? UI_SESSION_KEY_FALLBACK;
26
+ }
27
+ function ensureState(key) {
28
+ const nowMs = Date.now();
29
+ pruneStaleStates(nowMs);
30
+ const existing = STATE_BY_SESSION.get(key);
31
+ if (existing) {
32
+ existing.lastAccessMs = nowMs;
33
+ return existing.state;
34
+ }
35
+ const fresh = createState();
36
+ STATE_BY_SESSION.set(key, { state: fresh, lastAccessMs: nowMs });
37
+ return fresh;
38
+ }
39
+ function touchState(key) {
40
+ const entry = STATE_BY_SESSION.get(key);
41
+ if (entry) {
42
+ entry.lastAccessMs = Date.now();
43
+ }
44
+ }
45
+ function activeDocs(state, maxDocs = UI_DISPLAY_DOCS_MAX) {
46
+ return normalizeUiDocs([...state.docsById.values()], { maxDocs });
47
+ }
48
+ function preferredDocForState(state, candidate) {
49
+ const existing = state.docsById.get(candidate.ui_id);
50
+ if (!existing) {
51
+ return candidate;
52
+ }
53
+ const merged = normalizeUiDocs([existing, candidate], { maxDocs: 2 });
54
+ const chosen = merged.find((doc) => doc.ui_id === candidate.ui_id);
55
+ return chosen ?? candidate;
56
+ }
57
+ function short(text, max = 64) {
58
+ const normalized = text.replace(/\s+/g, " ").trim();
59
+ if (normalized.length <= max) {
60
+ return normalized;
61
+ }
62
+ if (max <= 1) {
63
+ return "…";
64
+ }
65
+ return `${normalized.slice(0, max - 1)}…`;
66
+ }
67
+ function statusSummary(docs) {
68
+ const ids = docs.map((doc) => doc.ui_id).join(", ") || "(none)";
69
+ return [`UI docs: ${docs.length}`, `ids: ${ids}`].join(" · ");
70
+ }
71
+ function snapshotText(docs, format) {
72
+ if (docs.length === 0) {
73
+ return "(no UI docs)";
74
+ }
75
+ if (format === "compact") {
76
+ return docs
77
+ .map((doc) => `${doc.ui_id}: ${short(doc.title, 32)} (${doc.revision.version})`)
78
+ .join(" | ");
79
+ }
80
+ const lines = [];
81
+ docs.slice(0, 8).forEach((doc, idx) => {
82
+ lines.push(`${idx + 1}. ${doc.title} [${doc.ui_id}]`);
83
+ if (doc.summary) {
84
+ lines.push(` summary: ${short(doc.summary, 120)}`);
85
+ }
86
+ if (doc.actions.length > 0) {
87
+ lines.push(` actions: ${doc.actions.map((action) => action.label).join(", ")}`);
88
+ }
89
+ });
90
+ if (docs.length > 8) {
91
+ lines.push(`... (+${docs.length - 8} more docs)`);
92
+ }
93
+ return lines.join("\n");
94
+ }
95
+ function parseDocInput(value) {
96
+ if (value === undefined) {
97
+ return { ok: false, error: "doc is required" };
98
+ }
99
+ const parsed = parseUiDoc(value);
100
+ if (!parsed) {
101
+ return { ok: false, error: "Invalid UiDoc." };
102
+ }
103
+ return { ok: true, doc: parsed };
104
+ }
105
+ function parseDocListInput(value) {
106
+ if (!Array.isArray(value)) {
107
+ return { ok: false, error: "docs must be an array" };
108
+ }
109
+ const docs = [];
110
+ for (let idx = 0; idx < value.length; idx += 1) {
111
+ const parsed = parseUiDoc(value[idx]);
112
+ if (!parsed) {
113
+ return { ok: false, error: `docs[${idx}]: invalid UiDoc` };
114
+ }
115
+ docs.push(parsed);
116
+ }
117
+ return { ok: true, docs: normalizeUiDocs(docs, { maxDocs: UI_DISPLAY_DOCS_MAX }) };
118
+ }
119
+ function parseSnapshotFormat(raw) {
120
+ const normalized = (raw ?? "compact").trim().toLowerCase();
121
+ return normalized === "multiline" ? "multiline" : "compact";
122
+ }
123
+ function applyUiAction(params, state) {
124
+ const docs = activeDocs(state);
125
+ switch (params.action) {
126
+ case "status":
127
+ return { ok: true, action: "status", message: statusSummary(docs) };
128
+ case "snapshot": {
129
+ const format = parseSnapshotFormat(params.snapshot_format);
130
+ return {
131
+ ok: true,
132
+ action: "snapshot",
133
+ message: snapshotText(docs, format),
134
+ extra: { snapshot_format: format },
135
+ };
136
+ }
137
+ case "set":
138
+ case "update": {
139
+ const parsed = parseDocInput(params.doc);
140
+ if (!parsed.ok) {
141
+ return { ok: false, action: params.action, message: parsed.error };
142
+ }
143
+ const preferred = preferredDocForState(state, parsed.doc);
144
+ state.docsById.set(parsed.doc.ui_id, preferred);
145
+ return {
146
+ ok: true,
147
+ action: params.action,
148
+ message: `UI doc set: ${parsed.doc.ui_id}`,
149
+ extra: { ui_id: parsed.doc.ui_id },
150
+ };
151
+ }
152
+ case "replace": {
153
+ const parsed = parseDocListInput(params.docs);
154
+ if (!parsed.ok) {
155
+ return { ok: false, action: "replace", message: parsed.error };
156
+ }
157
+ state.docsById.clear();
158
+ for (const doc of parsed.docs) {
159
+ state.docsById.set(doc.ui_id, doc);
160
+ }
161
+ return {
162
+ ok: true,
163
+ action: "replace",
164
+ message: `UI docs replaced (${parsed.docs.length}).`,
165
+ extra: { doc_count: parsed.docs.length },
166
+ };
167
+ }
168
+ case "remove": {
169
+ const uiId = (params.ui_id ?? "").trim();
170
+ if (!uiId) {
171
+ return { ok: false, action: "remove", message: "Missing ui_id." };
172
+ }
173
+ if (!state.docsById.delete(uiId)) {
174
+ return { ok: false, action: "remove", message: `UI doc not found: ${uiId}` };
175
+ }
176
+ return { ok: true, action: "remove", message: `UI doc removed: ${uiId}` };
177
+ }
178
+ case "clear":
179
+ state.docsById.clear();
180
+ return { ok: true, action: "clear", message: "UI docs cleared." };
181
+ }
182
+ }
183
+ function buildToolResult(opts) {
184
+ const docs = activeDocs(opts.state);
185
+ const result = {
186
+ content: [{ type: "text", text: opts.message }],
187
+ ui_docs: docs,
188
+ details: {
189
+ ok: opts.ok,
190
+ action: opts.action,
191
+ doc_count: docs.length,
192
+ ui_ids: docs.map((doc) => doc.ui_id),
193
+ ...(opts.extra ?? {}),
194
+ },
195
+ };
196
+ return result;
197
+ }
198
+ function renderDocPreview(theme, doc) {
199
+ const lines = [];
200
+ lines.push(`${theme.fg("accent", doc.title)} ${theme.fg("muted", `[${doc.ui_id}]`)}`);
201
+ if (doc.summary) {
202
+ lines.push(theme.fg("muted", short(doc.summary, 80)));
203
+ }
204
+ const components = doc.components.slice(0, UI_WIDGET_COMPONENTS_MAX);
205
+ if (components.length > 0) {
206
+ lines.push(theme.fg("dim", "Components:"));
207
+ for (const component of components) {
208
+ lines.push(` ${componentPreview(component)}`);
209
+ }
210
+ }
211
+ if (doc.actions.length > 0) {
212
+ lines.push(theme.fg("muted", "Actions:"));
213
+ const visibleActions = doc.actions.slice(0, UI_WIDGET_ACTIONS_MAX);
214
+ for (let idx = 0; idx < visibleActions.length; idx += 1) {
215
+ const action = visibleActions[idx];
216
+ lines.push(` ${idx + 1}. ${action.label}`);
217
+ }
218
+ if (doc.actions.length > visibleActions.length) {
219
+ lines.push(` ... (+${doc.actions.length - visibleActions.length} more actions)`);
220
+ }
221
+ lines.push(theme.fg("dim", "Actions are handled through channel-native callbacks."));
222
+ }
223
+ else {
224
+ lines.push(theme.fg("dim", "No interactive actions."));
225
+ }
226
+ return lines;
227
+ }
228
+ function componentPreview(component) {
229
+ const { kind } = component;
230
+ switch (kind) {
231
+ case "text":
232
+ return `text · ${short(component.text, 80)}`;
233
+ case "list":
234
+ return `list · ${component.title ?? kind} · ${component.items.length} item(s)`;
235
+ case "key_value":
236
+ return `key_value · ${component.title ?? kind} · ${component.rows.length} row(s)`;
237
+ case "divider":
238
+ return "divider";
239
+ }
240
+ return kind;
241
+ }
242
+ function refreshUi(ctx) {
243
+ const key = sessionKey(ctx);
244
+ const state = ensureState(key);
245
+ if (!ctx.hasUI) {
246
+ return;
247
+ }
248
+ const docs = activeDocs(state);
249
+ if (docs.length === 0) {
250
+ ctx.ui.setStatus("mu-ui", undefined);
251
+ ctx.ui.setWidget("mu-ui", undefined);
252
+ return;
253
+ }
254
+ const labels = docs.map((doc) => doc.ui_id).join(", ");
255
+ ctx.ui.setStatus("mu-ui", [
256
+ ctx.ui.theme.fg("dim", "ui"),
257
+ ctx.ui.theme.fg("muted", "·"),
258
+ ctx.ui.theme.fg("accent", `${docs.length}`),
259
+ ctx.ui.theme.fg("muted", "·"),
260
+ ctx.ui.theme.fg("text", labels),
261
+ ].join(" "));
262
+ ctx.ui.setWidget("mu-ui", renderDocPreview(ctx.ui.theme, docs[0]), { placement: "belowEditor" });
263
+ }
264
+ export function uiExtension(pi) {
265
+ registerMuSubcommand(pi, {
266
+ subcommand: "ui",
267
+ summary: "Inspect interactive UI docs",
268
+ usage: "/mu ui status|snapshot [compact|multiline]",
269
+ handler: async (args, ctx) => {
270
+ const tokens = args
271
+ .trim()
272
+ .split(/\s+/)
273
+ .filter((token) => token.length > 0);
274
+ const subcommand = tokens[0] ?? "status";
275
+ const key = sessionKey(ctx);
276
+ const state = ensureState(key);
277
+ if (subcommand === "status" || subcommand === "snapshot") {
278
+ const snapshotFormat = subcommand === "snapshot" ? tokens[1] : undefined;
279
+ const actionParams = {
280
+ action: subcommand,
281
+ snapshot_format: snapshotFormat,
282
+ };
283
+ const result = applyUiAction(actionParams, state);
284
+ refreshUi(ctx);
285
+ ctx.ui.notify(result.message, result.ok ? "info" : "error");
286
+ return;
287
+ }
288
+ ctx.ui.notify("Usage: /mu ui status|snapshot [compact|multiline]", "info");
289
+ },
290
+ });
291
+ pi.registerTool({
292
+ name: "mu_ui",
293
+ label: "mu UI",
294
+ description: "Publish, inspect, and manage interactive UI documents.",
295
+ parameters: {
296
+ type: "object",
297
+ additionalProperties: false,
298
+ properties: {
299
+ action: {
300
+ type: "string",
301
+ enum: ["status", "snapshot", "set", "update", "replace", "remove", "clear"],
302
+ },
303
+ doc: { type: "object", additionalProperties: true },
304
+ docs: { type: "array", items: { type: "object", additionalProperties: true } },
305
+ ui_id: { type: "string" },
306
+ snapshot_format: { type: "string", enum: ["compact", "multiline"] },
307
+ },
308
+ required: ["action"],
309
+ },
310
+ execute: async (_toolCallId, paramsRaw, _signal, _onUpdate, ctx) => {
311
+ const key = sessionKey(ctx);
312
+ const state = ensureState(key);
313
+ const params = paramsRaw;
314
+ const result = applyUiAction(params, state);
315
+ refreshUi(ctx);
316
+ return buildToolResult({ state, ...result });
317
+ },
318
+ });
319
+ pi.on("session_start", (_event, ctx) => {
320
+ refreshUi(ctx);
321
+ });
322
+ pi.on("session_switch", (_event, ctx) => {
323
+ refreshUi(ctx);
324
+ });
325
+ pi.on("session_shutdown", (_event, ctx) => {
326
+ const key = sessionKey(ctx);
327
+ touchState(key);
328
+ if (!ctx.hasUI) {
329
+ return;
330
+ }
331
+ ctx.ui.setStatus("mu-ui", undefined);
332
+ ctx.ui.setWidget("mu-ui", undefined);
333
+ });
334
+ }
335
+ export default uiExtension;