@femtomc/mu-agent 26.2.106 → 26.2.108

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 (42) hide show
  1. package/README.md +41 -21
  2. package/assets/mu-tui-logo.png +0 -0
  3. package/dist/extensions/index.d.ts +1 -1
  4. package/dist/extensions/index.d.ts.map +1 -1
  5. package/dist/extensions/index.js +1 -1
  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.js +2 -2
  10. package/dist/extensions/mu-serve.js +2 -2
  11. package/dist/extensions/ui.d.ts +4 -0
  12. package/dist/extensions/ui.d.ts.map +1 -0
  13. package/dist/extensions/ui.js +1055 -0
  14. package/dist/operator.d.ts +93 -255
  15. package/dist/operator.d.ts.map +1 -1
  16. package/dist/operator.js +41 -32
  17. package/package.json +33 -33
  18. package/prompts/skills/automation/SKILL.md +25 -0
  19. package/prompts/skills/{crons → automation/crons}/SKILL.md +2 -2
  20. package/prompts/skills/{heartbeats → automation/heartbeats}/SKILL.md +2 -2
  21. package/prompts/skills/core/SKILL.md +28 -0
  22. package/prompts/skills/{code-mode → core/code-mode}/SKILL.md +1 -1
  23. package/prompts/skills/{memory → core/memory}/SKILL.md +2 -2
  24. package/prompts/skills/{mu → core/mu}/SKILL.md +52 -9
  25. package/prompts/skills/{tmux → core/tmux}/SKILL.md +1 -1
  26. package/prompts/skills/messaging/SKILL.md +27 -0
  27. package/prompts/skills/subagents/SKILL.md +93 -243
  28. package/prompts/skills/{control-flow → subagents/control-flow}/SKILL.md +122 -17
  29. package/prompts/skills/subagents/execution/SKILL.md +428 -0
  30. package/prompts/skills/{model-routing → subagents/model-routing}/SKILL.md +179 -19
  31. package/prompts/skills/subagents/planning/SKILL.md +393 -0
  32. package/prompts/skills/{orchestration → subagents/protocol}/SKILL.md +7 -10
  33. package/prompts/skills/writing/SKILL.md +3 -2
  34. package/dist/extensions/hud.d.ts +0 -4
  35. package/dist/extensions/hud.d.ts.map +0 -1
  36. package/dist/extensions/hud.js +0 -483
  37. package/prompts/skills/hud/SKILL.md +0 -205
  38. package/prompts/skills/planning/SKILL.md +0 -244
  39. /package/prompts/skills/{setup-discord → messaging/setup-discord}/SKILL.md +0 -0
  40. /package/prompts/skills/{setup-neovim → messaging/setup-neovim}/SKILL.md +0 -0
  41. /package/prompts/skills/{setup-slack → messaging/setup-slack}/SKILL.md +0 -0
  42. /package/prompts/skills/{setup-telegram → messaging/setup-telegram}/SKILL.md +0 -0
