@femtomc/mu-agent 26.2.111 → 26.2.112

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;AAsqCpF,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;AA82CpF,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,QA6N3C;AAED,eAAe,WAAW,CAAC"}
@@ -13,6 +13,11 @@ const UI_PICKER_PANEL_MAX_WIDTH = 118;
13
13
  const UI_PICKER_PANEL_WIDTH_RATIO = 0.9;
14
14
  const UI_PICKER_PANEL_TOP_MARGIN = 1;
15
15
  const UI_PICKER_PANEL_BOTTOM_MARGIN = 1;
16
+ const UI_PICKER_TWO_PANE_MIN_WIDTH = 92;
17
+ const UI_PICKER_TWO_PANE_LEFT_MIN = 24;
18
+ const UI_PICKER_TWO_PANE_RIGHT_MIN = 32;
19
+ const UI_PICKER_TWO_PANE_SEPARATOR_WIDTH = 3;
20
+ const UI_PICKER_INTERACTION_HINT = "↑/↓ move · tab switch pane · enter select/submit · esc cancel · click rows";
16
21
  const UI_ENABLE_MOUSE_TRACKING = "\x1b[?1000h\x1b[?1006h";
17
22
  const UI_DISABLE_MOUSE_TRACKING = "\x1b[?1000l\x1b[?1006l";
