@femtomc/mu-agent 26.2.117 → 26.2.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,7 +88,7 @@ Default operator UI theme is `mu-gruvbox-dark`.
88
88
  - `/mu brand on|off|toggle` — enable/disable UI branding
89
89
  - `/mu ui ...` — inspect interactive `UiDoc`s (`status`/`snapshot`)
90
90
  - `/mu help` — dispatcher catalog of registered `/mu` subcommands
91
- - `ctrl+shift+u` — reopen local programmable-UI interaction flow
91
+ - `ctrl+shift+u` (primary) or `alt+u` (fallback) — reopen local programmable-UI interaction flow
92
92
 
93
93
  ## Tooling model (CLI-first)
94
94
 
@@ -1 +1 @@
1
- {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/extensions/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAwEpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QAoOjD;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/extensions/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AA0EpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QA2PjD;AAED,eAAe,iBAAiB,CAAC"}
@@ -17,6 +17,7 @@ const EMPTY_SNAPSHOT = {
17
17
  adapters: [],
18
18
  error: null,
19
19
  };
20
+ const BRANDING_STATUS_REFRESH_TIMEOUT_MS = 2_000;
20
21
  const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
21
22
  function visibleWidth(text) {
22
23
  return text.replace(ANSI_RE, "").length;
@@ -71,6 +72,7 @@ export function brandingExtension(pi) {
71
72
  let activeCtx = null;
72
73
  let pollTimer = null;
73
74
  let footerRequestRender = null;
75
+ let refreshInFlight = null;
74
76
  function applyDefaultTheme(ctx) {
75
77
  if (!ctx.hasUI)
76
78
  return;
@@ -145,8 +147,10 @@ export function brandingExtension(pi) {
145
147
  ctx.ui.setStatus("mu-overview", undefined);
146
148
  }
147
149
  async function refreshStatus(ctx) {
148
- if (!ctx.hasUI || !enabled)
150
+ const isActiveUiContext = () => activeCtx === ctx && ctx.hasUI && enabled;
151
+ if (!isActiveUiContext()) {
149
152
  return;
153
+ }
150
154
  if (!muServerUrl()) {
151
155
  snapshot = {
152
156
  ...EMPTY_SNAPSHOT,
@@ -157,7 +161,10 @@ export function brandingExtension(pi) {
157
161
  return;
158
162
  }
159
163
  try {
160
- const status = await fetchMuStatus(4_000);
164
+ const status = await fetchMuStatus(BRANDING_STATUS_REFRESH_TIMEOUT_MS);
165
+ if (!isActiveUiContext()) {
166
+ return;
167
+ }
161
168
  const cp = status.control_plane ?? {
162
169
  active: false,
163
170
  adapters: [],
@@ -173,21 +180,35 @@ export function brandingExtension(pi) {
173
180
  ctx.ui.setStatus("mu-overview", ctx.ui.theme.fg("dim", `μ open ${snapshot.openCount} · ready ${snapshot.readyCount} · cp ${snapshot.controlPlaneActive ? "on" : "off"}`));
174
181
  }
175
182
  catch (err) {
183
+ if (!isActiveUiContext()) {
184
+ return;
185
+ }
176
186
  snapshot = {
177
187
  ...EMPTY_SNAPSHOT,
178
188
  error: err instanceof Error ? err.message : String(err),
179
189
  };
180
190
  ctx.ui.setStatus("mu-overview", ctx.ui.theme.fg("warning", "μ status refresh failed"));
181
191
  }
192
+ if (!isActiveUiContext()) {
193
+ return;
194
+ }
182
195
  footerRequestRender?.();
183
196
  }
197
+ function requestStatusRefresh(ctx) {
198
+ if (refreshInFlight) {
199
+ return;
200
+ }
201
+ refreshInFlight = refreshStatus(ctx).finally(() => {
202
+ refreshInFlight = null;
203
+ });
204
+ }
184
205
  function ensurePolling() {
185
206
  if (pollTimer)
186
207
  return;
187
208
  pollTimer = setInterval(() => {
188
209
  if (!activeCtx)
189
210
  return;
190
- void refreshStatus(activeCtx);
211
+ requestStatusRefresh(activeCtx);
191
212
  }, 12_000);
192
213
  }
193
214
  function stopPolling() {
@@ -196,7 +217,7 @@ export function brandingExtension(pi) {
196
217
  clearInterval(pollTimer);
197
218
  pollTimer = null;
198
219
  }
199
- async function initialize(ctx) {
220
+ function initialize(ctx) {
200
221
  activeCtx = ctx;
201
222
  repoName = basename(ctx.cwd);
202
223
  currentModelLabel = shortModelLabel(ctx);
@@ -205,7 +226,7 @@ export function brandingExtension(pi) {
205
226
  if (enabled) {
206
227
  applyDefaultTheme(ctx);
207
228
  applyChrome(ctx);
208
- await refreshStatus(ctx);
229
+ requestStatusRefresh(ctx);
209
230
  ensurePolling();
210
231
  }
211
232
  else {
@@ -213,11 +234,11 @@ export function brandingExtension(pi) {
213
234
  stopPolling();
214
235
  }
215
236
  }
216
- pi.on("session_start", async (_event, ctx) => {
217
- await initialize(ctx);
237
+ pi.on("session_start", (_event, ctx) => {
238
+ initialize(ctx);
218
239
  });
219
- pi.on("session_switch", async (_event, ctx) => {
220
- await initialize(ctx);
240
+ pi.on("session_switch", (_event, ctx) => {
241
+ initialize(ctx);
221
242
  });
222
243
  pi.on("model_select", async (event, ctx) => {
223
244
  currentModelLabel = shortModelLabelFromEvent(event.model);
@@ -234,6 +255,7 @@ export function brandingExtension(pi) {
234
255
  stopPolling();
235
256
  footerRequestRender = null;
236
257
  activeCtx = null;
258
+ refreshInFlight = null;
237
259
  });
238
260
  registerMuSubcommand(pi, {
239
261
  subcommand: "brand",
@@ -257,7 +279,7 @@ export function brandingExtension(pi) {
257
279
  if (enabled) {
258
280
  applyDefaultTheme(ctx);
259
281
  applyChrome(ctx);
260
- await refreshStatus(ctx);
282
+ requestStatusRefresh(ctx);
261
283
  ensurePolling();
262
284
  }
263
285
  else {
@@ -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;AA89CpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA6N3C;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;AAq/CpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QAkO3C;AAED,eAAe,WAAW,CAAC"}
@@ -7,7 +7,10 @@ const UI_PICKER_LIST_ITEMS_MAX = 4;
7
7
  const UI_PICKER_KEYVALUE_ROWS_MAX = 4;
8
8
  const UI_SESSION_KEY_FALLBACK = "__mu_ui_active_session__";
9
9
  const UI_PROMPT_PREVIEW_MAX = 160;
10
- const UI_INTERACT_SHORTCUT = "ctrl+shift+u";
10
+ const UI_INTERACT_SHORTCUT_PRIMARY = "ctrl+shift+u";
11
+ const UI_INTERACT_SHORTCUT_FALLBACK = "alt+u";
12
+ const UI_INTERACT_SHORTCUTS = [UI_INTERACT_SHORTCUT_PRIMARY, UI_INTERACT_SHORTCUT_FALLBACK];
13
+ const UI_INTERACT_SHORTCUT_HINT = `${UI_INTERACT_SHORTCUT_PRIMARY} or ${UI_INTERACT_SHORTCUT_FALLBACK}`;
11
14
  const UI_PICKER_PANEL_MIN_WIDTH = 56;
12
15
  const UI_PICKER_PANEL_MAX_WIDTH = 118;
13
16
  const UI_PICKER_PANEL_WIDTH_RATIO = 0.9;
@@ -41,6 +44,8 @@ function createState() {
41
44
  promptedRevisionKeys: new Set(),
42
45
  awaitingUiIds: new Set(),
43
46
  interactionDepth: 0,
47
+ lastStatusText: null,
48
+ lastWidgetSignature: null,
44
49
  };
45
50
  }
46
51
  function pruneStaleStates(nowMs) {
@@ -1211,11 +1216,30 @@ function refreshUi(ctx) {
1211
1216
  if (!ctx.hasUI) {
1212
1217
  return;
1213
1218
  }
1219
+ const commitStatus = (next) => {
1220
+ if (state.lastStatusText === next) {
1221
+ return;
1222
+ }
1223
+ state.lastStatusText = next;
1224
+ ctx.ui.setStatus("mu-ui", next ?? undefined);
1225
+ };
1226
+ const commitWidget = (next) => {
1227
+ const signature = next && next.length > 0 ? next.join("\n") : null;
1228
+ if (state.lastWidgetSignature === signature) {
1229
+ return;
1230
+ }
1231
+ state.lastWidgetSignature = signature;
1232
+ if (next && next.length > 0) {
1233
+ ctx.ui.setWidget("mu-ui", next, { placement: "belowEditor" });
1234
+ return;
1235
+ }
1236
+ ctx.ui.setWidget("mu-ui", undefined);
1237
+ };
1214
1238
  retainAwaitingUiIdsForActiveDocs(state);
1215
1239
  const docs = activeDocs(state);
1216
1240
  if (docs.length === 0) {
1217
- ctx.ui.setStatus("mu-ui", undefined);
1218
- ctx.ui.setWidget("mu-ui", undefined);
1241
+ commitStatus(null);
1242
+ commitWidget(null);
1219
1243
  return;
1220
1244
  }
1221
1245
  const awaiting = awaitingDocs(state, docs);
@@ -1237,14 +1261,8 @@ function refreshUi(ctx) {
1237
1261
  statusParts.push(ctx.ui.theme.fg("muted", "·"), ctx.ui.theme.fg("dim", `async ${asyncStatusDocs.length}`));
1238
1262
  }
1239
1263
  statusParts.push(ctx.ui.theme.fg("muted", "·"), ctx.ui.theme.fg("text", labels));
1240
- ctx.ui.setStatus("mu-ui", statusParts.join(" "));
1241
- const widgetLines = asyncStatusWidgetLines(ctx, docs);
1242
- if (widgetLines && widgetLines.length > 0) {
1243
- ctx.ui.setWidget("mu-ui", widgetLines, { placement: "belowEditor" });
1244
- }
1245
- else {
1246
- ctx.ui.setWidget("mu-ui", undefined);
1247
- }
1264
+ commitStatus(statusParts.join(" "));
1265
+ commitWidget(asyncStatusWidgetLines(ctx, docs));
1248
1266
  }
1249
1267
  export function uiExtension(pi) {
1250
1268
  const commandUsage = "/mu ui status|snapshot [compact|multiline]|interact [ui_id [action_id]]";
@@ -1376,15 +1394,17 @@ export function uiExtension(pi) {
1376
1394
  ctx.ui.notify(usage, "info");
1377
1395
  },
1378
1396
  });
1379
- pi.registerShortcut(UI_INTERACT_SHORTCUT, {
1380
- description: "Open programmable UI modal and optionally submit prompt",
1381
- handler: async (ctx) => {
1382
- const key = sessionKey(ctx);
1383
- const state = ensureState(key);
1384
- await runUiActionFromDoc(ctx, state);
1385
- refreshUi(ctx);
1386
- },
1387
- });
1397
+ for (const shortcut of UI_INTERACT_SHORTCUTS) {
1398
+ pi.registerShortcut(shortcut, {
1399
+ description: "Open programmable UI modal and optionally submit prompt",
1400
+ handler: async (ctx) => {
1401
+ const key = sessionKey(ctx);
1402
+ const state = ensureState(key);
1403
+ await runUiActionFromDoc(ctx, state);
1404
+ refreshUi(ctx);
1405
+ },
1406
+ });
1407
+ }
1388
1408
  pi.registerTool({
1389
1409
  name: "mu_ui",
1390
1410
  label: "mu UI",
@@ -1434,7 +1454,7 @@ export function uiExtension(pi) {
1434
1454
  return;
1435
1455
  }
1436
1456
  if (pending.kind === "action") {
1437
- ctx.ui.notify(`Agent requested input via ${pending.uiId}. Submit now or press ${UI_INTERACT_SHORTCUT} later.`, "info");
1457
+ ctx.ui.notify(`Agent requested input via ${pending.uiId}. Submit now or press ${UI_INTERACT_SHORTCUT_HINT} later.`, "info");
1438
1458
  }
1439
1459
  await runUiActionFromDoc(ctx, state, pending.uiId, pending.actionId);
1440
1460
  refreshUi(ctx);
@@ -1442,6 +1462,9 @@ export function uiExtension(pi) {
1442
1462
  pi.on("session_shutdown", (_event, ctx) => {
1443
1463
  const key = sessionKey(ctx);
1444
1464
  touchState(key);
1465
+ const state = ensureState(key);
1466
+ state.lastStatusText = null;
1467
+ state.lastWidgetSignature = null;
1445
1468
  if (!ctx.hasUI) {
1446
1469
  return;
1447
1470
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.117",
3
+ "version": "26.2.119",
4
4
  "description": "Shared operator runtime for mu assistant sessions and serve extensions.",
5
5
  "keywords": [
6
6
  "mu",
@@ -24,7 +24,7 @@
24
24
  "themes/**"
25
25
  ],
26
26
  "dependencies": {
27
- "@femtomc/mu-core": "26.2.117",
27
+ "@femtomc/mu-core": "26.2.119",
28
28
  "@mariozechner/pi-agent-core": "^0.54.2",
29
29
  "@mariozechner/pi-ai": "^0.54.2",
30
30
  "@mariozechner/pi-coding-agent": "^0.54.2",