@femtomc/mu-agent 26.2.108 → 26.2.109

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
@@ -27,6 +27,7 @@ They are organized as category meta-skills plus subskills:
27
27
 
28
28
  - `core`
29
29
  - `mu`
30
+ - `programmable-ui`
30
31
  - `memory`
31
32
  - `tmux`
32
33
  - `code-mode`
@@ -48,6 +49,7 @@ They are organized as category meta-skills plus subskills:
48
49
 
49
50
  Starter skills are version-synced by CLI bootstrap. Initial bootstrap seeds missing
50
51
  skills; bundled-version changes refresh installed starter skill files.
52
+ Operator-facing workflow detail lives in the skill docs (for example, `core/programmable-ui`).
51
53
 
52
54
  ## Install
53
55
 
@@ -75,7 +77,7 @@ Current stack:
75
77
 
76
78
  - `brandingExtension` — mu compact header/footer branding + default theme
77
79
  - `eventLogExtension` — event tail + watch widget
78
- - `uiExtension` — programmable `UiDoc` surface (`/mu ui ...`, `mu_ui`) with terminal auto-prompt/awaiting behavior and deterministic action fallbacks
80
+ - `uiExtension` — programmable `UiDoc` surface (`/mu ui ...`, `mu_ui`)
79
81
 
80
82
  Default operator UI theme is `mu-gruvbox-dark`.
81
83
 
@@ -86,24 +88,7 @@ Default operator UI theme is `mu-gruvbox-dark`.
86
88
  - `/mu brand on|off|toggle` — enable/disable UI branding
87
89
  - `/mu ui ...` — inspect interactive `UiDoc`s (`status`/`snapshot`)
88
90
  - `/mu help` — dispatcher catalog of registered `/mu` subcommands
89
- - `ctrl+shift+u` — reopen local programmable-UI interaction flow (in-TUI doc/action picker, auto-fill payload-backed template values, prompt unresolved values, submit composed prompt)
90
-
91
- ## Programmable UI documents
92
-
93
- Skills can publish interactive UI state via the `mu_ui` tool. Rendered `UiDoc`s survive session reconnects
94
- (30 minute retention per session ID), respect revision/version bumps, and route action clicks/taps back to
95
- plain command text via `metadata.command_text` (the `/answer` flow is the reference pattern).
96
-
97
- Actions without `metadata.command_text` are treated as non-interactive and rendered as deterministic fallback rows.
98
-
99
- Current runtime behavior is channel-specific:
100
-
101
- - Slack renders rich blocks + interactive action buttons.
102
- - Discord/Telegram/Neovim render text-first docs; interactive actions are tokenized, while status-profile actions deterministically degrade to command-text fallback.
103
- - Terminal operator UI (`mu serve`) renders docs in-widget, auto-prompts when agent publishes new runnable actions, shows `awaiting` UI status/widget state until resolved, and supports manual reopen via `ctrl+shift+u` (in-TUI picker overlay + prompt composition).
104
- - When interactive controls cannot be rendered, adapters append deterministic text fallback.
105
-
106
- See the [Programmable UI substrate guide](../../docs/mu-ui.md) for the full support matrix and workflow.
91
+ - `ctrl+shift+u` — reopen local programmable-UI interaction flow
107
92
 
108
93
  ## Tooling model (CLI-first)
109
94
 
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/extensions/ui.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AA8+BpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA2N3C;AAED,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/extensions/ui.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAsiCpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA4N3C;AAED,eAAe,WAAW,CAAC"}
@@ -4,12 +4,21 @@ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
4
4
  const UI_DISPLAY_DOCS_MAX = 16;
5
5
  const UI_WIDGET_COMPONENTS_MAX = 6;
6
6
  const UI_WIDGET_ACTIONS_MAX = 4;
7
+ const UI_WIDGET_COMPONENT_DETAILS_MAX = 3;
7
8
  const UI_PICKER_COMPONENTS_MAX = 8;
8
9
  const UI_PICKER_LIST_ITEMS_MAX = 4;
9
10
  const UI_PICKER_KEYVALUE_ROWS_MAX = 4;
