@femtomc/mu-agent 26.2.109 → 26.2.111

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.
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;AAsiCpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA4N3C;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;AAsqCpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA6N3C;AAED,eAAe,WAAW,CAAC"}
@@ -1,16 +1,20 @@
1
1
  import { normalizeUiDocs, parseUiDoc, resolveUiStatusProfileName, uiStatusProfileWarnings, } from "@femtomc/mu-core";
2
- import { matchesKey } from "@mariozechner/pi-tui";
2
+ import { matchesKey, truncateToWidth, visibleWidth } 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
- const UI_WIDGET_COMPONENT_DETAILS_MAX = 3;
8
5
  const UI_PICKER_COMPONENTS_MAX = 8;
9
6
  const UI_PICKER_LIST_ITEMS_MAX = 4;
10
7
  const UI_PICKER_KEYVALUE_ROWS_MAX = 4;
11
8
  const UI_SESSION_KEY_FALLBACK = "__mu_ui_active_session__";
12
9
  const UI_PROMPT_PREVIEW_MAX = 160;
13
10
  const UI_INTERACT_SHORTCUT = "ctrl+shift+u";
11
+ const UI_PICKER_PANEL_MIN_WIDTH = 56;
12
+ const UI_PICKER_PANEL_MAX_WIDTH = 118;
13
+ const UI_PICKER_PANEL_WIDTH_RATIO = 0.9;
14
+ const UI_PICKER_PANEL_TOP_MARGIN = 1;
15
+ const UI_PICKER_PANEL_BOTTOM_MARGIN = 1;
16
+ const UI_ENABLE_MOUSE_TRACKING = "\x1b[?1000h\x1b[?1006h";
17
+ const UI_DISABLE_MOUSE_TRACKING = "\x1b[?1000l\x1b[?1006l";
14
18
  const UI_INTERACT_OVERLAY_OPTIONS = {
15
19
  anchor: "top-left",
16
20
  row: 0,
@@ -24,7 +28,7 @@ const UI_STATE_TTL_MS = 30 * 60 * 1000; // keep session state for 30 minutes aft
24
28
  function createState() {
25
29
  return {
26
30
  docsById: new Map(),
27
- pendingPrompt: null,
31
+ pendingPrompts: [],
28
32
  promptedRevisionKeys: new Set(),
29
33
  awaitingUiIds: new Set(),
30
34
  interactionDepth: 0,
@@ -88,6 +92,38 @@ function retainAwaitingUiIdsForActiveDocs(state) {
88
92
  }
89
93
  }
90
94
  }
95
+ function retainPendingPromptsForActiveDocs(state) {
96
+ state.pendingPrompts = state.pendingPrompts.filter((pending) => {
97
+ const doc = state.docsById.get(pending.uiId);
98
+ if (!doc) {
99
+ return false;
100
+ }
101
+ if (pending.kind === "review") {
102
+ return isStatusProfileStatusVariant(doc);
103
+ }
104
+ const actions = runnableActions(doc);
105
+ if (actions.length === 0) {
106
+ return false;
107
+ }
108
+ if (!pending.actionId) {
109
+ return true;
110
+ }
111
+ return actions.some((action) => action.id === pending.actionId);
112
+ });
113
+ }
114
+ function removePendingPromptsForUiId(state, uiId) {
115
+ state.pendingPrompts = state.pendingPrompts.filter((pending) => pending.uiId !== uiId);
116
+ }
117
+ function enqueuePendingPrompt(state, pending) {
118
+ const duplicate = state.pendingPrompts.some((existing) => {
119
+ return (existing.kind === pending.kind &&
120
+ existing.uiId === pending.uiId &&
121
+ existing.actionId === pending.actionId);
122
+ });
123
+ if (!duplicate) {
124
+ state.pendingPrompts.push(pending);
125
+ }
126
+ }
91
127
  function armAutoPromptForUiDocs(state, changedUiIds) {
92
128
  if (changedUiIds.length === 0) {
93
129
  return;
@@ -99,26 +135,30 @@ function armAutoPromptForUiDocs(state, changedUiIds) {
99
135
  changedDocs.push(doc);
100
136
  }
101
137
  }
102
- const candidates = changedDocs.filter((doc) => {
103
- if (runnableActions(doc).length === 0) {
104
- return false;
105
- }
106
- return !state.promptedRevisionKeys.has(docRevisionKey(doc));
107
- });
108
- if (candidates.length === 0) {
138
+ const unpromptedDocs = changedDocs.filter((doc) => !state.promptedRevisionKeys.has(docRevisionKey(doc)));
139
+ if (unpromptedDocs.length === 0) {
109
140
  return;
110
141
  }
111
- candidates.sort((left, right) => {
142
+ const byMostRecentRevision = (left, right) => {
112
143
  if (left.updated_at_ms !== right.updated_at_ms) {
113
144
  return right.updated_at_ms - left.updated_at_ms;
114
145
  }
115
146
  return left.ui_id.localeCompare(right.ui_id);
116
- });
117
- const doc = candidates[0];
118
- const actions = runnableActions(doc);
119
- const actionId = actions.length === 1 ? actions[0].id : undefined;
120
- state.pendingPrompt = { uiId: doc.ui_id, actionId };
121
- state.promptedRevisionKeys.add(docRevisionKey(doc));
147
+ };
148
+ const runnableCandidates = unpromptedDocs.filter((doc) => runnableActions(doc).length > 0).sort(byMostRecentRevision);
149
+ if (runnableCandidates.length > 0) {
150
+ const doc = runnableCandidates[0];
151
+ const actions = runnableActions(doc);
152
+ const actionId = actions.length === 1 ? actions[0].id : undefined;
153
+ enqueuePendingPrompt(state, { kind: "action", uiId: doc.ui_id, actionId });
154
+ state.promptedRevisionKeys.add(docRevisionKey(doc));
155
+ }
156
+ const statusCandidates = unpromptedDocs.filter((doc) => isStatusProfileStatusVariant(doc)).sort(byMostRecentRevision);
157
+ if (statusCandidates.length > 0) {
158
+ const doc = statusCandidates[0];
159
+ enqueuePendingPrompt(state, { kind: "review", uiId: doc.ui_id });
160
+ state.promptedRevisionKeys.add(docRevisionKey(doc));
161
+ }
122
162
  }
123
163
  function preferredDocForState(state, candidate) {
124
164
  const existing = state.docsById.get(candidate.ui_id);
@@ -454,12 +494,42 @@ function boundedIndex(index, length) {
454
494
  }
455
495
  return index;
456
496
  }
497
+ function parseSgrMouseEvent(data) {
498
+ const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
499
+ if (!match) {
500
+ return null;
501
+ }
502
+ const buttonCode = Number.parseInt(match[1] ?? "", 10);
503
+ const col = Number.parseInt(match[2] ?? "", 10);
504
+ const row = Number.parseInt(match[3] ?? "", 10);
505
+ if (!Number.isInteger(buttonCode) || !Number.isInteger(col) || !Number.isInteger(row)) {
506
+ return null;
507
+ }
508
+ return {
509
+ buttonCode,
510
+ col,
511
+ row,
512
+ release: match[4] === "m",
513
+ };
514
+ }
515
+ function isPrimaryMouseRelease(event) {
516
+ if (!event.release) {
517
+ return false;
518
+ }
519
+ if (event.buttonCode >= 64) {
520
+ return false;
521
+ }
522
+ const primaryButton = event.buttonCode & 0b11;
523
+ return primaryButton === 0;
524
+ }
457
525
  function pickerComponentLines(component) {
458
526
  switch (component.kind) {
459
527
  case "text":
460
- return [`text · ${component.text}`];
528
+ return [component.text];
461
529
  case "list": {
462
- const lines = [`list${component.title ? ` · ${component.title}` : ""}`];
530
+ const title = component.title?.trim();
531
+ const prefix = title && title.length > 0 ? `${title} · ` : "";
532
+ const lines = [`${prefix}${component.items.length} item(s)`];
463
533
  const visible = component.items.slice(0, UI_PICKER_LIST_ITEMS_MAX);
464
534
  for (const item of visible) {
465
535
  const detail = item.detail ? ` — ${item.detail}` : "";
@@ -471,7 +541,9 @@ function pickerComponentLines(component) {
471
541
  return lines;
472
542
  }
473
543
  case "key_value": {
474
- const lines = [`key_value${component.title ? ` · ${component.title}` : ""}`];
544
+ const title = component.title?.trim();
545
+ const prefix = title && title.length > 0 ? `${title} · ` : "";
546
+ const lines = [`${prefix}${component.rows.length} row(s)`];
475
547
  const visible = component.rows.slice(0, UI_PICKER_KEYVALUE_ROWS_MAX);
476
548
  for (const row of visible) {
477
549
  lines.push(`${row.key}: ${row.value}`);
@@ -482,19 +554,28 @@ function pickerComponentLines(component) {
482
554
  return lines;
483
555
  }
484
556
  case "divider":
485
- return ["divider"];
557
+ return [""];
486
558
  default:
487
559
  return ["component"];
488
560
  }
489
561
  }
490
562
  class UiActionPickerComponent {
563
+ #tui;
491
564
  #entries;
492
565
  #theme;
493
566
  #done;
494
567
  #mode = "doc";
495
568
  #docIndex = 0;
496
569
  #actionIndex = 0;
570
+ #mouseEnabled = false;
571
+ #panelRowStart = 1;
572
+ #panelRowEnd = 1;
573
+ #panelColStart = 1;
574
+ #panelColEnd = 1;
575
+ #docHitRows = new Map();
576
+ #actionHitRows = new Map();
497
577
  constructor(opts) {
578
+ this.#tui = opts.tui;
498
579
  this.#entries = opts.entries;
499
580
  this.#theme = opts.theme;
500
581
  this.#done = opts.done;
@@ -512,6 +593,21 @@ class UiActionPickerComponent {
512
593
  this.#mode = "action";
513
594
  }
514
595
  }
596
+ this.#enableMouseTracking();
597
+ }
598
+ #enableMouseTracking() {
599
+ if (this.#mouseEnabled) {
600
+ return;
601
+ }
602
+ this.#tui.terminal.write(UI_ENABLE_MOUSE_TRACKING);
603
+ this.#mouseEnabled = true;
604
+ }
605
+ #disableMouseTracking() {
606
+ if (!this.#mouseEnabled) {
607
+ return;
608
+ }
609
+ this.#tui.terminal.write(UI_DISABLE_MOUSE_TRACKING);
610
+ this.#mouseEnabled = false;
515
611
  }
516
612
  #currentEntry() {
517
613
  return this.#entries[this.#docIndex];
@@ -546,7 +642,36 @@ class UiActionPickerComponent {
546
642
  action,
547
643
  });
548
644
  }
645
+ #handleMouseEvent(event) {
646
+ if (!isPrimaryMouseRelease(event)) {
647
+ return;
648
+ }
649
+ if (event.row < this.#panelRowStart ||
650
+ event.row > this.#panelRowEnd ||
651
+ event.col < this.#panelColStart ||
652
+ event.col > this.#panelColEnd) {
653
+ return;
654
+ }
655
+ const docIndex = this.#docHitRows.get(event.row);
656
+ if (docIndex !== undefined) {
657
+ this.#docIndex = boundedIndex(docIndex, this.#entries.length);
658
+ this.#actionIndex = boundedIndex(this.#actionIndex, this.#currentActions().length);
659
+ this.#mode = "doc";
660
+ return;
661
+ }
662
+ const actionIndex = this.#actionHitRows.get(event.row);
663
+ if (actionIndex !== undefined) {
664
+ this.#mode = "action";
665
+ this.#actionIndex = boundedIndex(actionIndex, this.#currentActions().length);
666
+ this.#submit();
667
+ }
668
+ }
549
669
  handleInput(data) {
670
+ const mouse = parseSgrMouseEvent(data);
671
+ if (mouse) {
672
+ this.#handleMouseEvent(mouse);
673
+ return;
674
+ }
550
675
  if (matchesKey(data, "escape")) {
551
676
  this.#done(null);
552
677
  return;
@@ -595,67 +720,120 @@ class UiActionPickerComponent {
595
720
  invalidate() {
596
721
  // No cached state.
597
722
  }
723
+ dispose() {
724
+ this.#disableMouseTracking();
725
+ }
598
726
  render(width) {
599
- const maxWidth = Math.max(24, width - 2);
600
- const lines = [];
601
- lines.push(this.#theme.fg("accent", short("Programmable UI", maxWidth)));
602
- lines.push(this.#theme.fg("dim", short("↑/↓ move · tab switch · enter select/submit · esc cancel", maxWidth)));
603
- lines.push("");
604
- lines.push(this.#theme.fg(this.#mode === "doc" ? "accent" : "dim", short(`Documents (${this.#entries.length})`, maxWidth)));
727
+ const panelTargetWidth = Math.max(UI_PICKER_PANEL_MIN_WIDTH, Math.min(UI_PICKER_PANEL_MAX_WIDTH, Math.floor(width * UI_PICKER_PANEL_WIDTH_RATIO)));
728
+ const panelWidth = Math.max(4, Math.min(width, panelTargetWidth));
729
+ const innerWidth = Math.max(1, panelWidth - 2);
730
+ const maxContentWidth = Math.max(8, innerWidth);
731
+ const content = [];
732
+ const docHitRowsByContentIndex = new Map();
733
+ const actionHitRowsByContentIndex = new Map();
734
+ const pushContent = (line) => {
735
+ content.push(truncateToWidth(line, innerWidth, "…", true));
736
+ };
737
+ pushContent(this.#theme.fg("accent", short("Programmable UI", maxContentWidth)));
738
+ pushContent(this.#theme.fg("dim", short("↑/↓ move · tab switch · enter select/submit · esc cancel · click action to submit", maxContentWidth)));
739
+ pushContent("");
740
+ pushContent(this.#theme.fg(this.#mode === "doc" ? "accent" : "dim", short(`Documents (${this.#entries.length})`, maxContentWidth)));
605
741
  for (let idx = 0; idx < this.#entries.length; idx += 1) {
606
742
  const entry = this.#entries[idx];
607
743
  const active = idx === this.#docIndex;
608
744
  const marker = active ? (this.#mode === "doc" ? "▶" : "▸") : " ";
609
745
  const label = `${marker} ${entry.doc.ui_id} · ${entry.doc.title}`;
610
- lines.push(this.#theme.fg(active ? "accent" : "muted", short(label, maxWidth)));
746
+ docHitRowsByContentIndex.set(content.length, idx);
747
+ pushContent(this.#theme.fg(active ? "accent" : "muted", short(label, maxContentWidth)));
611
748
  }
612
749
  const selectedDoc = this.#currentEntry().doc;
613
750
  if (selectedDoc.summary) {
614
- lines.push("");
615
- lines.push(this.#theme.fg("dim", short(`Summary: ${selectedDoc.summary}`, maxWidth)));
751
+ pushContent("");
752
+ pushContent(this.#theme.fg("dim", short(`Summary: ${selectedDoc.summary}`, maxContentWidth)));
616
753
  }
617
- lines.push("");
618
- lines.push(this.#theme.fg("dim", short(`Components (${selectedDoc.components.length})`, maxWidth)));
754
+ pushContent("");
755
+ pushContent(this.#theme.fg("dim", short(`Components (${selectedDoc.components.length})`, maxContentWidth)));
619
756
  const visibleComponents = selectedDoc.components.slice(0, UI_PICKER_COMPONENTS_MAX);
620
757
  for (const component of visibleComponents) {
621
758
  const componentLines = pickerComponentLines(component);
622
759
  for (let idx = 0; idx < componentLines.length; idx += 1) {
623
760
  const line = componentLines[idx];
624
761
  const prefix = idx === 0 ? " " : " ";
625
- lines.push(this.#theme.fg("text", short(`${prefix}${line}`, maxWidth)));
762
+ pushContent(this.#theme.fg("text", short(`${prefix}${line}`, maxContentWidth)));
626
763
  }
627
764
  }
628
765
  if (selectedDoc.components.length > visibleComponents.length) {
629
- lines.push(this.#theme.fg("muted", short(` ... (+${selectedDoc.components.length - visibleComponents.length} more components)`, maxWidth)));
766
+ pushContent(this.#theme.fg("muted", short(` ... (+${selectedDoc.components.length - visibleComponents.length} more components)`, maxContentWidth)));
630
767
  }
631
768
  const actions = this.#currentActions();
632
- lines.push("");
633
- lines.push(this.#theme.fg(this.#mode === "action" ? "accent" : "dim", short(`Actions (${actions.length})`, maxWidth)));
769
+ pushContent("");
770
+ pushContent(this.#theme.fg(this.#mode === "action" ? "accent" : "dim", short(`Actions (${actions.length})`, maxContentWidth)));
634
771
  for (let idx = 0; idx < actions.length; idx += 1) {
635
772
  const action = actions[idx];
636
773
  const active = idx === this.#actionIndex;
637
774
  const marker = active ? (this.#mode === "action" ? "▶" : "▸") : " ";
638
775
  const label = `${marker} ${action.id} · ${action.label}`;
639
- lines.push(this.#theme.fg(active ? "accent" : "text", short(label, maxWidth)));
776
+ actionHitRowsByContentIndex.set(content.length, idx);
777
+ pushContent(this.#theme.fg(active ? "accent" : "text", short(label, maxContentWidth)));
640
778
  }
641
779
  const action = this.#currentAction();
642
780
  if (action?.description) {
643
- lines.push("");
644
- lines.push(this.#theme.fg("dim", short(`Ask: ${action.description}`, maxWidth)));
781
+ pushContent("");
782
+ pushContent(this.#theme.fg("dim", short(`Ask: ${action.description}`, maxContentWidth)));
645
783
  }
646
784
  if (action?.component_id) {
647
- lines.push(this.#theme.fg("dim", short(`Targets component: ${action.component_id}`, maxWidth)));
785
+ pushContent(this.#theme.fg("dim", short(`Targets component: ${action.component_id}`, maxContentWidth)));
648
786
  }
649
787
  const commandText = action ? actionCommandText(action) : null;
650
788
  if (commandText) {
651
- lines.push("");
652
- lines.push(this.#theme.fg("dim", short(`Prompt template: ${commandText}`, maxWidth)));
789
+ pushContent("");
790
+ pushContent(this.#theme.fg("dim", short(`Prompt template: ${commandText}`, maxContentWidth)));
791
+ }
792
+ const topMarginRows = Math.max(0, UI_PICKER_PANEL_TOP_MARGIN);
793
+ const bottomMarginRows = Math.max(0, UI_PICKER_PANEL_BOTTOM_MARGIN);
794
+ const leftPadWidth = Math.max(0, Math.floor((width - panelWidth) / 2));
795
+ const leftPad = " ".repeat(leftPadWidth);
796
+ const frame = [];
797
+ for (let row = 0; row < topMarginRows; row += 1) {
798
+ frame.push("");
653
799
  }
654
- return lines;
800
+ const title = " mu_ui ";
801
+ const titleWidth = Math.min(innerWidth, visibleWidth(title));
802
+ const leftRule = "─".repeat(Math.max(0, Math.floor((innerWidth - titleWidth) / 2)));
803
+ const rightRule = "─".repeat(Math.max(0, innerWidth - titleWidth - leftRule.length));
804
+ frame.push(`${leftPad}${this.#theme.fg("borderAccent", `╭${leftRule}`)}${this.#theme.fg("accent", title)}${this.#theme.fg("borderAccent", `${rightRule}╮`)}`);
805
+ this.#docHitRows.clear();
806
+ this.#actionHitRows.clear();
807
+ const contentStartRow = frame.length + 1;
808
+ for (let idx = 0; idx < content.length; idx += 1) {
809
+ const line = content[idx] ?? "";
810
+ const visible = visibleWidth(line);
811
+ const fill = " ".repeat(Math.max(0, innerWidth - visible));
812
+ frame.push(`${leftPad}${this.#theme.fg("border", "│")}${this.#theme.bg("customMessageBg", `${line}${fill}`)}${this.#theme.fg("border", "│")}`);
813
+ const row = contentStartRow + idx;
814
+ const docIndex = docHitRowsByContentIndex.get(idx);
815
+ if (docIndex !== undefined) {
816
+ this.#docHitRows.set(row, docIndex);
817
+ }
818
+ const actionIndex = actionHitRowsByContentIndex.get(idx);
819
+ if (actionIndex !== undefined) {
820
+ this.#actionHitRows.set(row, actionIndex);
821
+ }
822
+ }
823
+ frame.push(`${leftPad}${this.#theme.fg("borderAccent", `╰${"─".repeat(innerWidth)}╯`)}`);
824
+ for (let row = 0; row < bottomMarginRows; row += 1) {
825
+ frame.push("");
826
+ }
827
+ this.#panelRowStart = topMarginRows + 1;
828
+ this.#panelRowEnd = this.#panelRowStart + content.length + 1;
829
+ this.#panelColStart = leftPadWidth + 1;
830
+ this.#panelColEnd = leftPadWidth + panelWidth;
831
+ return frame;
655
832
  }
656
833
  }
657
834
  async function pickUiActionInteractively(opts) {
658
- const selected = await opts.ctx.ui.custom((_tui, theme, _keybindings, done) => new UiActionPickerComponent({
835
+ const selected = await opts.ctx.ui.custom((tui, theme, _keybindings, done) => new UiActionPickerComponent({
836
+ tui,
659
837
  entries: opts.entries,
660
838
  theme: theme,
661
839
  done,
@@ -670,6 +848,7 @@ async function pickUiActionInteractively(opts) {
670
848
  function applyUiAction(params, state) {
671
849
  retainPromptedRevisionKeysForActiveDocs(state);
672
850
  retainAwaitingUiIdsForActiveDocs(state);
851
+ retainPendingPromptsForActiveDocs(state);
673
852
  const docs = activeDocs(state);
674
853
  const awaitingCount = awaitingDocs(state, docs).length;
675
854
  switch (params.action) {
@@ -712,6 +891,7 @@ function applyUiAction(params, state) {
712
891
  }
713
892
  retainPromptedRevisionKeysForActiveDocs(state);
714
893
  retainAwaitingUiIdsForActiveDocs(state);
894
+ retainPendingPromptsForActiveDocs(state);
715
895
  return {
716
896
  ok: true,
717
897
  action: params.action,
@@ -735,6 +915,7 @@ function applyUiAction(params, state) {
735
915
  }
736
916
  retainPromptedRevisionKeysForActiveDocs(state);
737
917
  retainAwaitingUiIdsForActiveDocs(state);
918
+ retainPendingPromptsForActiveDocs(state);
738
919
  return {
739
920
  ok: true,
740
921
  action: "replace",
@@ -754,17 +935,16 @@ function applyUiAction(params, state) {
754
935
  if (!state.docsById.delete(uiId)) {
755
936
  return { ok: false, action: "remove", message: `UI doc not found: ${uiId}` };
756
937
  }
757
- if (state.pendingPrompt?.uiId === uiId) {
758
- state.pendingPrompt = null;
759
- }
938
+ removePendingPromptsForUiId(state, uiId);
760
939
  state.awaitingUiIds.delete(uiId);
761
940
  retainPromptedRevisionKeysForActiveDocs(state);
762
941
  retainAwaitingUiIdsForActiveDocs(state);
942
+ retainPendingPromptsForActiveDocs(state);
763
943
  return { ok: true, action: "remove", message: `UI doc removed: ${uiId}` };
764
944
  }
765
945
  case "clear":
766
946
  state.docsById.clear();
767
- state.pendingPrompt = null;
947
+ state.pendingPrompts = [];
768
948
  state.promptedRevisionKeys.clear();
769
949
  state.awaitingUiIds.clear();
770
950
  return { ok: true, action: "clear", message: "UI docs cleared." };
@@ -785,84 +965,6 @@ function buildToolResult(opts) {
785
965
  };
786
966
  return result;
787
967
  }
788
- function renderDocPreview(theme, doc, opts) {
789
- const lines = [];
790
- const headerParts = [theme.fg("accent", doc.title), theme.fg("muted", `[${doc.ui_id}]`)];
791
- if (opts.awaitingResponse) {
792
- headerParts.push(theme.fg("accent", "awaiting-response"));
793
- }
794
- lines.push(headerParts.join(" "));
795
- if (doc.summary) {
796
- lines.push(theme.fg("muted", short(doc.summary, 80)));
797
- }
798
- if (opts.awaitingCount > 0) {
799
- lines.push(theme.fg("accent", `Awaiting user response for ${opts.awaitingCount} UI doc(s).`));
800
- }
801
- const components = doc.components.slice(0, UI_WIDGET_COMPONENTS_MAX);
802
- if (components.length > 0) {
803
- lines.push(theme.fg("dim", "Components:"));
804
- for (const component of components) {
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
- }
810
- }
811
- }
812
- const interactiveActions = runnableActions(doc);
813
- if (interactiveActions.length > 0) {
814
- lines.push(theme.fg("muted", "Actions:"));
815
- const visibleActions = interactiveActions.slice(0, UI_WIDGET_ACTIONS_MAX);
816
- for (let idx = 0; idx < visibleActions.length; idx += 1) {
817
- const action = visibleActions[idx];
818
- lines.push(` ${idx + 1}. ${action.label}`);
819
- }
820
- if (interactiveActions.length > visibleActions.length) {
821
- lines.push(` ... (+${interactiveActions.length - visibleActions.length} more actions)`);
822
- }
823
- if (opts.awaitingResponse) {
824
- lines.push(theme.fg("accent", "Awaiting your response. Select an action to continue."));
825
- }
826
- lines.push(theme.fg("dim", `Press ${UI_INTERACT_SHORTCUT} to compose and submit a prompt from actions.`));
827
- }
828
- else {
829
- lines.push(theme.fg("dim", "No interactive actions."));
830
- }
831
- return lines;
832
- }
833
- function componentPreviewLines(component) {
834
- const { kind } = component;
835
- switch (kind) {
836
- case "text":
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
- }
861
- case "divider":
862
- return ["divider"];
863
- }
864
- return [kind];
865
- }
866
968
  function refreshUi(ctx) {
867
969
  const key = sessionKey(ctx);
868
970
  const state = ensureState(key);
@@ -892,15 +994,7 @@ function refreshUi(ctx) {
892
994
  ctx.ui.theme.fg("muted", "·"),
893
995
  ctx.ui.theme.fg("text", labels),
894
996
  ].join(" "));
895
- if (state.interactionDepth > 0) {
896
- ctx.ui.setWidget("mu-ui", undefined);
897
- return;
898
- }
899
- const primaryDoc = awaiting[0] ?? docs[0];
900
- ctx.ui.setWidget("mu-ui", renderDocPreview(ctx.ui.theme, primaryDoc, {
901
- awaitingResponse: state.awaitingUiIds.has(primaryDoc.ui_id),
902
- awaitingCount: awaiting.length,
903
- }), { placement: "belowEditor" });
997
+ ctx.ui.setWidget("mu-ui", undefined);
904
998
  }
905
999
  export function uiExtension(pi) {
906
1000
  const commandUsage = "/mu ui status|snapshot [compact|multiline]|interact [ui_id [action_id]]";
@@ -911,13 +1005,8 @@ export function uiExtension(pi) {
911
1005
  ctx.ui.notify("No UI docs are currently available.", "info");
912
1006
  return;
913
1007
  }
914
- const entries = docs
915
- .map((doc) => ({ doc, actions: runnableActions(doc) }))
916
- .filter((entry) => entry.actions.length > 0);
917
- if (entries.length === 0) {
918
- ctx.ui.notify("No runnable UI actions are currently available.", "error");
919
- return;
920
- }
1008
+ const entries = docs.map((doc) => ({ doc, actions: runnableActions(doc) }));
1009
+ const runnableEntries = entries.filter((entry) => entry.actions.length > 0);
921
1010
  let selectedDoc = null;
922
1011
  let selectedAction = null;
923
1012
  const normalizedUiId = uiId?.trim() ?? "";
@@ -944,14 +1033,19 @@ export function uiExtension(pi) {
944
1033
  actionId: normalizedActionId.length > 0 ? normalizedActionId : undefined,
945
1034
  });
946
1035
  if (!picked) {
947
- ctx.ui.notify("UI interaction cancelled.", "info");
1036
+ if (runnableEntries.length > 0) {
1037
+ ctx.ui.notify("UI interaction cancelled.", "info");
1038
+ }
948
1039
  return;
949
1040
  }
950
1041
  selectedDoc = picked.doc;
951
1042
  selectedAction = picked.action;
952
1043
  }
953
1044
  if (!selectedDoc || !selectedAction) {
954
- ctx.ui.notify("No UI action was selected.", "error");
1045
+ if (runnableEntries.length === 0) {
1046
+ return;
1047
+ }
1048
+ ctx.ui.notify("No UI action was selected.", "info");
955
1049
  return;
956
1050
  }
957
1051
  const commandText = actionCommandText(selectedAction);
@@ -990,10 +1084,9 @@ export function uiExtension(pi) {
990
1084
  }
991
1085
  pi.sendUserMessage(finalPrompt);
992
1086
  state.awaitingUiIds.delete(selectedDoc.ui_id);
993
- if (state.pendingPrompt?.uiId === selectedDoc.ui_id) {
994
- state.pendingPrompt = null;
995
- }
1087
+ removePendingPromptsForUiId(state, selectedDoc.ui_id);
996
1088
  retainAwaitingUiIdsForActiveDocs(state);
1089
+ retainPendingPromptsForActiveDocs(state);
997
1090
  ctx.ui.notify(`Submitted prompt from ${selectedDoc.ui_id}/${selectedAction.id}.`, "info");
998
1091
  });
999
1092
  registerMuSubcommand(pi, {
@@ -1034,7 +1127,7 @@ export function uiExtension(pi) {
1034
1127
  },
1035
1128
  });
1036
1129
  pi.registerShortcut(UI_INTERACT_SHORTCUT, {
1037
- description: "Interact with programmable UI docs and submit prompt",
1130
+ description: "Open programmable UI modal and optionally submit prompt",
1038
1131
  handler: async (ctx) => {
1039
1132
  const key = sessionKey(ctx);
1040
1133
  const state = ensureState(key);
@@ -1085,12 +1178,14 @@ export function uiExtension(pi) {
1085
1178
  }
1086
1179
  const key = sessionKey(ctx);
1087
1180
  const state = ensureState(key);
1088
- const pending = state.pendingPrompt;
1181
+ retainPendingPromptsForActiveDocs(state);
1182
+ const pending = state.pendingPrompts.shift();
1089
1183
  if (!pending) {
1090
1184
  return;
1091
1185
  }
1092
- state.pendingPrompt = null;
1093
- ctx.ui.notify(`Agent requested input via ${pending.uiId}. Submit now or press ${UI_INTERACT_SHORTCUT} later.`, "info");
1186
+ if (pending.kind === "action") {
1187
+ ctx.ui.notify(`Agent requested input via ${pending.uiId}. Submit now or press ${UI_INTERACT_SHORTCUT} later.`, "info");
1188
+ }
1094
1189
  await runUiActionFromDoc(ctx, state, pending.uiId, pending.actionId);
1095
1190
  refreshUi(ctx);
1096
1191
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.109",
3
+ "version": "26.2.111",
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.109",
28
+ "@femtomc/mu-core": "26.2.111",
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",