@femtomc/mu-agent 26.2.109 → 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.
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;AAs/BpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA6N3C;AAED,eAAe,WAAW,CAAC"}
@@ -2,9 +2,6 @@ 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
- 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;
@@ -24,7 +21,7 @@ const UI_STATE_TTL_MS = 30 * 60 * 1000; // keep session state for 30 minutes aft
24
21
  function createState() {
25
22
  return {
26
23
  docsById: new Map(),
27
- pendingPrompt: null,
24
+ pendingPrompts: [],
28
25
  promptedRevisionKeys: new Set(),
29
26
  awaitingUiIds: new Set(),
30
27
  interactionDepth: 0,
@@ -88,6 +85,38 @@ function retainAwaitingUiIdsForActiveDocs(state) {
88
85
  }
89
86
  }
90
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
+ }
91
120
  function armAutoPromptForUiDocs(state, changedUiIds) {
92
121
  if (changedUiIds.length === 0) {
93
122
  return;
@@ -99,26 +128,30 @@ function armAutoPromptForUiDocs(state, changedUiIds) {
99
128
  changedDocs.push(doc);
100
129
  }
101
130
  }
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) {
131
+ const unpromptedDocs = changedDocs.filter((doc) => !state.promptedRevisionKeys.has(docRevisionKey(doc)));
132
+ if (unpromptedDocs.length === 0) {
109
133
  return;
110
134
  }
111
- candidates.sort((left, right) => {
135
+ const byMostRecentRevision = (left, right) => {
112
136
  if (left.updated_at_ms !== right.updated_at_ms) {
113
137
  return right.updated_at_ms - left.updated_at_ms;
114
138
  }
115
139
  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));
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
+ }
122
155
  }