10
11
  const UI_SESSION_KEY_FALLBACK = "__mu_ui_active_session__";
11
12
  const UI_PROMPT_PREVIEW_MAX = 160;
12
13
  const UI_INTERACT_SHORTCUT = "ctrl+shift+u";
14
+ const UI_INTERACT_OVERLAY_OPTIONS = {
15
+ anchor: "top-left",
16
+ row: 0,
17
+ col: 0,
18
+ width: "100%",
19
+ maxHeight: "100%",
20
+ margin: 0,
21
+ };
13
22
  const STATE_BY_SESSION = new Map();
14
23
  const UI_STATE_TTL_MS = 30 * 60 * 1000; // keep session state for 30 minutes after last access
15
24
  function createState() {
@@ -18,6 +27,7 @@ function createState() {
18
27
  pendingPrompt: null,
19
28
  promptedRevisionKeys: new Set(),
20
29
  awaitingUiIds: new Set(),
30
+ interactionDepth: 0,
21
31
  };
22
32
  }
23
33
  function pruneStaleStates(nowMs) {
@@ -122,6 +132,23 @@ function preferredDocForState(state, candidate) {
122
132
  function awaitingDocs(state, docs) {
123
133
  return docs.filter((doc) => state.awaitingUiIds.has(doc.ui_id) && runnableActions(doc).length > 0);
124
134
  }
135
+ function beginUiInteraction(ctx, state) {
136
+ state.interactionDepth += 1;
137
+ refreshUi(ctx);
138
+ }
139
+ function endUiInteraction(ctx, state) {
140
+ state.interactionDepth = Math.max(0, state.interactionDepth - 1);
141
+ refreshUi(ctx);
142
+ }
143
+ async function withUiInteraction(ctx, state, run) {
144
+ beginUiInteraction(ctx, state);
145
+ try {
146
+ return await run();
147
+ }
148
+ finally {
149
+ endUiInteraction(ctx, state);
150
+ }
151
+ }
125
152
  function short(text, max = 64) {
126
153
  const normalized = text.replace(/\s+/g, " ").trim();
127
154
  if (normalized.length <= max) {
@@ -636,12 +663,7 @@ async function pickUiActionInteractively(opts) {
636
663
  initialActionId: opts.actionId,
637
664
  }), {
638
665
  overlay: true,
639
- overlayOptions: {
640
- anchor: "center",
641
- width: "78%",
642
- maxHeight: "70%",
643
- margin: 1,
644
- },
666
+ overlayOptions: UI_INTERACT_OVERLAY_OPTIONS,
645
667
  });
646
668
  return selected ?? null;
647
669
  }
@@ -780,7 +802,11 @@ function renderDocPreview(theme, doc, opts) {
780
802
  if (components.length > 0) {
781
803
  lines.push(theme.fg("dim", "Components:"));
782
804
  for (const component of components) {
783
- lines.push(` ${componentPreview(component)}`);
805
+ const previewLines = componentPreviewLines(component);
806
+ for (let idx = 0; idx < previewLines.length; idx += 1) {
807
+ const line = previewLines[idx];
808
+ lines.push(`${idx === 0 ? " " : " "}${line}`);
809
+ }
784
810
  }
785
811
  }
786
812
  const interactiveActions = runnableActions(doc);
@@ -804,19 +830,38 @@ function renderDocPreview(theme, doc, opts) {
804
830
  }
805
831
  return lines;
806
832
  }
807
- function componentPreview(component) {
833
+ function componentPreviewLines(component) {
808
834
  const { kind } = component;
809
835
  switch (kind) {
810
836
  case "text":
811
- return `text · ${short(component.text, 80)}`;
812
- case "list":
813
- return `list · ${component.title ?? kind} · ${component.items.length} item(s)`;
814
- case "key_value":
815
- return `key_value · ${component.title ?? kind} · ${component.rows.length} row(s)`;
837
+ return [`text · ${short(component.text, 80)}`];
838
+ case "list": {
839
+ const lines = [`list · ${component.title ?? kind} · ${component.items.length} item(s)`];
840
+ const visible = component.items.slice(0, UI_WIDGET_COMPONENT_DETAILS_MAX);
841
+ for (const item of visible) {
842
+ const detail = item.detail ? ` — ${short(item.detail, 28)}` : "";
843
+ lines.push(`• ${short(item.label, 72)}${detail}`);
844
+ }
845
+ if (component.items.length > visible.length) {
846
+ lines.push(`... (+${component.items.length - visible.length} more items)`);
847
+ }
848
+ return lines;
849
+ }
850
+ case "key_value": {
851
+ const lines = [`key_value · ${component.title ?? kind} · ${component.rows.length} row(s)`];
852
+ const visible = component.rows.slice(0, UI_WIDGET_COMPONENT_DETAILS_MAX);
853
+ for (const row of visible) {
854
+ lines.push(`${short(row.key, 20)}: ${short(row.value, 52)}`);
855
+ }
856
+ if (component.rows.length > visible.length) {
857
+ lines.push(`... (+${component.rows.length - visible.length} more rows)`);
858
+ }
859
+ return lines;
860
+ }
816
861
  case "divider":
817
- return "divider";
862
+ return ["divider"];
818
863
  }
819
- return kind;
864
+ return [kind];
820
865
  }
821
866
  function refreshUi(ctx) {
822
867
  const key = sessionKey(ctx);
@@ -833,17 +878,24 @@ function refreshUi(ctx) {
833
878
  }
834
879
  const awaiting = awaitingDocs(state, docs);
835
880
  const labels = docs.map((doc) => doc.ui_id).join(", ");
881
+ const readiness = state.interactionDepth > 0
882
+ ? ctx.ui.theme.fg("accent", "prompting")
883
+ : awaiting.length > 0
884
+ ? ctx.ui.theme.fg("accent", `awaiting ${awaiting.length}`)
885
+ : ctx.ui.theme.fg("dim", "ready");
836
886
  ctx.ui.setStatus("mu-ui", [
837
887
  ctx.ui.theme.fg("dim", "ui"),
838
888
  ctx.ui.theme.fg("muted", "·"),
839
889
  ctx.ui.theme.fg("accent", `${docs.length}`),
840
890
  ctx.ui.theme.fg("muted", "·"),
841
- awaiting.length > 0
842
- ? ctx.ui.theme.fg("accent", `awaiting ${awaiting.length}`)
843
- : ctx.ui.theme.fg("dim", "ready"),
891
+ readiness,
844
892
  ctx.ui.theme.fg("muted", "·"),
845
893
  ctx.ui.theme.fg("text", labels),
846
894
  ].join(" "));
895
+ if (state.interactionDepth > 0) {
896
+ ctx.ui.setWidget("mu-ui", undefined);
897
+ return;
898
+ }
847
899
  const primaryDoc = awaiting[0] ?? docs[0];
848
900
  ctx.ui.setWidget("mu-ui", renderDocPreview(ctx.ui.theme, primaryDoc, {
849
901
  awaitingResponse: state.awaitingUiIds.has(primaryDoc.ui_id),
@@ -853,7 +905,7 @@ function refreshUi(ctx) {
853
905
  export function uiExtension(pi) {
854
906
  const commandUsage = "/mu ui status|snapshot [compact|multiline]|interact [ui_id [action_id]]";
855
907
  const usage = `Usage: ${commandUsage}`;
856
- const runUiActionFromDoc = async (ctx, state, uiId, actionId) => {
908
+ const runUiActionFromDoc = async (ctx, state, uiId, actionId) => withUiInteraction(ctx, state, async () => {
857
909
  const docs = activeDocs(state, UI_DISPLAY_DOCS_MAX);
858
910
  if (docs.length === 0) {
859
911
  ctx.ui.notify("No UI docs are currently available.", "info");
@@ -943,7 +995,7 @@ export function uiExtension(pi) {
943
995
  }
944
996
  retainAwaitingUiIdsForActiveDocs(state);
945
997
  ctx.ui.notify(`Submitted prompt from ${selectedDoc.ui_id}/${selectedAction.id}.`, "info");
946
- };
998
+ });
947
999
  registerMuSubcommand(pi, {
948
1000
  subcommand: "ui",
949
1001
  summary: "Inspect and manage interactive UI docs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.108",
3
+ "version": "26.2.109",
4
4
  "description": "Shared operator runtime for mu assistant sessions and serve extensions.",
5
5
  "keywords": [
6
6
  "mu",
@@ -25,7 +25,7 @@
25
25
  "themes/**"
26
26
  ],
27
27
  "dependencies": {
28
- "@femtomc/mu-core": "26.2.108",
28
+ "@femtomc/mu-core": "26.2.109",
29
29
  "@mariozechner/pi-agent-core": "^0.54.2",
30
30
  "@mariozechner/pi-ai": "^0.54.2",
31
31
  "@mariozechner/pi-coding-agent": "^0.54.2",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: core
3
- description: "Meta-skill for core mu operating primitives. Routes to mu, memory, tmux, and code-mode based on task shape."
3
+ description: "Meta-skill for core mu operating primitives. Routes to mu, programmable-ui, memory, tmux, and code-mode based on task shape."
4
4
  ---
5
5
 
6
6
  # core
@@ -10,6 +10,7 @@ Use this meta-skill when the user asks for general `mu` operation guidance and y
10
10
  ## Subskills
11
11
 
12
12
  - `mu` — default CLI-first operating workflow (inspect, mutate, verify, handoff).
13
+ - `programmable-ui` — canonical `mu_ui`/`UiDoc` workflow for publishing, inspecting, and debugging interactive UI docs.
13
14
  - `memory` — prior-context retrieval, timeline reconstruction, and memory-index maintenance.
14
15
  - `tmux` — persistent terminal/session substrate for bounded command execution and fan-out.
15
16
  - `code-mode` — tmux-backed REPL loops for iterative execution and context compression.
@@ -17,12 +18,14 @@ Use this meta-skill when the user asks for general `mu` operation guidance and y
17
18
  ## Selection guide
18
19
 
19
20
  1. Start with `mu` for most day-to-day operator work.
20
- 2. Add `memory` when prior context or timeline anchors are required.
21
- 3. Add `tmux` when durable shell state or parallel worker shells are needed.
22
- 4. Add `code-mode` when solving by live execution is cheaper than chat-only reasoning.
21
+ 2. Route to `programmable-ui` when the user asks about `mu_ui`, `/mu ui ...`, `UiDoc` payloads, action wiring, or interactive prompt behavior.
22
+ 3. Add `memory` when prior context or timeline anchors are required.
23
+ 4. Add `tmux` when durable shell state or parallel worker shells are needed.
24
+ 5. Add `code-mode` when solving by live execution is cheaper than chat-only reasoning.
23
25
 
24
26
  ## Common patterns
25
27
 
26
28
  - **Bounded investigation**: Use `mu` commands (`get`, `read`, `health`) to inspect current state, then use `memory` to find "when did this last work?" before attempting a fix.
29
+ - **Programmable UI scaffolding**: Route to `programmable-ui` to emit schema-valid `UiDoc` templates quickly, verify state with `/mu ui status|snapshot`, and close docs with `mu_ui remove|clear`.
27
30
  - **Context compression**: If a user asks for complex debugging that involves running code and printing huge errors, route to `code-mode`. The agent can spin up a REPL, iterate on a fix offline, and return only the root cause to the chat.
28
31
  - **Parallel fan-out**: If a command takes a long time, or needs to run across multiple directories, route to `tmux` to spawn parallel worker shells, keep them running in the background, and periodically read their output.
@@ -224,6 +224,7 @@ Keep operator↔human communication mu_ui-first across these skills:
224
224
  - one non-interactive status doc per active profile (`metadata.profile.variant: "status"`)
225
225
  - separate interactive prompt docs for decisions (`metadata.command_text` actions)
226
226
  - explicit `mu_ui remove` teardown for resolved prompts and completed passes
227
+ For focused `UiDoc` schema templates/action wiring/status diagnostics, use `programmable-ui`.
227
228
  For REPL-driven exploration and context compression, use `code-mode`.
228
229
  For persistent terminal sessions and worker fan-out mechanics, use `tmux`.
229
230
  For recurring bounded automation loops, use `heartbeats`.
@@ -247,6 +248,7 @@ For wall-clock schedules (one-shot, interval, cron-expression), use `crons`.
247
248
 
248
249
  - Prior-context retrieval and index maintenance: **`memory`**
249
250
  - Planning/decomposition and DAG review: **`planning`**
251
+ - Programmable UI schema/templates/action-routing diagnostics: **`programmable-ui`**
250
252
  - mu_ui-first status/prompt communication patterns for DAG work: **`planning`**, **`execution`**, **`control-flow`**, **`model-routing`**
251
253
  - Shared DAG semantics for planning + execution: **`protocol`**
252
254
  - Loop/termination policy overlays (review gates, retries, escalation): **`control-flow`**
@@ -0,0 +1,299 @@
1
+ ---
2
+ name: programmable-ui
3
+ description: "Builds and debugs mu_ui UiDocs with schema-valid payloads, interaction wiring, and status/snapshot verification."
4
+ ---
5
+
6
+ # programmable-ui
7
+
8
+ Use this skill when the task involves `mu_ui`, `UiDoc` payloads, interactive actions, or `/mu ui ...` inspection commands.
9
+
10
+ ## Contents
11
+
12
+ - [Core contract](#core-contract)
13
+ - [60-second quickstart](#60-second-quickstart)
14
+ - [UiDoc schema cheat sheet](#uidoc-schema-cheat-sheet)
15
+ - [mu_ui action semantics](#mu_ui-action-semantics)
16
+ - [Canonical templates](#canonical-templates)
17
+ - [Status-profile rules](#status-profile-rules)
18
+ - [Debugging playbook](#debugging-playbook)
19
+ - [Verify and teardown checklist](#verify-and-teardown-checklist)
20
+ - [Evaluation scenarios](#evaluation-scenarios)
21
+
22
+ ## Core contract
23
+
24
+ 1. **Publish schema-valid docs only**
25
+ - `mu_ui` accepts `doc: object`, but runtime validation is strict (`UiDoc` schema).
26
+ - Invalid payloads fail with `Invalid UiDoc.`.
27
+
28
+ 2. **Keep interaction command-driven**
29
+ - Interactive actions must set `action.metadata.command_text` (for example `/answer yes`).
30
+ - User clicks/taps are translated back into normal command turns.
31
+
32
+ 3. **Separate status and decisions**
33
+ - Keep one non-interactive status doc per active profile (`metadata.profile.variant: "status"`).
34
+ - Use separate interactive docs for user decisions.
35
+
36
+ 4. **Use monotonic revisions**
37
+ - Increment `revision.version` on each update for the same `ui_id`.
38
+ - Replays/reconnects keep the highest revision deterministically.
39
+
40
+ 5. **Read -> act -> verify**
41
+ - After each `set|update|replace|remove|clear`, check `/mu ui status` and `/mu ui snapshot`.
42
+
43
+ 6. **Close docs explicitly**
44
+ - Resolve prompts with `mu_ui remove` (preferred) or `mu_ui clear`.
45
+
46
+ ## 60-second quickstart
47
+
48
+ 1. Publish one interactive doc:
49
+
50
+ ```json
51
+ {
52
+ "action": "set",
53
+ "doc": {
54
+ "v": 1,
55
+ "ui_id": "ui:demo",
56
+ "title": "Demo",
57
+ "summary": "Minimal interactive panel",
58
+ "components": [
59
+ { "kind": "text", "id": "intro", "text": "Choose an option", "metadata": {} }
60
+ ],
61
+ "actions": [
62
+ {
63
+ "id": "ack",
64
+ "label": "Acknowledge",
65
+ "kind": "primary",
66
+ "payload": { "choice": "ack" },
67
+ "metadata": { "command_text": "/answer ack" }
68
+ }
69
+ ],
70
+ "revision": { "id": "rev:demo:1", "version": 1 },
71
+ "updated_at_ms": 1,
72
+ "metadata": {}
73
+ }
74
+ }
75
+ ```
76
+
77
+ 2. Verify live state:
78
+
79
+ ```text
80
+ /mu ui status
81
+ /mu ui snapshot compact
82
+ /mu ui snapshot multiline
83
+ ```
84
+
85
+ 3. Handle command (`/answer ack`) in normal skill logic.
86
+
87
+ 4. Remove prompt doc:
88
+
89
+ ```json
90
+ { "action": "remove", "ui_id": "ui:demo" }
91
+ ```
92
+
93
+ ## UiDoc schema cheat sheet
94
+
95
+ Required top-level fields for each doc:
96
+
97
+ - `v`: `1`
98
+ - `ui_id`: non-empty string (max 64)
99
+ - `title`: non-empty string
100
+ - `components`: non-empty array (`text|list|key_value|divider`)
101
+ - `revision`: `{ id: string, version: nonnegative int }`
102
+ - `updated_at_ms`: nonnegative integer
103
+
104
+ Common optional fields (recommended):
105
+
106
+ - `summary`: deterministic fallback summary
107
+ - `actions`: interactive options (empty for pure status)
108
+ - `metadata`: profile/snapshot metadata and custom annotations
109
+
110
+ Component minimums:
111
+
112
+ - `text`: `id`, `text`
113
+ - `list`: `id`, `items[]` (`id`, `label`, optional `detail`, optional `tone`)
114
+ - `key_value`: `id`, `rows[]` (`key`, `value`, optional `tone`)
115
+ - `divider`: `id`
116
+
117
+ Action minimums:
118
+
119
+ - `id`, `label`
120
+ - `metadata.command_text` required for interactive routing
121
+ - optional: `kind`, `description`, `payload`, `component_id`, `callback_token`
122
+
123
+ ## mu_ui action semantics
124
+
125
+ - `status`
126
+ - Returns count, `ui_id` list, status-profile counts/warnings, and awaiting counts.
127
+ - `snapshot`
128
+ - `snapshot_format`: `compact|multiline` (defaults to `compact`).
129
+ - `set`
130
+ - Upsert one doc by `ui_id`.
131
+ - `update`
132
+ - Same behavior as `set` (single-doc upsert).
133
+ - `replace`
134
+ - Replace entire active doc set with `docs[]`.
135
+ - `remove`
136
+ - Remove one doc by `ui_id`.
137
+ - `clear`
138
+ - Remove all docs for the session.
139
+
140
+ ## Canonical templates
141
+
142
+ ### 1) Interactive `/answer` prompt
143
+
144
+ ```json
145
+ {
146
+ "action": "set",
147
+ "doc": {
148
+ "v": 1,
149
+ "ui_id": "ui:answer",
150
+ "title": "Answer",
151
+ "summary": "Please choose yes or no",
152
+ "components": [
153
+ { "kind": "text", "id": "prompt", "text": "Choose an answer", "metadata": {} }
154
+ ],
155
+ "actions": [
156
+ {
157
+ "id": "answer_yes",
158
+ "label": "Yes",
159
+ "kind": "primary",
160
+ "payload": { "choice": "yes" },
161
+ "metadata": { "command_text": "/answer yes" }
162
+ },
163
+ {
164
+ "id": "answer_no",
165
+ "label": "No",
166
+ "kind": "secondary",
167
+ "payload": { "choice": "no" },
168
+ "metadata": { "command_text": "/answer no" }
169
+ }
170
+ ],
171
+ "revision": { "id": "rev:answer:1", "version": 1 },
172
+ "updated_at_ms": 1,
173
+ "metadata": {}
174
+ }
175
+ }
176
+ ```
177
+
178
+ Handler contract:
179
+
180
+ 1. Parse `/answer <choice>`.
181
+ 2. Validate choice.
182
+ 3. `mu_ui remove` (or `clear`) for `ui:answer`.
183
+ 4. Emit normal response.
184
+
185
+ ### 2) Status-profile doc (non-interactive)
186
+
187
+ ```json
188
+ {
189
+ "action": "set",
190
+ "doc": {
191
+ "v": 1,
192
+ "ui_id": "ui:planning",
193
+ "title": "Planning status",
194
+ "summary": "Drafting issue DAG",
195
+ "components": [
196
+ {
197
+ "kind": "key_value",
198
+ "id": "kv",
199
+ "rows": [
200
+ { "key": "phase", "value": "decomposition" },
201
+ { "key": "next", "value": "approval prompt" }
202
+ ],
203
+ "metadata": {}
204
+ },
205
+ {
206
+ "kind": "list",
207
+ "id": "milestones",
208
+ "items": [
209
+ { "id": "m1", "label": "Root issue captured" },
210
+ { "id": "m2", "label": "Leaf tasks drafted" }
211
+ ],
212
+ "metadata": {}
213
+ }
214
+ ],
215
+ "actions": [],
216
+ "revision": { "id": "rev:planning:12", "version": 12 },
217
+ "updated_at_ms": 1730000000000,
218
+ "metadata": {
219
+ "profile": {
220
+ "id": "planning",
221
+ "variant": "status",
222
+ "snapshot": {
223
+ "compact": "planning: DAG draft ready",
224
+ "multiline": "phase: decomposition\nnext: approval prompt"
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ ```
231
+
232
+ ### 3) Parameterized command text with payload defaults
233
+
234
+ ```json
235
+ {
236
+ "id": "approve",
237
+ "label": "Approve",
238
+ "payload": { "choice": "approve", "note": "looks good" },
239
+ "metadata": { "command_text": "/answer choice={{choice}} note={{note}}" }
240
+ }
241
+ ```
242
+
243
+ In terminal UI interaction flow, placeholders are auto-filled from `payload` when possible, and unresolved fields are prompted.
244
+
245
+ ## Status-profile rules
246
+
247
+ When `metadata.profile.id` is one of `planning|subagents|control-flow|model-routing` and variant is `status`:
248
+
249
+ - expected `ui_id` values:
250
+ - `planning` -> `ui:planning`
251
+ - `subagents` -> `ui:subagents`
252
+ - `control-flow` -> `ui:control-flow`
253
+ - `model-routing` -> `ui:model-routing`
254
+ - keep `actions: []` (status docs are non-interactive)
255
+ - include `summary` plus `metadata.profile.snapshot.compact`
256
+ - preferred components by profile:
257
+ - `planning`: `key_value` + `list`
258
+ - `subagents`: `key_value` + `list`
259
+ - `control-flow`: `key_value`
260
+ - `model-routing`: `key_value` + `list`
261
+
262
+ Use `/mu ui status` to catch profile warnings early.
263
+
264
+ ## Debugging playbook
265
+
266
+ - **`doc is required`**
267
+ - Missing `doc` parameter for `set|update`.
268
+ - **`Invalid UiDoc.`**
269
+ - Schema mismatch. Re-check required fields and component/action shapes.
270
+ - **`docs must be an array` / `docs[i]: invalid UiDoc`**
271
+ - `replace` payload malformed.
272
+ - **Action appears but cannot run**
273
+ - Missing/empty `metadata.command_text`, or status-profile doc (actions intentionally non-runnable).
274
+ - **`awaiting` stays non-zero**
275
+ - Prompt doc still active. Remove with `mu_ui remove` once handled.
276
+
277
+ ## Verify and teardown checklist
278
+
279
+ After each change:
280
+
281
+ 1. `/mu ui status` shows expected doc count and ids.
282
+ 2. `/mu ui snapshot compact` shows deterministic summary.
283
+ 3. `/mu ui snapshot multiline` shows readable panel/action projection.
284
+ 4. Prompt resolved? Remove/clear doc explicitly.
285
+ 5. Keep issue/forum truth in sync; `mu_ui` is communication state, not source-of-truth task state.
286
+
287
+ ## Evaluation scenarios
288
+
289
+ 1. **First-time interactive prompt**
290
+ - Publish `ui:answer`, click action, confirm `/answer ...` reaches normal handler, remove prompt doc.
291
+
292
+ 2. **Status + decision split**
293
+ - Keep `ui:planning` status-profile doc active; open separate interactive approval doc; verify status remains non-interactive.
294
+
295
+ 3. **Replay/reconnect safety**
296
+ - Publish revisions 1 then 2; replay stale rev 1; verify highest revision remains active.
297
+
298
+ 4. **Channel degrade resilience**
299
+ - Ensure every actionable row has deterministic `command_text` fallback so manual command entry always works.