18
23
  const UI_INTERACT_OVERLAY_OPTIONS = {
@@ -522,6 +527,17 @@ function isPrimaryMouseRelease(event) {
522
527
  const primaryButton = event.buttonCode & 0b11;
523
528
  return primaryButton === 0;
524
529
  }
530
+ function fitStyledLine(text, width) {
531
+ const trimmed = truncateToWidth(text, width, "…", true);
532
+ const missing = width - visibleWidth(trimmed);
533
+ if (missing <= 0) {
534
+ return trimmed;
535
+ }
536
+ return `${trimmed}${" ".repeat(missing)}`;
537
+ }
538
+ function pluralize(count, singular, plural = `${singular}s`) {
539
+ return count === 1 ? singular : plural;
540
+ }
525
541
  function pickerComponentLines(component) {
526
542
  switch (component.kind) {
527
543
  case "text":
@@ -572,8 +588,7 @@ class UiActionPickerComponent {
572
588
  #panelRowEnd = 1;
573
589
  #panelColStart = 1;
574
590
  #panelColEnd = 1;
575
- #docHitRows = new Map();
576
- #actionHitRows = new Map();
591
+ #mouseTargets = [];
577
592
  constructor(opts) {
578
593
  this.#tui = opts.tui;
579
594
  this.#entries = opts.entries;
@@ -642,6 +657,118 @@ class UiActionPickerComponent {
642
657
  action,
643
658
  });
644
659
  }
660
+ #kindChip(action) {
661
+ switch (action.kind) {
662
+ case "primary":
663
+ return this.#theme.fg("success", "[primary]");
664
+ case "danger":
665
+ return this.#theme.fg("error", "[danger]");
666
+ case "link":
667
+ return this.#theme.fg("accent", "[link]");
668
+ case "secondary":
669
+ default:
670
+ return this.#theme.fg("muted", "[secondary]");
671
+ }
672
+ }
673
+ #buildDocsLines() {
674
+ const lines = [];
675
+ lines.push({
676
+ text: this.#theme.fg(this.#mode === "doc" ? "accent" : "dim", `Documents (${this.#entries.length})`),
677
+ });
678
+ for (let idx = 0; idx < this.#entries.length; idx += 1) {
679
+ const entry = this.#entries[idx];
680
+ const active = idx === this.#docIndex;
681
+ const marker = active ? (this.#mode === "doc" ? "▶" : "▸") : " ";
682
+ const actionCount = entry.actions.length;
683
+ const actionLabel = actionCount > 0
684
+ ? `${actionCount} ${pluralize(actionCount, "action")}`
685
+ : "status";
686
+ const title = this.#theme.fg(active ? "text" : "muted", `${marker} ${entry.doc.title}`);
687
+ const meta = this.#theme.fg("dim", `${entry.doc.ui_id} · ${actionLabel}`);
688
+ lines.push({
689
+ text: `${title} ${meta}`,
690
+ selected: active,
691
+ docIndex: idx,
692
+ });
693
+ }
694
+ return lines;
695
+ }
696
+ #buildDetailLines() {
697
+ const lines = [];
698
+ const selectedDoc = this.#currentEntry().doc;
699
+ const selectedActions = this.#currentActions();
700
+ lines.push({ text: this.#theme.fg("accent", selectedDoc.title) });
701
+ lines.push({
702
+ text: this.#theme.fg("dim", `${selectedDoc.ui_id} · revision ${selectedDoc.revision.version}`),
703
+ });
704
+ if (selectedDoc.summary) {
705
+ lines.push({ text: this.#theme.fg("dim", `Summary: ${selectedDoc.summary}`) });
706
+ }
707
+ lines.push({ text: "" });
708
+ lines.push({
709
+ text: this.#theme.fg("dim", `Components (${selectedDoc.components.length})`),
710
+ });
711
+ const visibleComponents = selectedDoc.components.slice(0, UI_PICKER_COMPONENTS_MAX);
712
+ for (const component of visibleComponents) {
713
+ const componentLines = pickerComponentLines(component);
714
+ for (let idx = 0; idx < componentLines.length; idx += 1) {
715
+ const line = componentLines[idx];
716
+ const prefix = idx === 0 ? " " : " ";
717
+ lines.push({ text: this.#theme.fg("text", `${prefix}${line}`) });
718
+ }
719
+ }
720
+ if (selectedDoc.components.length > visibleComponents.length) {
721
+ lines.push({
722
+ text: this.#theme.fg("muted", ` ... (+${selectedDoc.components.length - visibleComponents.length} more components)`),
723
+ });
724
+ }
725
+ lines.push({ text: "" });
726
+ lines.push({
727
+ text: this.#theme.fg(this.#mode === "action" ? "accent" : "dim", `Actions (${selectedActions.length})`),
728
+ });
729
+ for (let idx = 0; idx < selectedActions.length; idx += 1) {
730
+ const action = selectedActions[idx];
731
+ const active = idx === this.#actionIndex;
732
+ const marker = active ? (this.#mode === "action" ? "▶" : "▸") : " ";
733
+ const label = this.#theme.fg(active ? "text" : "muted", `${marker} ${action.label}`);
734
+ const chip = this.#kindChip(action);
735
+ const idLabel = this.#theme.fg("dim", `#${action.id}`);
736
+ lines.push({
737
+ text: `${label} ${chip} ${idLabel}`,
738
+ selected: active,
739
+ actionIndex: idx,
740
+ });
741
+ }
742
+ const action = this.#currentAction();
743
+ if (action?.description) {
744
+ lines.push({ text: "" });
745
+ lines.push({ text: this.#theme.fg("dim", `Ask: ${action.description}`) });
746
+ }
747
+ if (action?.component_id) {
748
+ lines.push({
749
+ text: this.#theme.fg("dim", `Targets component: ${action.component_id}`),
750
+ });
751
+ }
752
+ const commandText = action ? actionCommandText(action) : null;
753
+ if (commandText) {
754
+ lines.push({ text: "" });
755
+ lines.push({ text: this.#theme.fg("dim", `Prompt template: ${commandText}`) });
756
+ }
757
+ return lines;
758
+ }
759
+ #singleColumnLines() {
760
+ const lines = [];
761
+ const docs = this.#buildDocsLines();
762
+ const detail = this.#buildDetailLines();
763
+ for (const line of docs) {
764
+ lines.push(line);
765
+ }
766
+ lines.push({ text: "" });
767
+ for (const line of detail) {
768
+ lines.push(line);
769
+ }
770
+ return lines;
771
+ }
645
772
  #handleMouseEvent(event) {
646
773
  if (!isPrimaryMouseRelease(event)) {
647
774
  return;
@@ -652,19 +779,23 @@ class UiActionPickerComponent {
652
779
  event.col > this.#panelColEnd) {
653
780
  return;
654
781
  }
655
- const docIndex = this.#docHitRows.get(event.row);
656
- if (docIndex !== undefined) {
657
- this.#docIndex = boundedIndex(docIndex, this.#entries.length);
782
+ const target = this.#mouseTargets.find((candidate) => {
783
+ return (candidate.row === event.row &&
784
+ event.col >= candidate.colStart &&
785
+ event.col <= candidate.colEnd);
786
+ });
787
+ if (!target) {
788
+ return;
789
+ }
790
+ if (target.kind === "doc") {
791
+ this.#docIndex = boundedIndex(target.index, this.#entries.length);
658
792
  this.#actionIndex = boundedIndex(this.#actionIndex, this.#currentActions().length);
659
793
  this.#mode = "doc";
660
794
  return;
661
795
  }
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
- }
796
+ this.#mode = "action";
797
+ this.#actionIndex = boundedIndex(target.index, this.#currentActions().length);
798
+ this.#submit();
668
799
  }
669
800
  handleInput(data) {
670
801
  const mouse = parseSgrMouseEvent(data);
@@ -727,72 +858,86 @@ class UiActionPickerComponent {
727
858
  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
859
  const panelWidth = Math.max(4, Math.min(width, panelTargetWidth));
729
860
  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)));
741
- for (let idx = 0; idx < this.#entries.length; idx += 1) {
742
- const entry = this.#entries[idx];
743
- const active = idx === this.#docIndex;
744
- const marker = active ? (this.#mode === "doc" ? "▶" : "▸") : " ";
745
- const label = `${marker} ${entry.doc.ui_id} · ${entry.doc.title}`;
746
- docHitRowsByContentIndex.set(content.length, idx);
747
- pushContent(this.#theme.fg(active ? "accent" : "muted", short(label, maxContentWidth)));
748
- }
749
861
  const selectedDoc = this.#currentEntry().doc;
750
- if (selectedDoc.summary) {
751
- pushContent("");
752
- pushContent(this.#theme.fg("dim", short(`Summary: ${selectedDoc.summary}`, maxContentWidth)));
753
- }
754
- pushContent("");
755
- pushContent(this.#theme.fg("dim", short(`Components (${selectedDoc.components.length})`, maxContentWidth)));
756
- const visibleComponents = selectedDoc.components.slice(0, UI_PICKER_COMPONENTS_MAX);
757
- for (const component of visibleComponents) {
758
- const componentLines = pickerComponentLines(component);
759
- for (let idx = 0; idx < componentLines.length; idx += 1) {
760
- const line = componentLines[idx];
761
- const prefix = idx === 0 ? " " : " ";
762
- pushContent(this.#theme.fg("text", short(`${prefix}${line}`, maxContentWidth)));
862
+ const selectedActions = this.#currentActions();
863
+ const renderLines = [];
864
+ const pushFullLine = (line) => {
865
+ renderLines.push({
866
+ text: this.#theme.bg("customMessageBg", fitStyledLine(line, innerWidth)),
867
+ });
868
+ };
869
+ const modeLabel = this.#mode === "action" ? "action focus" : "document focus";
870
+ pushFullLine(`${this.#theme.fg("accent", "mu_ui")}${this.#theme.fg("dim", ` · ${this.#entries.length} ${pluralize(this.#entries.length, "doc")} · ${modeLabel}`)}`);
871
+ pushFullLine(this.#theme.fg("dim", short(UI_PICKER_INTERACTION_HINT, Math.max(8, innerWidth))));
872
+ pushFullLine(this.#theme.fg("borderMuted", "─".repeat(innerWidth)));
873
+ const minTwoPaneWidth = UI_PICKER_TWO_PANE_LEFT_MIN + UI_PICKER_TWO_PANE_RIGHT_MIN + UI_PICKER_TWO_PANE_SEPARATOR_WIDTH;
874
+ const useTwoPane = innerWidth >= UI_PICKER_TWO_PANE_MIN_WIDTH && innerWidth >= minTwoPaneWidth;
875
+ if (useTwoPane) {
876
+ let leftWidth = Math.max(UI_PICKER_TWO_PANE_LEFT_MIN, Math.floor(innerWidth * 0.34));
877
+ let rightWidth = innerWidth - leftWidth - UI_PICKER_TWO_PANE_SEPARATOR_WIDTH;
878
+ if (rightWidth < UI_PICKER_TWO_PANE_RIGHT_MIN) {
879
+ leftWidth = Math.max(UI_PICKER_TWO_PANE_LEFT_MIN, innerWidth - UI_PICKER_TWO_PANE_RIGHT_MIN - UI_PICKER_TWO_PANE_SEPARATOR_WIDTH);
880
+ rightWidth = innerWidth - leftWidth - UI_PICKER_TWO_PANE_SEPARATOR_WIDTH;
881
+ }
882
+ const leftLines = this.#buildDocsLines();
883
+ const rightLines = this.#buildDetailLines();
884
+ const rowCount = Math.max(leftLines.length, rightLines.length);
885
+ for (let idx = 0; idx < rowCount; idx += 1) {
886
+ const left = leftLines[idx];
887
+ const right = rightLines[idx];
888
+ const leftCell = this.#theme.bg(left?.selected ? "selectedBg" : "customMessageBg", fitStyledLine(left?.text ?? "", leftWidth));
889
+ const separator = this.#theme.bg("customMessageBg", this.#theme.fg("borderMuted", " │ "));
890
+ const rightCell = this.#theme.bg(right?.selected ? "selectedBg" : "customMessageBg", fitStyledLine(right?.text ?? "", rightWidth));
891
+ const row = {
892
+ text: `${leftCell}${separator}${rightCell}`,
893
+ };
894
+ if (left?.docIndex !== undefined) {
895
+ row.docTarget = {
896
+ index: left.docIndex,
897
+ colStart: 1,
898
+ colEnd: leftWidth,
899
+ };
900
+ }
901
+ if (right?.actionIndex !== undefined) {
902
+ row.actionTarget = {
903
+ index: right.actionIndex,
904
+ colStart: leftWidth + UI_PICKER_TWO_PANE_SEPARATOR_WIDTH + 1,
905
+ colEnd: leftWidth + UI_PICKER_TWO_PANE_SEPARATOR_WIDTH + rightWidth,
906
+ };
907
+ }
908
+ renderLines.push(row);
763
909
  }
764
910
  }
765
- if (selectedDoc.components.length > visibleComponents.length) {
766
- pushContent(this.#theme.fg("muted", short(` ... (+${selectedDoc.components.length - visibleComponents.length} more components)`, maxContentWidth)));
767
- }
768
- const actions = this.#currentActions();
769
- pushContent("");
770
- pushContent(this.#theme.fg(this.#mode === "action" ? "accent" : "dim", short(`Actions (${actions.length})`, maxContentWidth)));
771
- for (let idx = 0; idx < actions.length; idx += 1) {
772
- const action = actions[idx];
773
- const active = idx === this.#actionIndex;
774
- const marker = active ? (this.#mode === "action" ? "▶" : "▸") : " ";
775
- const label = `${marker} ${action.id} · ${action.label}`;
776
- actionHitRowsByContentIndex.set(content.length, idx);
777
- pushContent(this.#theme.fg(active ? "accent" : "text", short(label, maxContentWidth)));
778
- }
779
- const action = this.#currentAction();
780
- if (action?.description) {
781
- pushContent("");
782
- pushContent(this.#theme.fg("dim", short(`Ask: ${action.description}`, maxContentWidth)));
783
- }
784
- if (action?.component_id) {
785
- pushContent(this.#theme.fg("dim", short(`Targets component: ${action.component_id}`, maxContentWidth)));
786
- }
787
- const commandText = action ? actionCommandText(action) : null;
788
- if (commandText) {
789
- pushContent("");
790
- pushContent(this.#theme.fg("dim", short(`Prompt template: ${commandText}`, maxContentWidth)));
911
+ else {
912
+ const singleColumn = this.#singleColumnLines();
913
+ for (const line of singleColumn) {
914
+ const row = {
915
+ text: this.#theme.bg(line.selected ? "selectedBg" : "customMessageBg", fitStyledLine(line.text, innerWidth)),
916
+ };
917
+ if (line.docIndex !== undefined) {
918
+ row.docTarget = {
919
+ index: line.docIndex,
920
+ colStart: 1,
921
+ colEnd: innerWidth,
922
+ };
923
+ }
924
+ if (line.actionIndex !== undefined) {
925
+ row.actionTarget = {
926
+ index: line.actionIndex,
927
+ colStart: 1,
928
+ colEnd: innerWidth,
929
+ };
930
+ }
931
+ renderLines.push(row);
932
+ }
791
933
  }
934
+ pushFullLine(this.#theme.fg("borderMuted", "─".repeat(innerWidth)));
935
+ pushFullLine(this.#theme.fg("dim", short(`selected ${selectedDoc.ui_id} · revision ${selectedDoc.revision.version} · ${selectedActions.length} ${pluralize(selectedActions.length, "action")}`, Math.max(8, innerWidth))));
792
936
  const topMarginRows = Math.max(0, UI_PICKER_PANEL_TOP_MARGIN);
793
937
  const bottomMarginRows = Math.max(0, UI_PICKER_PANEL_BOTTOM_MARGIN);
794
938
  const leftPadWidth = Math.max(0, Math.floor((width - panelWidth) / 2));
795
939
  const leftPad = " ".repeat(leftPadWidth);
940
+ const panelColStart = leftPadWidth + 1;
796
941
  const frame = [];
797
942
  for (let row = 0; row < topMarginRows; row += 1) {
798
943
  frame.push("");
@@ -802,22 +947,29 @@ class UiActionPickerComponent {
802
947
  const leftRule = "─".repeat(Math.max(0, Math.floor((innerWidth - titleWidth) / 2)));
803
948
  const rightRule = "─".repeat(Math.max(0, innerWidth - titleWidth - leftRule.length));
804
949
  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();
950
+ this.#mouseTargets = [];
807
951
  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", "│")}`);
952
+ for (let idx = 0; idx < renderLines.length; idx += 1) {
953
+ const line = renderLines[idx];
954
+ frame.push(`${leftPad}${this.#theme.fg("border", "│")}${line.text}${this.#theme.fg("border", "│")}`);
813
955
  const row = contentStartRow + idx;
814
- const docIndex = docHitRowsByContentIndex.get(idx);
815
- if (docIndex !== undefined) {
816
- this.#docHitRows.set(row, docIndex);
956
+ if (line.docTarget) {
957
+ this.#mouseTargets.push({
958
+ kind: "doc",
959
+ index: line.docTarget.index,
960
+ row,
961
+ colStart: panelColStart + line.docTarget.colStart,
962
+ colEnd: panelColStart + line.docTarget.colEnd,
963
+ });
817
964
  }
818
- const actionIndex = actionHitRowsByContentIndex.get(idx);
819
- if (actionIndex !== undefined) {
820
- this.#actionHitRows.set(row, actionIndex);
965
+ if (line.actionTarget) {
966
+ this.#mouseTargets.push({
967
+ kind: "action",
968
+ index: line.actionTarget.index,
969
+ row,
970
+ colStart: panelColStart + line.actionTarget.colStart,
971
+ colEnd: panelColStart + line.actionTarget.colEnd,
972
+ });
821
973
  }
822
974
  }
823
975
  frame.push(`${leftPad}${this.#theme.fg("borderAccent", `╰${"─".repeat(innerWidth)}╯`)}`);
@@ -825,8 +977,8 @@ class UiActionPickerComponent {
825
977
  frame.push("");
826
978
  }
827
979
  this.#panelRowStart = topMarginRows + 1;
828
- this.#panelRowEnd = this.#panelRowStart + content.length + 1;
829
- this.#panelColStart = leftPadWidth + 1;
980
+ this.#panelRowEnd = this.#panelRowStart + renderLines.length + 1;
981
+ this.#panelColStart = panelColStart;
830
982
  this.#panelColEnd = leftPadWidth + panelWidth;
831
983
  return frame;
832
984
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.111",
3
+ "version": "26.2.112",
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.111",
28
+ "@femtomc/mu-core": "26.2.112",
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",