@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.
- package/assets/mu-tui-logo.png +0 -0
- package/dist/extensions/ui.d.ts.map +1 -1
- package/dist/extensions/ui.js +248 -153
- package/package.json +2 -2
package/assets/mu-tui-logo.png
CHANGED
|
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;
|
|
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"}
|
package/dist/extensions/ui.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 [
|
|
528
|
+
return [component.text];
|
|
461
529
|
case "list": {
|
|
462
|
-
const
|
|
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
|
|
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 ["
|
|
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
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
751
|
+
pushContent("");
|
|
752
|
+
pushContent(this.#theme.fg("dim", short(`Summary: ${selectedDoc.summary}`, maxContentWidth)));
|
|
616
753
|
}
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
762
|
+
pushContent(this.#theme.fg("text", short(`${prefix}${line}`, maxContentWidth)));
|
|
626
763
|
}
|
|
627
764
|
}
|
|
628
765
|
if (selectedDoc.components.length > visibleComponents.length) {
|
|
629
|
-
|
|
766
|
+
pushContent(this.#theme.fg("muted", short(` ... (+${selectedDoc.components.length - visibleComponents.length} more components)`, maxContentWidth)));
|
|
630
767
|
}
|
|
631
768
|
const actions = this.#currentActions();
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
644
|
-
|
|
781
|
+
pushContent("");
|
|
782
|
+
pushContent(this.#theme.fg("dim", short(`Ask: ${action.description}`, maxContentWidth)));
|
|
645
783
|
}
|
|
646
784
|
if (action?.component_id) {
|
|
647
|
-
|
|
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
|
-
|
|
652
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
1181
|
+
retainPendingPromptsForActiveDocs(state);
|
|
1182
|
+
const pending = state.pendingPrompts.shift();
|
|
1089
1183
|
if (!pending) {
|
|
1090
1184
|
return;
|
|
1091
1185
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
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.
|
|
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.
|
|
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",
|