@femtomc/mu-agent 26.2.110 → 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;AAs/BpF,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;AAsqCpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA6N3C;AAED,eAAe,WAAW,CAAC"}
@@ -1,5 +1,5 @@
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
5
  const UI_PICKER_COMPONENTS_MAX = 8;
@@ -8,6 +8,13 @@ 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
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";
11
18
  const UI_INTERACT_OVERLAY_OPTIONS = {
12
19
  anchor: "top-left",
13
20
  row: 0,
@@ -487,6 +494,34 @@ function boundedIndex(index, length) {
487
494
  }
488
495
  return index;
489
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
+ }
490
525
  function pickerComponentLines(component) {
491
526
  switch (component.kind) {
492
527
  case "text":
@@ -525,13 +560,22 @@ function pickerComponentLines(component) {
525
560
  }
526
561
  }
527
562
  class UiActionPickerComponent {
563
+ #tui;
528
564
  #entries;
529
565
  #theme;
530
566
  #done;
531
567
  #mode = "doc";
532
568
  #docIndex = 0;
533
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();
534
577
  constructor(opts) {
578
+ this.#tui = opts.tui;
535
579
  this.#entries = opts.entries;
536
580
  this.#theme = opts.theme;
537
581
  this.#done = opts.done;
@@ -549,6 +593,21 @@ class UiActionPickerComponent {
549
593
  this.#mode = "action";
550
594
  }
551
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;
552
611
  }
553
612
  #currentEntry() {
554
613
  return this.#entries[this.#docIndex];
@@ -583,7 +642,36 @@ class UiActionPickerComponent {
583
642
  action,
584
643
  });
585
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
+ }
586
669
  handleInput(data) {
670
+ const mouse = parseSgrMouseEvent(data);
671
+ if (mouse) {
672
+ this.#handleMouseEvent(mouse);
673
+ return;
674
+ }
587
675
  if (matchesKey(data, "escape")) {
588
676
  this.#done(null);
589
677
  return;
@@ -632,67 +720,120 @@ class UiActionPickerComponent {
632
720
  invalidate() {
633
721
  // No cached state.
634
722
  }
723
+ dispose() {
724
+ this.#disableMouseTracking();
725
+ }
635
726
  render(width) {
636
- const maxWidth = Math.max(24, width - 2);
637
- const lines = [];
638
- lines.push(this.#theme.fg("accent", short("Programmable UI", maxWidth)));
639
- lines.push(this.#theme.fg("dim", short("↑/↓ move · tab switch · enter select/submit · esc cancel", maxWidth)));
640
- lines.push("");
641
- 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)));
642
741
  for (let idx = 0; idx < this.#entries.length; idx += 1) {
643
742
  const entry = this.#entries[idx];
644
743
  const active = idx === this.#docIndex;
645
744
  const marker = active ? (this.#mode === "doc" ? "▶" : "▸") : " ";
646
745
  const label = `${marker} ${entry.doc.ui_id} · ${entry.doc.title}`;
647
- 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)));
648
748
  }
649
749
  const selectedDoc = this.#currentEntry().doc;
650
750
  if (selectedDoc.summary) {
651
- lines.push("");
652
- lines.push(this.#theme.fg("dim", short(`Summary: ${selectedDoc.summary}`, maxWidth)));
751
+ pushContent("");
752
+ pushContent(this.#theme.fg("dim", short(`Summary: ${selectedDoc.summary}`, maxContentWidth)));
653
753
  }
654
- lines.push("");
655
- 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)));
656
756
  const visibleComponents = selectedDoc.components.slice(0, UI_PICKER_COMPONENTS_MAX);
657
757
  for (const component of visibleComponents) {
658
758
  const componentLines = pickerComponentLines(component);
659
759
  for (let idx = 0; idx < componentLines.length; idx += 1) {
660
760
  const line = componentLines[idx];
661
761
  const prefix = idx === 0 ? " " : " ";
662
- lines.push(this.#theme.fg("text", short(`${prefix}${line}`, maxWidth)));
762
+ pushContent(this.#theme.fg("text", short(`${prefix}${line}`, maxContentWidth)));
663
763
  }
664
764
  }
665
765
  if (selectedDoc.components.length > visibleComponents.length) {
666
- 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)));
667
767
  }
668
768
  const actions = this.#currentActions();
669
- lines.push("");
670
- 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)));
671
771
  for (let idx = 0; idx < actions.length; idx += 1) {
672
772
  const action = actions[idx];
673
773
  const active = idx === this.#actionIndex;
674
774
  const marker = active ? (this.#mode === "action" ? "▶" : "▸") : " ";
675
775
  const label = `${marker} ${action.id} · ${action.label}`;
676
- 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)));
677
778
  }
678
779
  const action = this.#currentAction();
679
780
  if (action?.description) {
680
- lines.push("");
681
- lines.push(this.#theme.fg("dim", short(`Ask: ${action.description}`, maxWidth)));
781
+ pushContent("");
782
+ pushContent(this.#theme.fg("dim", short(`Ask: ${action.description}`, maxContentWidth)));
682
783
  }
683
784
  if (action?.component_id) {
684
- 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)));
685
786
  }
686
787
  const commandText = action ? actionCommandText(action) : null;
687
788
  if (commandText) {
688
- lines.push("");
689
- 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("");
799
+ }
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("");
690
826
  }
691
- return lines;
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;
692
832
  }
693
833
  }
694
834
  async function pickUiActionInteractively(opts) {
695
- 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,
696
837
  entries: opts.entries,
697
838
  theme: theme,
698
839
  done,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.110",
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.110",
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",