@femtomc/mu-agent 26.2.108 → 26.2.110
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 +4 -19
- package/assets/mu-tui-logo.png +0 -0
- package/dist/extensions/ui.d.ts.map +1 -1
- package/dist/extensions/ui.js +118 -112
- package/package.json +2 -2
- package/prompts/skills/core/SKILL.md +7 -4
- package/prompts/skills/core/mu/SKILL.md +2 -0
- package/prompts/skills/core/programmable-ui/SKILL.md +299 -0
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`)
|
|
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
|
|
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
|
|
package/assets/mu-tui-logo.png
CHANGED
|
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;
|
|
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;AAs/BpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA6N3C;AAED,eAAe,WAAW,CAAC"}
|
package/dist/extensions/ui.js
CHANGED
|
@@ -2,22 +2,29 @@ import { normalizeUiDocs, parseUiDoc, resolveUiStatusProfileName, uiStatusProfil
|
|
|
2
2
|
import { matchesKey } from "@mariozechner/pi-tui";
|
|
3
3
|
import { registerMuSubcommand } from "./mu-command-dispatcher.js";
|
|
4
4
|
const UI_DISPLAY_DOCS_MAX = 16;
|
|
5
|
-
const UI_WIDGET_COMPONENTS_MAX = 6;
|
|
6
|
-
const UI_WIDGET_ACTIONS_MAX = 4;
|
|
7
5
|
const UI_PICKER_COMPONENTS_MAX = 8;
|
|
8
6
|
const UI_PICKER_LIST_ITEMS_MAX = 4;
|
|
9
7
|
const UI_PICKER_KEYVALUE_ROWS_MAX = 4;
|
|
10
8
|
const UI_SESSION_KEY_FALLBACK = "__mu_ui_active_session__";
|
|
11
9
|
const UI_PROMPT_PREVIEW_MAX = 160;
|
|
12
10
|
const UI_INTERACT_SHORTCUT = "ctrl+shift+u";
|
|
11
|
+
const UI_INTERACT_OVERLAY_OPTIONS = {
|
|
12
|
+
anchor: "top-left",
|
|
13
|
+
row: 0,
|
|
14
|
+
col: 0,
|
|
15
|
+
width: "100%",
|
|
16
|
+
maxHeight: "100%",
|
|
17
|
+
margin: 0,
|
|
18
|
+
};
|
|
13
19
|
const STATE_BY_SESSION = new Map();
|
|
14
20
|
const UI_STATE_TTL_MS = 30 * 60 * 1000; // keep session state for 30 minutes after last access
|
|
15
21
|
function createState() {
|
|
16
22
|
return {
|
|
17
23
|
docsById: new Map(),
|
|
18
|
-
|
|
24
|
+
pendingPrompts: [],
|
|
19
25
|
promptedRevisionKeys: new Set(),
|
|
20
26
|
awaitingUiIds: new Set(),
|
|
27
|
+
interactionDepth: 0,
|
|
21
28
|
};
|
|
22
29
|
}
|
|
23
30
|
function pruneStaleStates(nowMs) {
|
|
@@ -78,6 +85,38 @@ function retainAwaitingUiIdsForActiveDocs(state) {
|
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
87
|
}
|
|
88
|
+
function retainPendingPromptsForActiveDocs(state) {
|
|
89
|
+
state.pendingPrompts = state.pendingPrompts.filter((pending) => {
|
|
90
|
+
const doc = state.docsById.get(pending.uiId);
|
|
91
|
+
if (!doc) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (pending.kind === "review") {
|
|
95
|
+
return isStatusProfileStatusVariant(doc);
|
|
96
|
+
}
|
|
97
|
+
const actions = runnableActions(doc);
|
|
98
|
+
if (actions.length === 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (!pending.actionId) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return actions.some((action) => action.id === pending.actionId);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function removePendingPromptsForUiId(state, uiId) {
|
|
108
|
+
state.pendingPrompts = state.pendingPrompts.filter((pending) => pending.uiId !== uiId);
|
|
109
|
+
}
|
|
110
|
+
function enqueuePendingPrompt(state, pending) {
|
|
111
|
+
const duplicate = state.pendingPrompts.some((existing) => {
|
|
112
|
+
return (existing.kind === pending.kind &&
|
|
113
|
+
existing.uiId === pending.uiId &&
|
|
114
|
+
existing.actionId === pending.actionId);
|
|
115
|
+
});
|
|
116
|
+
if (!duplicate) {
|
|
117
|
+
state.pendingPrompts.push(pending);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
81
120
|
function armAutoPromptForUiDocs(state, changedUiIds) {
|
|
82
121
|
if (changedUiIds.length === 0) {
|
|
83
122
|
return;
|
|
@@ -89,26 +128,30 @@ function armAutoPromptForUiDocs(state, changedUiIds) {
|
|
|
89
128
|
changedDocs.push(doc);
|
|
90
129
|
}
|
|
91
130
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
return !state.promptedRevisionKeys.has(docRevisionKey(doc));
|
|
97
|
-
});
|
|
98
|
-
if (candidates.length === 0) {
|
|
131
|
+
const unpromptedDocs = changedDocs.filter((doc) => !state.promptedRevisionKeys.has(docRevisionKey(doc)));
|
|
132
|
+
if (unpromptedDocs.length === 0) {
|
|
99
133
|
return;
|
|
100
134
|
}
|
|
101
|
-
|
|
135
|
+
const byMostRecentRevision = (left, right) => {
|
|
102
136
|
if (left.updated_at_ms !== right.updated_at_ms) {
|
|
103
137
|
return right.updated_at_ms - left.updated_at_ms;
|
|
104
138
|
}
|
|
105
139
|
return left.ui_id.localeCompare(right.ui_id);
|
|
106
|
-
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
};
|
|
141
|
+
const runnableCandidates = unpromptedDocs.filter((doc) => runnableActions(doc).length > 0).sort(byMostRecentRevision);
|
|
142
|
+
if (runnableCandidates.length > 0) {
|
|
143
|
+
const doc = runnableCandidates[0];
|
|
144
|
+
const actions = runnableActions(doc);
|
|
145
|
+
const actionId = actions.length === 1 ? actions[0].id : undefined;
|
|
146
|
+
enqueuePendingPrompt(state, { kind: "action", uiId: doc.ui_id, actionId });
|
|
147
|
+
state.promptedRevisionKeys.add(docRevisionKey(doc));
|
|
148
|
+
}
|
|
149
|
+
const statusCandidates = unpromptedDocs.filter((doc) => isStatusProfileStatusVariant(doc)).sort(byMostRecentRevision);
|
|
150
|
+
if (statusCandidates.length > 0) {
|
|
151
|
+
const doc = statusCandidates[0];
|
|
152
|
+
enqueuePendingPrompt(state, { kind: "review", uiId: doc.ui_id });
|
|
153
|
+
state.promptedRevisionKeys.add(docRevisionKey(doc));
|
|
154
|
+
}
|
|
112
155
|
}
|
|
113
156
|
function preferredDocForState(state, candidate) {
|
|
114
157
|
const existing = state.docsById.get(candidate.ui_id);
|
|
@@ -122,6 +165,23 @@ function preferredDocForState(state, candidate) {
|
|
|
122
165
|
function awaitingDocs(state, docs) {
|
|
123
166
|
return docs.filter((doc) => state.awaitingUiIds.has(doc.ui_id) && runnableActions(doc).length > 0);
|
|
124
167
|
}
|
|
168
|
+
function beginUiInteraction(ctx, state) {
|
|
169
|
+
state.interactionDepth += 1;
|
|
170
|
+
refreshUi(ctx);
|
|
171
|
+
}
|
|
172
|
+
function endUiInteraction(ctx, state) {
|
|
173
|
+
state.interactionDepth = Math.max(0, state.interactionDepth - 1);
|
|
174
|
+
refreshUi(ctx);
|
|
175
|
+
}
|
|
176
|
+
async function withUiInteraction(ctx, state, run) {
|
|
177
|
+
beginUiInteraction(ctx, state);
|
|
178
|
+
try {
|
|
179
|
+
return await run();
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
endUiInteraction(ctx, state);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
125
185
|
function short(text, max = 64) {
|
|
126
186
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
127
187
|
if (normalized.length <= max) {
|
|
@@ -430,9 +490,11 @@ function boundedIndex(index, length) {
|
|
|
430
490
|
function pickerComponentLines(component) {
|
|
431
491
|
switch (component.kind) {
|
|
432
492
|
case "text":
|
|
433
|
-
return [
|
|
493
|
+
return [component.text];
|
|
434
494
|
case "list": {
|
|
435
|
-
const
|
|
495
|
+
const title = component.title?.trim();
|
|
496
|
+
const prefix = title && title.length > 0 ? `${title} · ` : "";
|
|
497
|
+
const lines = [`${prefix}${component.items.length} item(s)`];
|
|
436
498
|
const visible = component.items.slice(0, UI_PICKER_LIST_ITEMS_MAX);
|
|
437
499
|
for (const item of visible) {
|
|
438
500
|
const detail = item.detail ? ` — ${item.detail}` : "";
|
|
@@ -444,7 +506,9 @@ function pickerComponentLines(component) {
|
|
|
444
506
|
return lines;
|
|
445
507
|
}
|
|
446
508
|
case "key_value": {
|
|
447
|
-
const
|
|
509
|
+
const title = component.title?.trim();
|
|
510
|
+
const prefix = title && title.length > 0 ? `${title} · ` : "";
|
|
511
|
+
const lines = [`${prefix}${component.rows.length} row(s)`];
|
|
448
512
|
const visible = component.rows.slice(0, UI_PICKER_KEYVALUE_ROWS_MAX);
|
|
449
513
|
for (const row of visible) {
|
|
450
514
|
lines.push(`${row.key}: ${row.value}`);
|
|
@@ -455,7 +519,7 @@ function pickerComponentLines(component) {
|
|
|
455
519
|
return lines;
|
|
456
520
|
}
|
|
457
521
|
case "divider":
|
|
458
|
-
return ["
|
|
522
|
+
return ["—"];
|
|
459
523
|
default:
|
|
460
524
|
return ["component"];
|
|
461
525
|
}
|
|
@@ -636,18 +700,14 @@ async function pickUiActionInteractively(opts) {
|
|
|
636
700
|
initialActionId: opts.actionId,
|
|
637
701
|
}), {
|
|
638
702
|
overlay: true,
|
|
639
|
-
overlayOptions:
|
|
640
|
-
anchor: "center",
|
|
641
|
-
width: "78%",
|
|
642
|
-
maxHeight: "70%",
|
|
643
|
-
margin: 1,
|
|
644
|
-
},
|
|
703
|
+
overlayOptions: UI_INTERACT_OVERLAY_OPTIONS,
|
|
645
704
|
});
|
|
646
705
|
return selected ?? null;
|
|
647
706
|
}
|
|
648
707
|
function applyUiAction(params, state) {
|
|
649
708
|
retainPromptedRevisionKeysForActiveDocs(state);
|
|
650
709
|
retainAwaitingUiIdsForActiveDocs(state);
|
|
710
|
+
retainPendingPromptsForActiveDocs(state);
|
|
651
711
|
const docs = activeDocs(state);
|
|
652
712
|
const awaitingCount = awaitingDocs(state, docs).length;
|
|
653
713
|
switch (params.action) {
|
|
@@ -690,6 +750,7 @@ function applyUiAction(params, state) {
|
|
|
690
750
|
}
|
|
691
751
|
retainPromptedRevisionKeysForActiveDocs(state);
|
|
692
752
|
retainAwaitingUiIdsForActiveDocs(state);
|
|
753
|
+
retainPendingPromptsForActiveDocs(state);
|
|
693
754
|
return {
|
|
694
755
|
ok: true,
|
|
695
756
|
action: params.action,
|
|
@@ -713,6 +774,7 @@ function applyUiAction(params, state) {
|
|
|
713
774
|
}
|
|
714
775
|
retainPromptedRevisionKeysForActiveDocs(state);
|
|
715
776
|
retainAwaitingUiIdsForActiveDocs(state);
|
|
777
|
+
retainPendingPromptsForActiveDocs(state);
|
|
716
778
|
return {
|
|
717
779
|
ok: true,
|
|
718
780
|
action: "replace",
|
|
@@ -732,17 +794,16 @@ function applyUiAction(params, state) {
|
|
|
732
794
|
if (!state.docsById.delete(uiId)) {
|
|
733
795
|
return { ok: false, action: "remove", message: `UI doc not found: ${uiId}` };
|
|
734
796
|
}
|
|
735
|
-
|
|
736
|
-
state.pendingPrompt = null;
|
|
737
|
-
}
|
|
797
|
+
removePendingPromptsForUiId(state, uiId);
|
|
738
798
|
state.awaitingUiIds.delete(uiId);
|
|
739
799
|
retainPromptedRevisionKeysForActiveDocs(state);
|
|
740
800
|
retainAwaitingUiIdsForActiveDocs(state);
|
|
801
|
+
retainPendingPromptsForActiveDocs(state);
|
|
741
802
|
return { ok: true, action: "remove", message: `UI doc removed: ${uiId}` };
|
|
742
803
|
}
|
|
743
804
|
case "clear":
|
|
744
805
|
state.docsById.clear();
|
|
745
|
-
state.
|
|
806
|
+
state.pendingPrompts = [];
|
|
746
807
|
state.promptedRevisionKeys.clear();
|
|
747
808
|
state.awaitingUiIds.clear();
|
|
748
809
|
return { ok: true, action: "clear", message: "UI docs cleared." };
|
|
@@ -763,61 +824,6 @@ function buildToolResult(opts) {
|
|
|
763
824
|
};
|
|
764
825
|
return result;
|
|
765
826
|
}
|
|
766
|
-
function renderDocPreview(theme, doc, opts) {
|
|
767
|
-
const lines = [];
|
|
768
|
-
const headerParts = [theme.fg("accent", doc.title), theme.fg("muted", `[${doc.ui_id}]`)];
|
|
769
|
-
if (opts.awaitingResponse) {
|
|
770
|
-
headerParts.push(theme.fg("accent", "awaiting-response"));
|
|
771
|
-
}
|
|
772
|
-
lines.push(headerParts.join(" "));
|
|
773
|
-
if (doc.summary) {
|
|
774
|
-
lines.push(theme.fg("muted", short(doc.summary, 80)));
|
|
775
|
-
}
|
|
776
|
-
if (opts.awaitingCount > 0) {
|
|
777
|
-
lines.push(theme.fg("accent", `Awaiting user response for ${opts.awaitingCount} UI doc(s).`));
|
|
778
|
-
}
|
|
779
|
-
const components = doc.components.slice(0, UI_WIDGET_COMPONENTS_MAX);
|
|
780
|
-
if (components.length > 0) {
|
|
781
|
-
lines.push(theme.fg("dim", "Components:"));
|
|
782
|
-
for (const component of components) {
|
|
783
|
-
lines.push(` ${componentPreview(component)}`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
const interactiveActions = runnableActions(doc);
|
|
787
|
-
if (interactiveActions.length > 0) {
|
|
788
|
-
lines.push(theme.fg("muted", "Actions:"));
|
|
789
|
-
const visibleActions = interactiveActions.slice(0, UI_WIDGET_ACTIONS_MAX);
|
|
790
|
-
for (let idx = 0; idx < visibleActions.length; idx += 1) {
|
|
791
|
-
const action = visibleActions[idx];
|
|
792
|
-
lines.push(` ${idx + 1}. ${action.label}`);
|
|
793
|
-
}
|
|
794
|
-
if (interactiveActions.length > visibleActions.length) {
|
|
795
|
-
lines.push(` ... (+${interactiveActions.length - visibleActions.length} more actions)`);
|
|
796
|
-
}
|
|
797
|
-
if (opts.awaitingResponse) {
|
|
798
|
-
lines.push(theme.fg("accent", "Awaiting your response. Select an action to continue."));
|
|
799
|
-
}
|
|
800
|
-
lines.push(theme.fg("dim", `Press ${UI_INTERACT_SHORTCUT} to compose and submit a prompt from actions.`));
|
|
801
|
-
}
|
|
802
|
-
else {
|
|
803
|
-
lines.push(theme.fg("dim", "No interactive actions."));
|
|
804
|
-
}
|
|
805
|
-
return lines;
|
|
806
|
-
}
|
|
807
|
-
function componentPreview(component) {
|
|
808
|
-
const { kind } = component;
|
|
809
|
-
switch (kind) {
|
|
810
|
-
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)`;
|
|
816
|
-
case "divider":
|
|
817
|
-
return "divider";
|
|
818
|
-
}
|
|
819
|
-
return kind;
|
|
820
|
-
}
|
|
821
827
|
function refreshUi(ctx) {
|
|
822
828
|
const key = sessionKey(ctx);
|
|
823
829
|
const state = ensureState(key);
|
|
@@ -833,39 +839,33 @@ function refreshUi(ctx) {
|
|
|
833
839
|
}
|
|
834
840
|
const awaiting = awaitingDocs(state, docs);
|
|
835
841
|
const labels = docs.map((doc) => doc.ui_id).join(", ");
|
|
842
|
+
const readiness = state.interactionDepth > 0
|
|
843
|
+
? ctx.ui.theme.fg("accent", "prompting")
|
|
844
|
+
: awaiting.length > 0
|
|
845
|
+
? ctx.ui.theme.fg("accent", `awaiting ${awaiting.length}`)
|
|
846
|
+
: ctx.ui.theme.fg("dim", "ready");
|
|
836
847
|
ctx.ui.setStatus("mu-ui", [
|
|
837
848
|
ctx.ui.theme.fg("dim", "ui"),
|
|
838
849
|
ctx.ui.theme.fg("muted", "·"),
|
|
839
850
|
ctx.ui.theme.fg("accent", `${docs.length}`),
|
|
840
851
|
ctx.ui.theme.fg("muted", "·"),
|
|
841
|
-
|
|
842
|
-
? ctx.ui.theme.fg("accent", `awaiting ${awaiting.length}`)
|
|
843
|
-
: ctx.ui.theme.fg("dim", "ready"),
|
|
852
|
+
readiness,
|
|
844
853
|
ctx.ui.theme.fg("muted", "·"),
|
|
845
854
|
ctx.ui.theme.fg("text", labels),
|
|
846
855
|
].join(" "));
|
|
847
|
-
|
|
848
|
-
ctx.ui.setWidget("mu-ui", renderDocPreview(ctx.ui.theme, primaryDoc, {
|
|
849
|
-
awaitingResponse: state.awaitingUiIds.has(primaryDoc.ui_id),
|
|
850
|
-
awaitingCount: awaiting.length,
|
|
851
|
-
}), { placement: "belowEditor" });
|
|
856
|
+
ctx.ui.setWidget("mu-ui", undefined);
|
|
852
857
|
}
|
|
853
858
|
export function uiExtension(pi) {
|
|
854
859
|
const commandUsage = "/mu ui status|snapshot [compact|multiline]|interact [ui_id [action_id]]";
|
|
855
860
|
const usage = `Usage: ${commandUsage}`;
|
|
856
|
-
const runUiActionFromDoc = async (ctx, state, uiId, actionId) => {
|
|
861
|
+
const runUiActionFromDoc = async (ctx, state, uiId, actionId) => withUiInteraction(ctx, state, async () => {
|
|
857
862
|
const docs = activeDocs(state, UI_DISPLAY_DOCS_MAX);
|
|
858
863
|
if (docs.length === 0) {
|
|
859
864
|
ctx.ui.notify("No UI docs are currently available.", "info");
|
|
860
865
|
return;
|
|
861
866
|
}
|
|
862
|
-
const entries = docs
|
|
863
|
-
|
|
864
|
-
.filter((entry) => entry.actions.length > 0);
|
|
865
|
-
if (entries.length === 0) {
|
|
866
|
-
ctx.ui.notify("No runnable UI actions are currently available.", "error");
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
867
|
+
const entries = docs.map((doc) => ({ doc, actions: runnableActions(doc) }));
|
|
868
|
+
const runnableEntries = entries.filter((entry) => entry.actions.length > 0);
|
|
869
869
|
let selectedDoc = null;
|
|
870
870
|
let selectedAction = null;
|
|
871
871
|
const normalizedUiId = uiId?.trim() ?? "";
|
|
@@ -892,14 +892,19 @@ export function uiExtension(pi) {
|
|
|
892
892
|
actionId: normalizedActionId.length > 0 ? normalizedActionId : undefined,
|
|
893
893
|
});
|
|
894
894
|
if (!picked) {
|
|
895
|
-
|
|
895
|
+
if (runnableEntries.length > 0) {
|
|
896
|
+
ctx.ui.notify("UI interaction cancelled.", "info");
|
|
897
|
+
}
|
|
896
898
|
return;
|
|
897
899
|
}
|
|
898
900
|
selectedDoc = picked.doc;
|
|
899
901
|
selectedAction = picked.action;
|
|
900
902
|
}
|
|
901
903
|
if (!selectedDoc || !selectedAction) {
|
|
902
|
-
|
|
904
|
+
if (runnableEntries.length === 0) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
ctx.ui.notify("No UI action was selected.", "info");
|
|
903
908
|
return;
|
|
904
909
|
}
|
|
905
910
|
const commandText = actionCommandText(selectedAction);
|
|
@@ -938,12 +943,11 @@ export function uiExtension(pi) {
|
|
|
938
943
|
}
|
|
939
944
|
pi.sendUserMessage(finalPrompt);
|
|
940
945
|
state.awaitingUiIds.delete(selectedDoc.ui_id);
|
|
941
|
-
|
|
942
|
-
state.pendingPrompt = null;
|
|
943
|
-
}
|
|
946
|
+
removePendingPromptsForUiId(state, selectedDoc.ui_id);
|
|
944
947
|
retainAwaitingUiIdsForActiveDocs(state);
|
|
948
|
+
retainPendingPromptsForActiveDocs(state);
|
|
945
949
|
ctx.ui.notify(`Submitted prompt from ${selectedDoc.ui_id}/${selectedAction.id}.`, "info");
|
|
946
|
-
};
|
|
950
|
+
});
|
|
947
951
|
registerMuSubcommand(pi, {
|
|
948
952
|
subcommand: "ui",
|
|
949
953
|
summary: "Inspect and manage interactive UI docs",
|
|
@@ -982,7 +986,7 @@ export function uiExtension(pi) {
|
|
|
982
986
|
},
|
|
983
987
|
});
|
|
984
988
|
pi.registerShortcut(UI_INTERACT_SHORTCUT, {
|
|
985
|
-
description: "
|
|
989
|
+
description: "Open programmable UI modal and optionally submit prompt",
|
|
986
990
|
handler: async (ctx) => {
|
|
987
991
|
const key = sessionKey(ctx);
|
|
988
992
|
const state = ensureState(key);
|
|
@@ -1033,12 +1037,14 @@ export function uiExtension(pi) {
|
|
|
1033
1037
|
}
|
|
1034
1038
|
const key = sessionKey(ctx);
|
|
1035
1039
|
const state = ensureState(key);
|
|
1036
|
-
|
|
1040
|
+
retainPendingPromptsForActiveDocs(state);
|
|
1041
|
+
const pending = state.pendingPrompts.shift();
|
|
1037
1042
|
if (!pending) {
|
|
1038
1043
|
return;
|
|
1039
1044
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1045
|
+
if (pending.kind === "action") {
|
|
1046
|
+
ctx.ui.notify(`Agent requested input via ${pending.uiId}. Submit now or press ${UI_INTERACT_SHORTCUT} later.`, "info");
|
|
1047
|
+
}
|
|
1042
1048
|
await runUiActionFromDoc(ctx, state, pending.uiId, pending.actionId);
|
|
1043
1049
|
refreshUi(ctx);
|
|
1044
1050
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-agent",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.110",
|
|
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.
|
|
28
|
+
"@femtomc/mu-core": "26.2.110",
|
|
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.
|
|
21
|
-
3. Add `
|
|
22
|
-
4. Add `
|
|
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.
|