@@ -0,0 +1,1055 @@
1
+ import { normalizeUiDocs, parseUiDoc, resolveUiStatusProfileName, uiStatusProfileWarnings, } from "@femtomc/mu-core";
2
+ import { matchesKey } from "@mariozechner/pi-tui";
3
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
4
+ const UI_DISPLAY_DOCS_MAX = 16;
5
+ const UI_WIDGET_COMPONENTS_MAX = 6;
6
+ const UI_WIDGET_ACTIONS_MAX = 4;
7
+ const UI_PICKER_COMPONENTS_MAX = 8;
8
+ const UI_PICKER_LIST_ITEMS_MAX = 4;
9
+ const UI_PICKER_KEYVALUE_ROWS_MAX = 4;
10
+ const UI_SESSION_KEY_FALLBACK = "__mu_ui_active_session__";
11
+ const UI_PROMPT_PREVIEW_MAX = 160;
12
+ const UI_INTERACT_SHORTCUT = "ctrl+shift+u";
13
+ const STATE_BY_SESSION = new Map();
14
+ const UI_STATE_TTL_MS = 30 * 60 * 1000; // keep session state for 30 minutes after last access
15
+ function createState() {
16
+ return {
17
+ docsById: new Map(),
18
+ pendingPrompt: null,
19
+ promptedRevisionKeys: new Set(),
20
+ awaitingUiIds: new Set(),
21
+ };
22
+ }
23
+ function pruneStaleStates(nowMs) {
24
+ for (const [key, entry] of STATE_BY_SESSION.entries()) {
25
+ if (nowMs - entry.lastAccessMs > UI_STATE_TTL_MS) {
26
+ STATE_BY_SESSION.delete(key);
27
+ }
28
+ }
29
+ }
30
+ function sessionKey(ctx) {
31
+ const manager = ctx.sessionManager;
32
+ if (!manager) {
33
+ return UI_SESSION_KEY_FALLBACK;
34
+ }
35
+ const sessionId = manager.getSessionId();
36
+ return sessionId ?? UI_SESSION_KEY_FALLBACK;
37
+ }
38
+ function ensureState(key) {
39
+ const nowMs = Date.now();
40
+ pruneStaleStates(nowMs);
41
+ const existing = STATE_BY_SESSION.get(key);
42
+ if (existing) {
43
+ existing.lastAccessMs = nowMs;
44
+ return existing.state;
45
+ }
46
+ const fresh = createState();
47
+ STATE_BY_SESSION.set(key, { state: fresh, lastAccessMs: nowMs });
48
+ return fresh;
49
+ }
50
+ function touchState(key) {
51
+ const entry = STATE_BY_SESSION.get(key);
52
+ if (entry) {
53
+ entry.lastAccessMs = Date.now();
54
+ }
55
+ }
56
+ function activeDocs(state, maxDocs = UI_DISPLAY_DOCS_MAX) {
57
+ return normalizeUiDocs([...state.docsById.values()], { maxDocs });
58
+ }
59
+ function docRevisionKey(doc) {
60
+ return `${doc.ui_id}:${doc.revision.id}:${doc.revision.version}`;
61
+ }
62
+ function retainPromptedRevisionKeysForActiveDocs(state) {
63
+ const activeRevisionKeys = new Set();
64
+ for (const doc of state.docsById.values()) {
65
+ activeRevisionKeys.add(docRevisionKey(doc));
66
+ }
67
+ for (const key of [...state.promptedRevisionKeys]) {
68
+ if (!activeRevisionKeys.has(key)) {
69
+ state.promptedRevisionKeys.delete(key);
70
+ }
71
+ }
72
+ }
73
+ function retainAwaitingUiIdsForActiveDocs(state) {
74
+ for (const uiId of [...state.awaitingUiIds]) {
75
+ const doc = state.docsById.get(uiId);
76
+ if (!doc || runnableActions(doc).length === 0) {
77
+ state.awaitingUiIds.delete(uiId);
78
+ }
79
+ }
80
+ }
81
+ function armAutoPromptForUiDocs(state, changedUiIds) {
82
+ if (changedUiIds.length === 0) {
83
+ return;
84
+ }
85
+ const changedDocs = [];
86
+ for (const uiId of changedUiIds) {
87
+ const doc = state.docsById.get(uiId);
88
+ if (doc) {
89
+ changedDocs.push(doc);
90
+ }
91
+ }
92
+ const candidates = changedDocs.filter((doc) => {
93
+ if (runnableActions(doc).length === 0) {
94
+ return false;
95
+ }
96
+ return !state.promptedRevisionKeys.has(docRevisionKey(doc));
97
+ });
98
+ if (candidates.length === 0) {
99
+ return;
100
+ }
101
+ candidates.sort((left, right) => {
102
+ if (left.updated_at_ms !== right.updated_at_ms) {
103
+ return right.updated_at_ms - left.updated_at_ms;
104
+ }
105
+ return left.ui_id.localeCompare(right.ui_id);
106
+ });
107
+ const doc = candidates[0];
108
+ const actions = runnableActions(doc);
109
+ const actionId = actions.length === 1 ? actions[0].id : undefined;
110
+ state.pendingPrompt = { uiId: doc.ui_id, actionId };
111
+ state.promptedRevisionKeys.add(docRevisionKey(doc));
112
+ }
113
+ function preferredDocForState(state, candidate) {
114
+ const existing = state.docsById.get(candidate.ui_id);
115
+ if (!existing) {
116
+ return candidate;
117
+ }
118
+ const merged = normalizeUiDocs([existing, candidate], { maxDocs: 2 });
119
+ const chosen = merged.find((doc) => doc.ui_id === candidate.ui_id);
120
+ return chosen ?? candidate;
121
+ }
122
+ function awaitingDocs(state, docs) {
123
+ return docs.filter((doc) => state.awaitingUiIds.has(doc.ui_id) && runnableActions(doc).length > 0);
124
+ }
125
+ function short(text, max = 64) {
126
+ const normalized = text.replace(/\s+/g, " ").trim();
127
+ if (normalized.length <= max) {
128
+ return normalized;
129
+ }
130
+ if (max <= 1) {
131
+ return "…";
132
+ }
133
+ return `${normalized.slice(0, max - 1)}…`;
134
+ }
135
+ function statusProfileId(doc) {
136
+ return resolveUiStatusProfileName(doc);
137
+ }
138
+ function statusProfileMetadata(doc) {
139
+ const profile = doc.metadata.profile;
140
+ if (!isPlainObject(profile)) {
141
+ return null;
142
+ }
143
+ return profile;
144
+ }
145
+ function statusProfileVariant(doc) {
146
+ const profile = statusProfileMetadata(doc);
147
+ const rawVariant = typeof profile?.variant === "string" ? profile.variant.trim().toLowerCase() : "";
148
+ return rawVariant.length > 0 ? rawVariant : "status";
149
+ }
150
+ function isStatusProfileStatusVariant(doc) {
151
+ return statusProfileId(doc) !== null && statusProfileVariant(doc) === "status";
152
+ }
153
+ function statusProfileSnapshot(doc, format) {
154
+ if (!isStatusProfileStatusVariant(doc)) {
155
+ return null;
156
+ }
157
+ const profile = statusProfileMetadata(doc);
158
+ if (!profile) {
159
+ return null;
160
+ }
161
+ const snapshot = profile.snapshot;
162
+ if (!isPlainObject(snapshot)) {
163
+ return null;
164
+ }
165
+ const raw = snapshot[format];
166
+ if (typeof raw !== "string") {
167
+ return null;
168
+ }
169
+ const normalized = raw.trim();
170
+ return normalized.length > 0 ? normalized : null;
171
+ }
172
+ function compactSnapshotValue(doc) {
173
+ const profileCompact = statusProfileSnapshot(doc, "compact");
174
+ if (profileCompact) {
175
+ return short(profileCompact, 96);
176
+ }
177
+ if (doc.summary) {
178
+ return short(doc.summary, 64);
179
+ }
180
+ return `${short(doc.title, 32)} (${doc.revision.version})`;
181
+ }
182
+ function statusProfileDocCount(docs) {
183
+ return docs.reduce((count, doc) => count + (isStatusProfileStatusVariant(doc) ? 1 : 0), 0);
184
+ }
185
+ function statusProfileWarningCount(docs) {
186
+ return docs.reduce((count, doc) => count + uiStatusProfileWarnings(doc).length, 0);
187
+ }
188
+ function statusSummary(docs, awaitingCount = 0) {
189
+ const ids = docs.map((doc) => doc.ui_id).join(", ") || "(none)";
190
+ const parts = [`UI docs: ${docs.length}`, `ids: ${ids}`];
191
+ const statusProfiles = statusProfileDocCount(docs);
192
+ if (statusProfiles > 0) {
193
+ parts.push(`status_profiles: ${statusProfiles}`);
194
+ }
195
+ const warningCount = statusProfileWarningCount(docs);
196
+ if (warningCount > 0) {
197
+ parts.push(`profile_warnings: ${warningCount}`);
198
+ }
199
+ if (awaitingCount > 0) {
200
+ parts.push(`awaiting: ${awaitingCount}`);
201
+ }
202
+ return parts.join(" · ");
203
+ }
204
+ function snapshotText(docs, format) {
205
+ if (docs.length === 0) {
206
+ return "(no UI docs)";
207
+ }
208
+ if (format === "compact") {
209
+ return docs.map((doc) => `${doc.ui_id}: ${compactSnapshotValue(doc)}`).join(" | ");
210
+ }
211
+ const lines = [];
212
+ docs.slice(0, 8).forEach((doc, idx) => {
213
+ const profileId = statusProfileId(doc);
214
+ lines.push(`${idx + 1}. ${doc.title} [${doc.ui_id}]${profileId ? ` profile=${profileId}` : ""}`);
215
+ const profileMultiline = statusProfileSnapshot(doc, "multiline");
216
+ if (profileMultiline) {
217
+ const snapshotLines = profileMultiline
218
+ .split(/\r?\n/)
219
+ .map((line) => line.trim())
220
+ .filter((line) => line.length > 0);
221
+ if (snapshotLines.length > 0) {
222
+ for (const line of snapshotLines) {
223
+ lines.push(` ${short(line, 120)}`);
224
+ }
225
+ }
226
+ else {
227
+ lines.push(` snapshot: ${compactSnapshotValue(doc)}`);
228
+ }
229
+ }
230
+ else if (isStatusProfileStatusVariant(doc)) {
231
+ lines.push(` snapshot: ${compactSnapshotValue(doc)}`);
232
+ }
233
+ else if (doc.summary) {
234
+ lines.push(` summary: ${short(doc.summary, 120)}`);
235
+ }
236
+ if (isStatusProfileStatusVariant(doc)) {
237
+ if (doc.actions.length > 0) {
238
+ lines.push(` actions omitted for status profile (${doc.actions.length})`);
239
+ }
240
+ return;
241
+ }
242
+ if (doc.actions.length > 0) {
243
+ const labels = [...doc.actions]
244
+ .sort((left, right) => left.id.localeCompare(right.id))
245
+ .map((action) => action.label)
246
+ .join(", ");
247
+ lines.push(` actions: ${labels}`);
248
+ }
249
+ });
250
+ if (docs.length > 8) {
251
+ lines.push(`... (+${docs.length - 8} more docs)`);
252
+ }
253
+ return lines.join("\n");
254
+ }
255
+ function parseDocInput(value) {
256
+ if (value === undefined) {
257
+ return { ok: false, error: "doc is required" };
258
+ }
259
+ const parsed = parseUiDoc(value);
260
+ if (!parsed) {
261
+ return { ok: false, error: "Invalid UiDoc." };
262
+ }
263
+ return { ok: true, doc: parsed };
264
+ }
265
+ function parseDocListInput(value) {
266
+ if (!Array.isArray(value)) {
267
+ return { ok: false, error: "docs must be an array" };
268
+ }
269
+ const docs = [];
270
+ for (let idx = 0; idx < value.length; idx += 1) {
271
+ const parsed = parseUiDoc(value[idx]);
272
+ if (!parsed) {
273
+ return { ok: false, error: `docs[${idx}]: invalid UiDoc` };
274
+ }
275
+ docs.push(parsed);
276
+ }
277
+ return { ok: true, docs: normalizeUiDocs(docs, { maxDocs: UI_DISPLAY_DOCS_MAX }) };
278
+ }
279
+ function statusProfileWarningsExtraForDoc(doc) {
280
+ const warnings = uiStatusProfileWarnings(doc);
281
+ if (warnings.length === 0) {
282
+ return {};
283
+ }
284
+ return { profile_warnings: warnings };
285
+ }
286
+ function statusProfileWarningsExtraForDocs(docs) {
287
+ const byUiId = {};
288
+ for (const doc of docs) {
289
+ const warnings = uiStatusProfileWarnings(doc);
290
+ if (warnings.length > 0) {
291
+ byUiId[doc.ui_id] = warnings;
292
+ }
293
+ }
294
+ return Object.keys(byUiId).length > 0 ? { profile_warnings: byUiId } : {};
295
+ }
296
+ function parseSnapshotFormat(raw) {
297
+ const normalized = (raw ?? "compact").trim().toLowerCase();
298
+ return normalized === "multiline" ? "multiline" : "compact";
299
+ }
300
+ function isPlainObject(value) {
301
+ return typeof value === "object" && value !== null && !Array.isArray(value);
302
+ }
303
+ function actionCommandText(action) {
304
+ const raw = typeof action.metadata.command_text === "string" ? action.metadata.command_text.trim() : "";
305
+ return raw.length > 0 ? raw : null;
306
+ }
307
+ function extractTemplateKeys(text) {
308
+ const keys = [];
309
+ const seen = new Set();
310
+ const re = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
311
+ let match;
312
+ while ((match = re.exec(text)) !== null) {
313
+ const key = (match[1] ?? "").trim();
314
+ if (!key || seen.has(key)) {
315
+ continue;
316
+ }
317
+ seen.add(key);
318
+ keys.push(key);
319
+ }
320
+ return keys;
321
+ }
322
+ function escapeRegExp(value) {
323
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
324
+ }
325
+ function replaceTemplateValues(template, values) {
326
+ let out = template;
327
+ for (const [key, value] of Object.entries(values)) {
328
+ const keyRe = new RegExp(`\\{\\{\\s*${escapeRegExp(key)}\\s*\\}\\}`, "g");
329
+ out = out.replace(keyRe, value);
330
+ }
331
+ return out;
332
+ }
333
+ function primitiveTemplateDefault(value) {
334
+ switch (typeof value) {
335
+ case "string":
336
+ return value;
337
+ case "number":
338
+ case "boolean":
339
+ return String(value);
340
+ default:
341
+ return value === null ? "" : null;
342
+ }
343
+ }
344
+ function valueAtTemplatePath(root, key) {
345
+ if (!key) {
346
+ return undefined;
347
+ }
348
+ const segments = key.split(".").filter((segment) => segment.length > 0);
349
+ if (segments.length === 0) {
350
+ return undefined;
351
+ }
352
+ let current = root;
353
+ for (const segment of segments) {
354
+ if (Array.isArray(current)) {
355
+ const index = Number.parseInt(segment, 10);
356
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
357
+ return undefined;
358
+ }
359
+ current = current[index];
360
+ continue;
361
+ }
362
+ if (!isPlainObject(current)) {
363
+ return undefined;
364
+ }
365
+ current = current[segment];
366
+ }
367
+ return current;
368
+ }
369
+ function templateDefaultsForAction(action, keys) {
370
+ if (!isPlainObject(action.payload) || keys.length === 0) {
371
+ return {};
372
+ }
373
+ const payload = action.payload;
374
+ const out = {};
375
+ for (const key of keys) {
376
+ const fallback = primitiveTemplateDefault(valueAtTemplatePath(payload, key));
377
+ if (fallback !== null) {
378
+ out[key] = fallback;
379
+ }
380
+ }
381
+ return out;
382
+ }
383
+ async function collectTemplateValues(opts) {
384
+ if (opts.templateKeys.length === 0) {
385
+ return {};
386
+ }
387
+ const defaults = templateDefaultsForAction(opts.action, opts.templateKeys);
388
+ const out = { ...defaults };
389
+ for (const key of opts.templateKeys) {
390
+ const defaultValue = defaults[key];
391
+ if (typeof defaultValue === "string" && defaultValue.trim().length > 0) {
392
+ continue;
393
+ }
394
+ const placeholder = typeof defaultValue === "string" ? defaultValue : `value for ${key}`;
395
+ const entered = await opts.ctx.ui.input(`UI field: ${key}`, placeholder);
396
+ if (entered === undefined) {
397
+ return null;
398
+ }
399
+ out[key] = entered;
400
+ }
401
+ return out;
402
+ }
403
+ function composePromptFromAction(opts) {
404
+ const rendered = replaceTemplateValues(opts.commandText, opts.templateValues).trim();
405
+ const unresolvedKeys = extractTemplateKeys(rendered);
406
+ if (unresolvedKeys.length === 0) {
407
+ return rendered;
408
+ }
409
+ const unresolvedLines = unresolvedKeys.map((key) => `- ${key}: (missing)`);
410
+ return [rendered, "", "Missing template values:", ...unresolvedLines].join("\n");
411
+ }
412
+ function runnableActions(doc) {
413
+ if (isStatusProfileStatusVariant(doc)) {
414
+ return [];
415
+ }
416
+ return doc.actions.filter((action) => actionCommandText(action) !== null);
417
+ }
418
+ function boundedIndex(index, length) {
419
+ if (length <= 0) {
420
+ return 0;
421
+ }
422
+ if (index < 0) {
423
+ return 0;
424
+ }
425
+ if (index >= length) {
426
+ return length - 1;
427
+ }
428
+ return index;
429
+ }
430
+ function pickerComponentLines(component) {
431
+ switch (component.kind) {
432
+ case "text":
433
+ return [`text · ${component.text}`];
434
+ case "list": {
435
+ const lines = [`list${component.title ? ` · ${component.title}` : ""}`];
436
+ const visible = component.items.slice(0, UI_PICKER_LIST_ITEMS_MAX);
437
+ for (const item of visible) {
438
+ const detail = item.detail ? ` — ${item.detail}` : "";
439
+ lines.push(`• ${item.label}${detail}`);
440
+ }
441
+ if (component.items.length > visible.length) {
442
+ lines.push(`... (+${component.items.length - visible.length} more items)`);
443
+ }
444
+ return lines;
445
+ }
446
+ case "key_value": {
447
+ const lines = [`key_value${component.title ? ` · ${component.title}` : ""}`];
448
+ const visible = component.rows.slice(0, UI_PICKER_KEYVALUE_ROWS_MAX);
449
+ for (const row of visible) {
450
+ lines.push(`${row.key}: ${row.value}`);
451
+ }
452
+ if (component.rows.length > visible.length) {
453
+ lines.push(`... (+${component.rows.length - visible.length} more rows)`);
454
+ }
455
+ return lines;
456
+ }
457
+ case "divider":
458
+ return ["divider"];
459
+ default:
460
+ return ["component"];
461
+ }
462
+ }
463
+ class UiActionPickerComponent {
464
+ #entries;
465
+ #theme;
466
+ #done;
467
+ #mode = "doc";
468
+ #docIndex = 0;
469
+ #actionIndex = 0;
470
+ constructor(opts) {
471
+ this.#entries = opts.entries;
472
+ this.#theme = opts.theme;
473
+ this.#done = opts.done;
474
+ if (opts.initialUiId && opts.initialUiId.trim().length > 0) {
475
+ const initialDocIndex = this.#entries.findIndex((entry) => entry.doc.ui_id === opts.initialUiId);
476
+ if (initialDocIndex >= 0) {
477
+ this.#docIndex = initialDocIndex;
478
+ }
479
+ }
480
+ const actions = this.#currentActions();
481
+ if (opts.initialActionId && opts.initialActionId.trim().length > 0) {
482
+ const initialActionIndex = actions.findIndex((action) => action.id === opts.initialActionId);
483
+ if (initialActionIndex >= 0) {
484
+ this.#actionIndex = initialActionIndex;
485
+ this.#mode = "action";
486
+ }
487
+ }
488
+ }
489
+ #currentEntry() {
490
+ return this.#entries[this.#docIndex];
491
+ }
492
+ #currentActions() {
493
+ return this.#currentEntry().actions;
494
+ }
495
+ #currentAction() {
496
+ const actions = this.#currentActions();
497
+ return actions.length > 0 ? actions[boundedIndex(this.#actionIndex, actions.length)] : null;
498
+ }
499
+ #moveDoc(delta) {
500
+ this.#docIndex = boundedIndex(this.#docIndex + delta, this.#entries.length);
501
+ this.#actionIndex = boundedIndex(this.#actionIndex, this.#currentActions().length);
502
+ }
503
+ #moveAction(delta) {
504
+ const actions = this.#currentActions();
505
+ if (actions.length === 0) {
506
+ this.#actionIndex = 0;
507
+ return;
508
+ }
509
+ this.#actionIndex = boundedIndex(this.#actionIndex + delta, actions.length);
510
+ }
511
+ #submit() {
512
+ const action = this.#currentAction();
513
+ if (!action) {
514
+ this.#done(null);
515
+ return;
516
+ }
517
+ this.#done({
518
+ doc: this.#currentEntry().doc,
519
+ action,
520
+ });
521
+ }
522
+ handleInput(data) {
523
+ if (matchesKey(data, "escape")) {
524
+ this.#done(null);
525
+ return;
526
+ }
527
+ if (matchesKey(data, "tab") || matchesKey(data, "right")) {
528
+ this.#mode = "action";
529
+ this.#actionIndex = boundedIndex(this.#actionIndex, this.#currentActions().length);
530
+ return;
531
+ }
532
+ if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
533
+ this.#mode = "doc";
534
+ return;
535
+ }
536
+ if (matchesKey(data, "up")) {
537
+ if (this.#mode === "doc") {
538
+ this.#moveDoc(-1);
539
+ }
540
+ else {
541
+ this.#moveAction(-1);
542
+ }
543
+ return;
544
+ }
545
+ if (matchesKey(data, "down")) {
546
+ if (this.#mode === "doc") {
547
+ this.#moveDoc(1);
548
+ }
549
+ else {
550
+ this.#moveAction(1);
551
+ }
552
+ return;
553
+ }
554
+ if (matchesKey(data, "enter") || matchesKey(data, "return")) {
555
+ if (this.#mode === "doc") {
556
+ const actions = this.#currentActions();
557
+ if (actions.length <= 1) {
558
+ this.#submit();
559
+ }
560
+ else {
561
+ this.#mode = "action";
562
+ }
563
+ return;
564
+ }
565
+ this.#submit();
566
+ }
567
+ }
568
+ invalidate() {
569
+ // No cached state.
570
+ }
571
+ render(width) {
572
+ const maxWidth = Math.max(24, width - 2);
573
+ const lines = [];
574
+ lines.push(this.#theme.fg("accent", short("Programmable UI", maxWidth)));
575
+ lines.push(this.#theme.fg("dim", short("↑/↓ move · tab switch · enter select/submit · esc cancel", maxWidth)));
576
+ lines.push("");
577
+ lines.push(this.#theme.fg(this.#mode === "doc" ? "accent" : "dim", short(`Documents (${this.#entries.length})`, maxWidth)));
578
+ for (let idx = 0; idx < this.#entries.length; idx += 1) {
579
+ const entry = this.#entries[idx];
580
+ const active = idx === this.#docIndex;
581
+ const marker = active ? (this.#mode === "doc" ? "▶" : "▸") : " ";
582
+ const label = `${marker} ${entry.doc.ui_id} · ${entry.doc.title}`;
583
+ lines.push(this.#theme.fg(active ? "accent" : "muted", short(label, maxWidth)));
584
+ }
585
+ const selectedDoc = this.#currentEntry().doc;
586
+ if (selectedDoc.summary) {
587
+ lines.push("");
588
+ lines.push(this.#theme.fg("dim", short(`Summary: ${selectedDoc.summary}`, maxWidth)));
589
+ }
590
+ lines.push("");
591
+ lines.push(this.#theme.fg("dim", short(`Components (${selectedDoc.components.length})`, maxWidth)));
592
+ const visibleComponents = selectedDoc.components.slice(0, UI_PICKER_COMPONENTS_MAX);
593
+ for (const component of visibleComponents) {
594
+ const componentLines = pickerComponentLines(component);
595
+ for (let idx = 0; idx < componentLines.length; idx += 1) {
596
+ const line = componentLines[idx];
597
+ const prefix = idx === 0 ? " " : " ";
598
+ lines.push(this.#theme.fg("text", short(`${prefix}${line}`, maxWidth)));
599
+ }
600
+ }
601
+ if (selectedDoc.components.length > visibleComponents.length) {
602
+ lines.push(this.#theme.fg("muted", short(` ... (+${selectedDoc.components.length - visibleComponents.length} more components)`, maxWidth)));
603
+ }
604
+ const actions = this.#currentActions();
605
+ lines.push("");
606
+ lines.push(this.#theme.fg(this.#mode === "action" ? "accent" : "dim", short(`Actions (${actions.length})`, maxWidth)));
607
+ for (let idx = 0; idx < actions.length; idx += 1) {
608
+ const action = actions[idx];
609
+ const active = idx === this.#actionIndex;
610
+ const marker = active ? (this.#mode === "action" ? "▶" : "▸") : " ";
611
+ const label = `${marker} ${action.id} · ${action.label}`;
612
+ lines.push(this.#theme.fg(active ? "accent" : "text", short(label, maxWidth)));
613
+ }
614
+ const action = this.#currentAction();
615
+ if (action?.description) {
616
+ lines.push("");
617
+ lines.push(this.#theme.fg("dim", short(`Ask: ${action.description}`, maxWidth)));
618
+ }
619
+ if (action?.component_id) {
620
+ lines.push(this.#theme.fg("dim", short(`Targets component: ${action.component_id}`, maxWidth)));
621
+ }
622
+ const commandText = action ? actionCommandText(action) : null;
623
+ if (commandText) {
624
+ lines.push("");
625
+ lines.push(this.#theme.fg("dim", short(`Prompt template: ${commandText}`, maxWidth)));
626
+ }
627
+ return lines;
628
+ }
629
+ }
630
+ async function pickUiActionInteractively(opts) {
631
+ const selected = await opts.ctx.ui.custom((_tui, theme, _keybindings, done) => new UiActionPickerComponent({
632
+ entries: opts.entries,
633
+ theme: theme,
634
+ done,
635
+ initialUiId: opts.uiId,
636
+ initialActionId: opts.actionId,
637
+ }), {
638
+ overlay: true,
639
+ overlayOptions: {
640
+ anchor: "center",
641
+ width: "78%",
642
+ maxHeight: "70%",
643
+ margin: 1,
644
+ },
645
+ });
646
+ return selected ?? null;
647
+ }
648
+ function applyUiAction(params, state) {
649
+ retainPromptedRevisionKeysForActiveDocs(state);
650
+ retainAwaitingUiIdsForActiveDocs(state);
651
+ const docs = activeDocs(state);
652
+ const awaitingCount = awaitingDocs(state, docs).length;
653
+ switch (params.action) {
654
+ case "status":
655
+ return {
656
+ ok: true,
657
+ action: "status",
658
+ message: statusSummary(docs, awaitingCount),
659
+ extra: {
660
+ status_profile_count: statusProfileDocCount(docs),
661
+ ...statusProfileWarningsExtraForDocs(docs),
662
+ },
663
+ };
664
+ case "snapshot": {
665
+ const format = parseSnapshotFormat(params.snapshot_format);
666
+ return {
667
+ ok: true,
668
+ action: "snapshot",
669
+ message: snapshotText(docs, format),
670
+ extra: {
671
+ snapshot_format: format,
672
+ status_profile_count: statusProfileDocCount(docs),
673
+ ...statusProfileWarningsExtraForDocs(docs),
674
+ },
675
+ };
676
+ }
677
+ case "set":
678
+ case "update": {
679
+ const parsed = parseDocInput(params.doc);
680
+ if (!parsed.ok) {
681
+ return { ok: false, action: params.action, message: parsed.error };
682
+ }
683
+ const preferred = preferredDocForState(state, parsed.doc);
684
+ state.docsById.set(parsed.doc.ui_id, preferred);
685
+ if (runnableActions(preferred).length > 0) {
686
+ state.awaitingUiIds.add(parsed.doc.ui_id);
687
+ }
688
+ else {
689
+ state.awaitingUiIds.delete(parsed.doc.ui_id);
690
+ }
691
+ retainPromptedRevisionKeysForActiveDocs(state);
692
+ retainAwaitingUiIdsForActiveDocs(state);
693
+ return {
694
+ ok: true,
695
+ action: params.action,
696
+ message: `UI doc set: ${parsed.doc.ui_id}`,
697
+ extra: { ui_id: parsed.doc.ui_id, ...statusProfileWarningsExtraForDoc(preferred) },
698
+ changedUiIds: [parsed.doc.ui_id],
699
+ };
700
+ }
701
+ case "replace": {
702
+ const parsed = parseDocListInput(params.docs);
703
+ if (!parsed.ok) {
704
+ return { ok: false, action: "replace", message: parsed.error };
705
+ }
706
+ state.docsById.clear();
707
+ state.awaitingUiIds.clear();
708
+ for (const doc of parsed.docs) {
709
+ state.docsById.set(doc.ui_id, doc);
710
+ if (runnableActions(doc).length > 0) {
711
+ state.awaitingUiIds.add(doc.ui_id);
712
+ }
713
+ }
714
+ retainPromptedRevisionKeysForActiveDocs(state);
715
+ retainAwaitingUiIdsForActiveDocs(state);
716
+ return {
717
+ ok: true,
718
+ action: "replace",
719
+ message: `UI docs replaced (${parsed.docs.length}).`,
720
+ extra: {
721
+ doc_count: parsed.docs.length,
722
+ ...statusProfileWarningsExtraForDocs(parsed.docs),
723
+ },
724
+ changedUiIds: parsed.docs.map((doc) => doc.ui_id),
725
+ };
726
+ }
727
+ case "remove": {
728
+ const uiId = (params.ui_id ?? "").trim();
729
+ if (!uiId) {
730
+ return { ok: false, action: "remove", message: "Missing ui_id." };
731
+ }
732
+ if (!state.docsById.delete(uiId)) {
733
+ return { ok: false, action: "remove", message: `UI doc not found: ${uiId}` };
734
+ }
735
+ if (state.pendingPrompt?.uiId === uiId) {
736
+ state.pendingPrompt = null;
737
+ }
738
+ state.awaitingUiIds.delete(uiId);
739
+ retainPromptedRevisionKeysForActiveDocs(state);
740
+ retainAwaitingUiIdsForActiveDocs(state);
741
+ return { ok: true, action: "remove", message: `UI doc removed: ${uiId}` };
742
+ }
743
+ case "clear":
744
+ state.docsById.clear();
745
+ state.pendingPrompt = null;
746
+ state.promptedRevisionKeys.clear();
747
+ state.awaitingUiIds.clear();
748
+ return { ok: true, action: "clear", message: "UI docs cleared." };
749
+ }
750
+ }
751
+ function buildToolResult(opts) {
752
+ const docs = activeDocs(opts.state);
753
+ const result = {
754
+ content: [{ type: "text", text: opts.message }],
755
+ ui_docs: docs,
756
+ details: {
757
+ ok: opts.ok,
758
+ action: opts.action,
759
+ doc_count: docs.length,
760
+ ui_ids: docs.map((doc) => doc.ui_id),
761
+ ...(opts.extra ?? {}),
762
+ },
763
+ };
764
+ return result;
765
+ }
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
+ function refreshUi(ctx) {
822
+ const key = sessionKey(ctx);
823
+ const state = ensureState(key);
824
+ if (!ctx.hasUI) {
825
+ return;
826
+ }
827
+ retainAwaitingUiIdsForActiveDocs(state);
828
+ const docs = activeDocs(state);
829
+ if (docs.length === 0) {
830
+ ctx.ui.setStatus("mu-ui", undefined);
831
+ ctx.ui.setWidget("mu-ui", undefined);
832
+ return;
833
+ }
834
+ const awaiting = awaitingDocs(state, docs);
835
+ const labels = docs.map((doc) => doc.ui_id).join(", ");
836
+ ctx.ui.setStatus("mu-ui", [
837
+ ctx.ui.theme.fg("dim", "ui"),
838
+ ctx.ui.theme.fg("muted", "·"),
839
+ ctx.ui.theme.fg("accent", `${docs.length}`),
840
+ 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"),
844
+ ctx.ui.theme.fg("muted", "·"),
845
+ ctx.ui.theme.fg("text", labels),
846
+ ].join(" "));
847
+ const primaryDoc = awaiting[0] ?? docs[0];
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" });
852
+ }
853
+ export function uiExtension(pi) {
854
+ const commandUsage = "/mu ui status|snapshot [compact|multiline]|interact [ui_id [action_id]]";
855
+ const usage = `Usage: ${commandUsage}`;
856
+ const runUiActionFromDoc = async (ctx, state, uiId, actionId) => {
857
+ const docs = activeDocs(state, UI_DISPLAY_DOCS_MAX);
858
+ if (docs.length === 0) {
859
+ ctx.ui.notify("No UI docs are currently available.", "info");
860
+ return;
861
+ }
862
+ const entries = docs
863
+ .map((doc) => ({ doc, actions: runnableActions(doc) }))
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
+ }
869
+ let selectedDoc = null;
870
+ let selectedAction = null;
871
+ const normalizedUiId = uiId?.trim() ?? "";
872
+ const normalizedActionId = actionId?.trim() ?? "";
873
+ if (normalizedUiId.length > 0 && normalizedActionId.length > 0) {
874
+ const entry = entries.find((candidate) => candidate.doc.ui_id === normalizedUiId) ?? null;
875
+ if (!entry) {
876
+ ctx.ui.notify(`UI doc not found: ${normalizedUiId}`, "error");
877
+ return;
878
+ }
879
+ const action = entry.actions.find((candidate) => candidate.id === normalizedActionId) ?? null;
880
+ if (!action) {
881
+ ctx.ui.notify(`Action not found: ${normalizedActionId} in ${normalizedUiId}`, "error");
882
+ return;
883
+ }
884
+ selectedDoc = entry.doc;
885
+ selectedAction = action;
886
+ }
887
+ else {
888
+ const picked = await pickUiActionInteractively({
889
+ ctx,
890
+ entries,
891
+ uiId: normalizedUiId.length > 0 ? normalizedUiId : undefined,
892
+ actionId: normalizedActionId.length > 0 ? normalizedActionId : undefined,
893
+ });
894
+ if (!picked) {
895
+ ctx.ui.notify("UI interaction cancelled.", "info");
896
+ return;
897
+ }
898
+ selectedDoc = picked.doc;
899
+ selectedAction = picked.action;
900
+ }
901
+ if (!selectedDoc || !selectedAction) {
902
+ ctx.ui.notify("No UI action was selected.", "error");
903
+ return;
904
+ }
905
+ const commandText = actionCommandText(selectedAction);
906
+ if (!commandText) {
907
+ ctx.ui.notify(`Action ${selectedAction.id} is missing metadata.command_text.`, "error");
908
+ return;
909
+ }
910
+ const templateKeys = extractTemplateKeys(commandText);
911
+ const templateValues = await collectTemplateValues({
912
+ ctx,
913
+ action: selectedAction,
914
+ templateKeys,
915
+ });
916
+ if (!templateValues) {
917
+ ctx.ui.notify("UI interaction cancelled.", "info");
918
+ return;
919
+ }
920
+ const composed = composePromptFromAction({
921
+ commandText,
922
+ templateValues,
923
+ });
924
+ const edited = await ctx.ui.editor(`Review prompt (${selectedDoc.ui_id}/${selectedAction.id})`, composed);
925
+ if (edited === undefined) {
926
+ ctx.ui.notify("UI submit cancelled.", "info");
927
+ return;
928
+ }
929
+ const finalPrompt = edited.trim();
930
+ if (finalPrompt.length === 0) {
931
+ ctx.ui.notify("Cannot submit an empty prompt.", "error");
932
+ return;
933
+ }
934
+ const confirmed = await ctx.ui.confirm("Submit UI prompt", short(finalPrompt, UI_PROMPT_PREVIEW_MAX));
935
+ if (!confirmed) {
936
+ ctx.ui.notify("UI submit cancelled.", "info");
937
+ return;
938
+ }
939
+ pi.sendUserMessage(finalPrompt);
940
+ state.awaitingUiIds.delete(selectedDoc.ui_id);
941
+ if (state.pendingPrompt?.uiId === selectedDoc.ui_id) {
942
+ state.pendingPrompt = null;
943
+ }
944
+ retainAwaitingUiIdsForActiveDocs(state);
945
+ ctx.ui.notify(`Submitted prompt from ${selectedDoc.ui_id}/${selectedAction.id}.`, "info");
946
+ };
947
+ registerMuSubcommand(pi, {
948
+ subcommand: "ui",
949
+ summary: "Inspect and manage interactive UI docs",
950
+ usage: commandUsage,
951
+ handler: async (args, ctx) => {
952
+ const tokens = args
953
+ .trim()
954
+ .split(/\s+/)
955
+ .filter((token) => token.length > 0);
956
+ const subcommand = tokens[0] ?? "status";
957
+ const key = sessionKey(ctx);
958
+ const state = ensureState(key);
959
+ if (subcommand === "status" || subcommand === "snapshot") {
960
+ const snapshotFormat = subcommand === "snapshot" ? tokens[1] : undefined;
961
+ const actionParams = {
962
+ action: subcommand,
963
+ snapshot_format: snapshotFormat,
964
+ };
965
+ const result = applyUiAction(actionParams, state);
966
+ refreshUi(ctx);
967
+ ctx.ui.notify(result.message, result.ok ? "info" : "error");
968
+ return;
969
+ }
970
+ if (subcommand === "interact") {
971
+ if (tokens.length > 3) {
972
+ ctx.ui.notify(usage, "info");
973
+ return;
974
+ }
975
+ const uiId = tokens[1];
976
+ const actionId = tokens[2];
977
+ await runUiActionFromDoc(ctx, state, uiId, actionId);
978
+ refreshUi(ctx);
979
+ return;
980
+ }
981
+ ctx.ui.notify(usage, "info");
982
+ },
983
+ });
984
+ pi.registerShortcut(UI_INTERACT_SHORTCUT, {
985
+ description: "Interact with programmable UI docs and submit prompt",
986
+ handler: async (ctx) => {
987
+ const key = sessionKey(ctx);
988
+ const state = ensureState(key);
989
+ await runUiActionFromDoc(ctx, state);
990
+ refreshUi(ctx);
991
+ },
992
+ });
993
+ pi.registerTool({
994
+ name: "mu_ui",
995
+ label: "mu UI",
996
+ description: "Publish, inspect, and manage interactive UI documents.",
997
+ parameters: {
998
+ type: "object",
999
+ additionalProperties: false,
1000
+ properties: {
1001
+ action: {
1002
+ type: "string",
1003
+ enum: ["status", "snapshot", "set", "update", "replace", "remove", "clear"],
1004
+ },
1005
+ doc: { type: "object", additionalProperties: true },
1006
+ docs: { type: "array", items: { type: "object", additionalProperties: true } },
1007
+ ui_id: { type: "string" },
1008
+ snapshot_format: { type: "string", enum: ["compact", "multiline"] },
1009
+ },
1010
+ required: ["action"],
1011
+ },
1012
+ execute: async (_toolCallId, paramsRaw, _signal, _onUpdate, ctx) => {
1013
+ const key = sessionKey(ctx);
1014
+ const state = ensureState(key);
1015
+ const params = paramsRaw;
1016
+ const result = applyUiAction(params, state);
1017
+ if (ctx.hasUI && result.ok && result.changedUiIds && result.changedUiIds.length > 0) {
1018
+ armAutoPromptForUiDocs(state, result.changedUiIds);
1019
+ }
1020
+ refreshUi(ctx);
1021
+ return buildToolResult({ state, ...result });
1022
+ },
1023
+ });
1024
+ pi.on("session_start", (_event, ctx) => {
1025
+ refreshUi(ctx);
1026
+ });
1027
+ pi.on("session_switch", (_event, ctx) => {
1028
+ refreshUi(ctx);
1029
+ });
1030
+ pi.on("agent_end", async (_event, ctx) => {
1031
+ if (!ctx.hasUI) {
1032
+ return;
1033
+ }
1034
+ const key = sessionKey(ctx);
1035
+ const state = ensureState(key);
1036
+ const pending = state.pendingPrompt;
1037
+ if (!pending) {
1038
+ return;
1039
+ }
1040
+ state.pendingPrompt = null;
1041
+ ctx.ui.notify(`Agent requested input via ${pending.uiId}. Submit now or press ${UI_INTERACT_SHORTCUT} later.`, "info");
1042
+ await runUiActionFromDoc(ctx, state, pending.uiId, pending.actionId);
1043
+ refreshUi(ctx);
1044
+ });
1045
+ pi.on("session_shutdown", (_event, ctx) => {
1046
+ const key = sessionKey(ctx);
1047
+ touchState(key);
1048
+ if (!ctx.hasUI) {
1049
+ return;
1050
+ }
1051
+ ctx.ui.setStatus("mu-ui", undefined);
1052
+ ctx.ui.setWidget("mu-ui", undefined);
1053
+ });
1054
+ }
1055
+ export default uiExtension;