123
156
  function preferredDocForState(state, candidate) {
124
157
  const existing = state.docsById.get(candidate.ui_id);
@@ -457,9 +490,11 @@ function boundedIndex(index, length) {
457
490
  function pickerComponentLines(component) {
458
491
  switch (component.kind) {
459
492
  case "text":
460
- return [`text · ${component.text}`];
493
+ return [component.text];
461
494
  case "list": {
462
- const lines = [`list${component.title ? ` · ${component.title}` : ""}`];
495
+ const title = component.title?.trim();
496
+ const prefix = title && title.length > 0 ? `${title} · ` : "";
497
+ const lines = [`${prefix}${component.items.length} item(s)`];
463
498
  const visible = component.items.slice(0, UI_PICKER_LIST_ITEMS_MAX);
464
499
  for (const item of visible) {
465
500
  const detail = item.detail ? ` — ${item.detail}` : "";
@@ -471,7 +506,9 @@ function pickerComponentLines(component) {
471
506
  return lines;
472
507
  }
473
508
  case "key_value": {
474
- const lines = [`key_value${component.title ? ` · ${component.title}` : ""}`];
509
+ const title = component.title?.trim();
510
+ const prefix = title && title.length > 0 ? `${title} · ` : "";
511
+ const lines = [`${prefix}${component.rows.length} row(s)`];
475
512
  const visible = component.rows.slice(0, UI_PICKER_KEYVALUE_ROWS_MAX);
476
513
  for (const row of visible) {
477
514
  lines.push(`${row.key}: ${row.value}`);
@@ -482,7 +519,7 @@ function pickerComponentLines(component) {
482
519
  return lines;
483
520
  }
484
521
  case "divider":
485
- return ["divider"];
522
+ return [""];
486
523
  default:
487
524
  return ["component"];
488
525
  }
@@ -670,6 +707,7 @@ async function pickUiActionInteractively(opts) {
670
707
  function applyUiAction(params, state) {
671
708
  retainPromptedRevisionKeysForActiveDocs(state);
672
709
  retainAwaitingUiIdsForActiveDocs(state);
710
+ retainPendingPromptsForActiveDocs(state);
673
711
  const docs = activeDocs(state);
674
712
  const awaitingCount = awaitingDocs(state, docs).length;
675
713
  switch (params.action) {
@@ -712,6 +750,7 @@ function applyUiAction(params, state) {
712
750
  }
713
751
  retainPromptedRevisionKeysForActiveDocs(state);
714
752
  retainAwaitingUiIdsForActiveDocs(state);
753
+ retainPendingPromptsForActiveDocs(state);
715
754
  return {
716
755
  ok: true,
717
756
  action: params.action,
@@ -735,6 +774,7 @@ function applyUiAction(params, state) {
735
774
  }
736
775
  retainPromptedRevisionKeysForActiveDocs(state);
737
776
  retainAwaitingUiIdsForActiveDocs(state);
777
+ retainPendingPromptsForActiveDocs(state);
738
778
  return {
739
779
  ok: true,
740
780
  action: "replace",
@@ -754,17 +794,16 @@ function applyUiAction(params, state) {
754
794
  if (!state.docsById.delete(uiId)) {
755
795
  return { ok: false, action: "remove", message: `UI doc not found: ${uiId}` };
756
796
  }
757
- if (state.pendingPrompt?.uiId === uiId) {
758
- state.pendingPrompt = null;
759
- }
797
+ removePendingPromptsForUiId(state, uiId);
760
798
  state.awaitingUiIds.delete(uiId);
761
799
  retainPromptedRevisionKeysForActiveDocs(state);
762
800
  retainAwaitingUiIdsForActiveDocs(state);
801
+ retainPendingPromptsForActiveDocs(state);
763
802
  return { ok: true, action: "remove", message: `UI doc removed: ${uiId}` };
764
803
  }
765
804
  case "clear":
766
805
  state.docsById.clear();
767
- state.pendingPrompt = null;
806
+ state.pendingPrompts = [];
768
807
  state.promptedRevisionKeys.clear();
769
808
  state.awaitingUiIds.clear();
770
809
  return { ok: true, action: "clear", message: "UI docs cleared." };
@@ -785,84 +824,6 @@ function buildToolResult(opts) {
785
824
  };
786
825
  return result;
787
826
  }
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
827
  function refreshUi(ctx) {
867
828
  const key = sessionKey(ctx);
868
829
  const state = ensureState(key);
@@ -892,15 +853,7 @@ function refreshUi(ctx) {
892
853
  ctx.ui.theme.fg("muted", "·"),
893
854
  ctx.ui.theme.fg("text", labels),
894
855
  ].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" });
856
+ ctx.ui.setWidget("mu-ui", undefined);
904
857
  }
905
858
  export function uiExtension(pi) {
906
859
  const commandUsage = "/mu ui status|snapshot [compact|multiline]|interact [ui_id [action_id]]";
@@ -911,13 +864,8 @@ export function uiExtension(pi) {
911
864
  ctx.ui.notify("No UI docs are currently available.", "info");
912
865
  return;
913
866
  }
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
- }
867
+ const entries = docs.map((doc) => ({ doc, actions: runnableActions(doc) }));
868
+ const runnableEntries = entries.filter((entry) => entry.actions.length > 0);
921
869
  let selectedDoc = null;
922
870
  let selectedAction = null;
923
871
  const normalizedUiId = uiId?.trim() ?? "";
@@ -944,14 +892,19 @@ export function uiExtension(pi) {
944
892
  actionId: normalizedActionId.length > 0 ? normalizedActionId : undefined,
945
893
  });
946
894
  if (!picked) {
947
- ctx.ui.notify("UI interaction cancelled.", "info");
895
+ if (runnableEntries.length > 0) {
896
+ ctx.ui.notify("UI interaction cancelled.", "info");
897
+ }
948
898
  return;
949
899
  }
950
900
  selectedDoc = picked.doc;
951
901
  selectedAction = picked.action;
952
902
  }
953
903
  if (!selectedDoc || !selectedAction) {
954
- ctx.ui.notify("No UI action was selected.", "error");
904
+ if (runnableEntries.length === 0) {
905
+ return;
906
+ }
907
+ ctx.ui.notify("No UI action was selected.", "info");
955
908
  return;
956
909
  }
957
910
  const commandText = actionCommandText(selectedAction);
@@ -990,10 +943,9 @@ export function uiExtension(pi) {
990
943
  }
991
944
  pi.sendUserMessage(finalPrompt);
992
945
  state.awaitingUiIds.delete(selectedDoc.ui_id);
993
- if (state.pendingPrompt?.uiId === selectedDoc.ui_id) {
994
- state.pendingPrompt = null;
995
- }
946
+ removePendingPromptsForUiId(state, selectedDoc.ui_id);
996
947
  retainAwaitingUiIdsForActiveDocs(state);
948
+ retainPendingPromptsForActiveDocs(state);
997
949
  ctx.ui.notify(`Submitted prompt from ${selectedDoc.ui_id}/${selectedAction.id}.`, "info");
998
950
  });
999
951
  registerMuSubcommand(pi, {
@@ -1034,7 +986,7 @@ export function uiExtension(pi) {
1034
986
  },
1035
987
  });
1036
988
  pi.registerShortcut(UI_INTERACT_SHORTCUT, {
1037
- description: "Interact with programmable UI docs and submit prompt",
989
+ description: "Open programmable UI modal and optionally submit prompt",
1038
990
  handler: async (ctx) => {
1039
991
  const key = sessionKey(ctx);
1040
992
  const state = ensureState(key);
@@ -1085,12 +1037,14 @@ export function uiExtension(pi) {
1085
1037
  }
1086
1038
  const key = sessionKey(ctx);
1087
1039
  const state = ensureState(key);
1088
- const pending = state.pendingPrompt;
1040
+ retainPendingPromptsForActiveDocs(state);
1041
+ const pending = state.pendingPrompts.shift();
1089
1042
  if (!pending) {
1090
1043
  return;
1091
1044
  }
1092
- state.pendingPrompt = null;
1093
- ctx.ui.notify(`Agent requested input via ${pending.uiId}. Submit now or press ${UI_INTERACT_SHORTCUT} later.`, "info");
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
+ }
1094
1048
  await runUiActionFromDoc(ctx, state, pending.uiId, pending.actionId);
1095
1049
  refreshUi(ctx);
1096
1050
  });
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.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.109",
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",