mu-harness 0.16.22 → 0.17.1
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/esm/tui/chat/ChatApp.d.ts +10 -6
- package/esm/tui/chat/ChatApp.js +61 -38
- package/esm/tui/chat/editor.d.ts +12 -0
- package/esm/tui/chat/editor.js +75 -17
- package/esm/tui/chat/picker.d.ts +5 -0
- package/esm/tui/chat/picker.js +14 -0
- package/package.json +3 -3
- package/script/tui/chat/ChatApp.d.ts +10 -6
- package/script/tui/chat/ChatApp.js +59 -36
- package/script/tui/chat/editor.d.ts +12 -0
- package/script/tui/chat/editor.js +75 -17
- package/script/tui/chat/picker.d.ts +5 -0
- package/script/tui/chat/picker.js +15 -0
|
@@ -54,12 +54,12 @@ export interface ChatHost {
|
|
|
54
54
|
*/
|
|
55
55
|
banner?: string;
|
|
56
56
|
/**
|
|
57
|
-
* Lean input presentation:
|
|
58
|
-
* model/provider/agent footer inside the input
|
|
59
|
-
* the status bar — leaving a
|
|
57
|
+
* Lean input presentation: keep the surface-background input frame but drop
|
|
58
|
+
* the model/provider/agent footer inside the input and the context readout on
|
|
59
|
+
* the status bar — leaving a framed prompt + editor. The {@link ChatHost.banner}
|
|
60
60
|
* splash is independent and still shown if set. Hosts that want the full
|
|
61
|
-
* information-rich input (
|
|
62
|
-
*
|
|
61
|
+
* information-rich input (model · provider · @agent footer, token/context usage
|
|
62
|
+
* in the status line) leave this unset (the default).
|
|
63
63
|
*/
|
|
64
64
|
minimal?: boolean;
|
|
65
65
|
onExit(code: number): void;
|
|
@@ -89,6 +89,8 @@ export declare class ChatApp {
|
|
|
89
89
|
private running;
|
|
90
90
|
private readonly queue;
|
|
91
91
|
private readonly pendingShell;
|
|
92
|
+
private readonly pastes;
|
|
93
|
+
private pasteSeq;
|
|
92
94
|
private models;
|
|
93
95
|
private paletteCursor;
|
|
94
96
|
private paletteDismissedFor;
|
|
@@ -132,8 +134,11 @@ export declare class ChatApp {
|
|
|
132
134
|
private onTurnComplete;
|
|
133
135
|
private stripFileMentions;
|
|
134
136
|
private send;
|
|
137
|
+
private capturePaste;
|
|
138
|
+
private expandPastes;
|
|
135
139
|
private flushShellContext;
|
|
136
140
|
private submit;
|
|
141
|
+
private clearPastes;
|
|
137
142
|
private enqueueFromInput;
|
|
138
143
|
private onInputChange;
|
|
139
144
|
private intercept;
|
|
@@ -148,7 +153,6 @@ export declare class ChatApp {
|
|
|
148
153
|
private pickerVisible;
|
|
149
154
|
private pickerMove;
|
|
150
155
|
private pickerAccept;
|
|
151
|
-
private deleteMention;
|
|
152
156
|
private pushHistory;
|
|
153
157
|
private navigateHistory;
|
|
154
158
|
private newSession;
|
package/esm/tui/chat/ChatApp.js
CHANGED
|
@@ -4,9 +4,9 @@ import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
|
4
4
|
import { box, column, flex, measure, ProcessTerminal, scrollView, truncateToWidth, TUI, visibleWidth, } from 'mu-tui';
|
|
5
5
|
import { buildCommands, filterCommands } from './commands.js';
|
|
6
6
|
import { MultilineEditor } from './editor.js';
|
|
7
|
-
import { activeMention, collectCandidates, rank } from './picker.js';
|
|
7
|
+
import { activeMention, collectCandidates, mentionRanges, rank } from './picker.js';
|
|
8
8
|
import { formatTokens, statusComponent, statusFromEvent } from './status.js';
|
|
9
|
-
import { asHexColor, styleToAnsi, ThemeProvider, themesByName } from './theme.js';
|
|
9
|
+
import { asHexColor, fgToAnsi, styleToAnsi, ThemeProvider, themesByName } from './theme.js';
|
|
10
10
|
import { entryComponent, formatToolArgs, stickyHeader, Transcript, transcriptComponent, } from './transcript.js';
|
|
11
11
|
const RESET = '\x1b[0m';
|
|
12
12
|
const PROMPT_WIDTH = 2;
|
|
@@ -88,6 +88,8 @@ export class ChatApp {
|
|
|
88
88
|
running = false;
|
|
89
89
|
queue = [];
|
|
90
90
|
pendingShell = [];
|
|
91
|
+
pastes = new Map();
|
|
92
|
+
pasteSeq = 0;
|
|
91
93
|
models = [];
|
|
92
94
|
paletteCursor = 0;
|
|
93
95
|
paletteDismissedFor = '__none__';
|
|
@@ -132,6 +134,20 @@ export class ChatApp {
|
|
|
132
134
|
onSubmit: (value) => this.submit(value),
|
|
133
135
|
onChange: (value) => this.onInputChange(value),
|
|
134
136
|
});
|
|
137
|
+
this.editor.mentionRanges = (value, cursor) => {
|
|
138
|
+
const ranges = mentionRanges(value, activeMention(value, cursor)?.start);
|
|
139
|
+
for (const placeholder of this.pastes.keys()) {
|
|
140
|
+
for (let i = value.indexOf(placeholder); i !== -1; i = value.indexOf(placeholder, i + placeholder.length)) {
|
|
141
|
+
ranges.push({ start: i, end: i + placeholder.length });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return ranges;
|
|
145
|
+
};
|
|
146
|
+
this.editor.onPaste = (text) => this.capturePaste(text);
|
|
147
|
+
this.editor.chipColor = () => {
|
|
148
|
+
const bg = this.theme().styles.commandPaletteSelected.bg;
|
|
149
|
+
return bg ? fgToAnsi(bg) : '';
|
|
150
|
+
};
|
|
135
151
|
this.scroll = scrollView({ render: (s) => transcriptComponent(this.transcript, this.theme()).render(s) }, { stickyHeader: (info) => this.stickyHeaderView(info), footer: () => this.jumpToBottomHint() });
|
|
136
152
|
this.subScroll = scrollView({ render: (s) => transcriptComponent(this.subTranscript, this.theme()).render(s) });
|
|
137
153
|
this.commands = buildCommands(this.commandHost()).filter((c) => {
|
|
@@ -143,6 +159,8 @@ export class ChatApp {
|
|
|
143
159
|
});
|
|
144
160
|
this.tui.setRoot({ render: (s) => this.root().render(s) });
|
|
145
161
|
this.tui.setBackgroundColor(this.theme().colors.background);
|
|
162
|
+
this.tui.setToastBackground(this.theme().colors.surface);
|
|
163
|
+
this.tui.setToastForeground(this.theme().colors.text);
|
|
146
164
|
this.tui.setFocus(this.editor);
|
|
147
165
|
this.tui.addInputInterceptor((event) => this.intercept(event));
|
|
148
166
|
this.tui.addGlobalKeybinding({ chord: { key: 'c', ctrl: true }, handler: () => this.onCtrlC() });
|
|
@@ -151,6 +169,8 @@ export class ChatApp {
|
|
|
151
169
|
this.tui.addGlobalKeybinding({ chord: { key: 'end', ctrl: true }, handler: () => this.jumpToBottom() });
|
|
152
170
|
this.unsubscribeTheme = this.themeProvider.subscribe(() => {
|
|
153
171
|
this.tui.setBackgroundColor(this.theme().colors.background);
|
|
172
|
+
this.tui.setToastBackground(this.theme().colors.surface);
|
|
173
|
+
this.tui.setToastForeground(this.theme().colors.text);
|
|
154
174
|
this.tui.requestRender(true);
|
|
155
175
|
});
|
|
156
176
|
this.bindSession();
|
|
@@ -462,6 +482,23 @@ export class ChatApp {
|
|
|
462
482
|
this.showError(err instanceof Error ? err.message : String(err));
|
|
463
483
|
});
|
|
464
484
|
}
|
|
485
|
+
capturePaste(text) {
|
|
486
|
+
const lines = text.split('\n').length;
|
|
487
|
+
if (lines < 2 && text.length <= 200)
|
|
488
|
+
return undefined;
|
|
489
|
+
const id = ++this.pasteSeq;
|
|
490
|
+
const summary = lines > 1 ? `${lines} lines` : `${text.length} chars`;
|
|
491
|
+
const placeholder = `[pasted #${id}, ${summary}]`;
|
|
492
|
+
this.pastes.set(placeholder, text);
|
|
493
|
+
return placeholder;
|
|
494
|
+
}
|
|
495
|
+
expandPastes(text) {
|
|
496
|
+
let out = text;
|
|
497
|
+
for (const [placeholder, content] of this.pastes) {
|
|
498
|
+
out = out.split(placeholder).join(content);
|
|
499
|
+
}
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
465
502
|
flushShellContext(userText) {
|
|
466
503
|
if (this.pendingShell.length === 0)
|
|
467
504
|
return userText;
|
|
@@ -478,35 +515,47 @@ export class ChatApp {
|
|
|
478
515
|
if (this.modelPickerOpen || this.sessionPickerOpen)
|
|
479
516
|
return;
|
|
480
517
|
this.clearError();
|
|
481
|
-
this.
|
|
518
|
+
const text = this.expandPastes(trimmed);
|
|
482
519
|
this.editor.setValue('');
|
|
483
|
-
|
|
484
|
-
|
|
520
|
+
this.clearPastes();
|
|
521
|
+
this.pushHistory(text);
|
|
522
|
+
if (text.startsWith('!') || text.startsWith('$')) {
|
|
523
|
+
this.runShell(text.slice(1).trim());
|
|
485
524
|
return;
|
|
486
525
|
}
|
|
487
|
-
if (
|
|
488
|
-
this.runCommand(
|
|
526
|
+
if (text.startsWith('/')) {
|
|
527
|
+
this.runCommand(text);
|
|
489
528
|
return;
|
|
490
529
|
}
|
|
491
|
-
if (this.tryDispatch(
|
|
530
|
+
if (this.tryDispatch(text))
|
|
492
531
|
return;
|
|
493
532
|
if (this.running) {
|
|
494
|
-
this.queue.push(
|
|
533
|
+
this.queue.push(text);
|
|
495
534
|
this.tui.requestRender();
|
|
496
535
|
return;
|
|
497
536
|
}
|
|
498
|
-
this.send(
|
|
537
|
+
this.send(text);
|
|
538
|
+
}
|
|
539
|
+
clearPastes() {
|
|
540
|
+
this.pastes.clear();
|
|
541
|
+
this.pasteSeq = 0;
|
|
499
542
|
}
|
|
500
543
|
enqueueFromInput() {
|
|
501
544
|
const value = this.editor.getValue().trim();
|
|
502
545
|
if (!value)
|
|
503
546
|
return;
|
|
504
|
-
this.
|
|
547
|
+
const text = this.expandPastes(value);
|
|
505
548
|
this.editor.setValue('');
|
|
506
|
-
this.
|
|
549
|
+
this.clearPastes();
|
|
550
|
+
this.pushHistory(text);
|
|
551
|
+
this.queue.push(text);
|
|
507
552
|
this.tui.requestRender();
|
|
508
553
|
}
|
|
509
554
|
onInputChange(value) {
|
|
555
|
+
for (const placeholder of this.pastes.keys()) {
|
|
556
|
+
if (!value.includes(placeholder))
|
|
557
|
+
this.pastes.delete(placeholder);
|
|
558
|
+
}
|
|
510
559
|
if (value !== this.paletteDismissedFor)
|
|
511
560
|
this.paletteDismissedFor = '__none__';
|
|
512
561
|
const items = this.paletteItems();
|
|
@@ -554,11 +603,6 @@ export class ChatApp {
|
|
|
554
603
|
}
|
|
555
604
|
if (key === 'escape' || key === 'esc')
|
|
556
605
|
return this.onEscape();
|
|
557
|
-
if (key === 'backspace') {
|
|
558
|
-
if (this.deleteMention())
|
|
559
|
-
return true;
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
606
|
if (key === 'enter' && event.alt) {
|
|
563
607
|
if (this.running) {
|
|
564
608
|
this.enqueueFromInput();
|
|
@@ -716,25 +760,6 @@ export class ChatApp {
|
|
|
716
760
|
this.tui.requestRender();
|
|
717
761
|
return true;
|
|
718
762
|
}
|
|
719
|
-
deleteMention() {
|
|
720
|
-
if (this.pickerMention !== undefined)
|
|
721
|
-
return false;
|
|
722
|
-
const value = this.editor.getValue();
|
|
723
|
-
const cursor = this.editor.cursorPos;
|
|
724
|
-
const re = /@[^\s]+/g;
|
|
725
|
-
let match;
|
|
726
|
-
while ((match = re.exec(value)) !== null) {
|
|
727
|
-
const start = match.index;
|
|
728
|
-
const end = start + match[0].length;
|
|
729
|
-
if (cursor > start && cursor <= end) {
|
|
730
|
-
this.editor.setValue(value.slice(0, start) + value.slice(end));
|
|
731
|
-
this.editor.setCursor(start);
|
|
732
|
-
this.tui.requestRender();
|
|
733
|
-
return true;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
return false;
|
|
737
|
-
}
|
|
738
763
|
pushHistory(text) {
|
|
739
764
|
this.host.history?.append(text);
|
|
740
765
|
if (this.history[this.history.length - 1] !== text)
|
|
@@ -1091,8 +1116,6 @@ export class ChatApp {
|
|
|
1091
1116
|
}
|
|
1092
1117
|
inputPanel() {
|
|
1093
1118
|
const inner = this.approvalView() ?? this.editorInner();
|
|
1094
|
-
if (this.minimal)
|
|
1095
|
-
return box(inner, { padding: 0 });
|
|
1096
1119
|
return box(inner, { background: this.theme().colors.surface, padding: 1 });
|
|
1097
1120
|
}
|
|
1098
1121
|
editorInner() {
|
package/esm/tui/chat/editor.d.ts
CHANGED
|
@@ -5,12 +5,19 @@ export interface MultilineEditorOptions {
|
|
|
5
5
|
onSubmit?: (value: string) => void;
|
|
6
6
|
onChange?: (value: string) => void;
|
|
7
7
|
}
|
|
8
|
+
interface ChipRange {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
8
12
|
export declare class MultilineEditor implements Component {
|
|
9
13
|
private value;
|
|
10
14
|
private cursor;
|
|
11
15
|
private readonly placeholder;
|
|
12
16
|
private readonly maxRows;
|
|
13
17
|
hiddenPrefix: string;
|
|
18
|
+
chipColor?: () => string;
|
|
19
|
+
mentionRanges?: (value: string, cursor: number) => ChipRange[];
|
|
20
|
+
onPaste?: (text: string) => string | undefined;
|
|
14
21
|
onSubmit?: (value: string) => void;
|
|
15
22
|
onChange?: (value: string) => void;
|
|
16
23
|
constructor(opts?: MultilineEditorOptions);
|
|
@@ -24,8 +31,13 @@ export declare class MultilineEditor implements Component {
|
|
|
24
31
|
private lineStart;
|
|
25
32
|
private lineEnd;
|
|
26
33
|
private insert;
|
|
34
|
+
private chips;
|
|
35
|
+
private moveLeft;
|
|
36
|
+
private moveRight;
|
|
27
37
|
private backspace;
|
|
28
38
|
private deleteForward;
|
|
29
39
|
private cursorRowCol;
|
|
30
40
|
render(s: Surface): void;
|
|
41
|
+
private renderRow;
|
|
31
42
|
}
|
|
43
|
+
export {};
|
package/esm/tui/chat/editor.js
CHANGED
|
@@ -2,12 +2,16 @@ import { truncateToWidth, visibleWidth } from 'mu-tui';
|
|
|
2
2
|
const CURSOR = '\x1b[7m';
|
|
3
3
|
const RESET = '\x1b[0m';
|
|
4
4
|
const DIM = '\x1b[2m';
|
|
5
|
+
const CHIP = '\x1b[33m';
|
|
5
6
|
export class MultilineEditor {
|
|
6
7
|
value = '';
|
|
7
8
|
cursor = 0;
|
|
8
9
|
placeholder;
|
|
9
10
|
maxRows;
|
|
10
11
|
hiddenPrefix = '';
|
|
12
|
+
chipColor;
|
|
13
|
+
mentionRanges;
|
|
14
|
+
onPaste;
|
|
11
15
|
onSubmit;
|
|
12
16
|
onChange;
|
|
13
17
|
constructor(opts = {}) {
|
|
@@ -36,7 +40,7 @@ export class MultilineEditor {
|
|
|
36
40
|
}
|
|
37
41
|
handleInput(event) {
|
|
38
42
|
if (event.type === 'paste') {
|
|
39
|
-
this.insert(event.text);
|
|
43
|
+
this.insert(this.onPaste?.(event.text) ?? event.text);
|
|
40
44
|
return;
|
|
41
45
|
}
|
|
42
46
|
if (event.type === 'text') {
|
|
@@ -62,10 +66,10 @@ export class MultilineEditor {
|
|
|
62
66
|
this.deleteForward();
|
|
63
67
|
return;
|
|
64
68
|
case 'left':
|
|
65
|
-
this.
|
|
69
|
+
this.moveLeft();
|
|
66
70
|
return;
|
|
67
71
|
case 'right':
|
|
68
|
-
this.
|
|
72
|
+
this.moveRight();
|
|
69
73
|
return;
|
|
70
74
|
case 'home':
|
|
71
75
|
this.cursor = this.lineStart();
|
|
@@ -90,17 +94,35 @@ export class MultilineEditor {
|
|
|
90
94
|
this.cursor += text.length;
|
|
91
95
|
this.onChange?.(this.value);
|
|
92
96
|
}
|
|
97
|
+
chips() {
|
|
98
|
+
return this.mentionRanges ? this.mentionRanges(this.value, this.cursor) : [];
|
|
99
|
+
}
|
|
100
|
+
moveLeft() {
|
|
101
|
+
const chip = this.chips().find((c) => c.start < this.cursor && this.cursor <= c.end);
|
|
102
|
+
this.cursor = chip ? chip.start : Math.max(0, this.cursor - 1);
|
|
103
|
+
}
|
|
104
|
+
moveRight() {
|
|
105
|
+
const chip = this.chips().find((c) => c.start <= this.cursor && this.cursor < c.end);
|
|
106
|
+
this.cursor = chip ? chip.end : Math.min(this.value.length, this.cursor + 1);
|
|
107
|
+
}
|
|
93
108
|
backspace() {
|
|
94
109
|
if (this.cursor === 0)
|
|
95
110
|
return;
|
|
96
|
-
|
|
97
|
-
this.cursor
|
|
111
|
+
const chip = this.chips().find((c) => c.start < this.cursor && this.cursor <= c.end);
|
|
112
|
+
const from = chip ? chip.start : this.cursor - 1;
|
|
113
|
+
const to = chip ? chip.end : this.cursor;
|
|
114
|
+
this.value = this.value.slice(0, from) + this.value.slice(to);
|
|
115
|
+
this.cursor = from;
|
|
98
116
|
this.onChange?.(this.value);
|
|
99
117
|
}
|
|
100
118
|
deleteForward() {
|
|
101
119
|
if (this.cursor >= this.value.length)
|
|
102
120
|
return;
|
|
103
|
-
|
|
121
|
+
const chip = this.chips().find((c) => c.start <= this.cursor && this.cursor < c.end);
|
|
122
|
+
const from = chip ? chip.start : this.cursor;
|
|
123
|
+
const to = chip ? chip.end : this.cursor + 1;
|
|
124
|
+
this.value = this.value.slice(0, from) + this.value.slice(to);
|
|
125
|
+
this.cursor = from;
|
|
104
126
|
this.onChange?.(this.value);
|
|
105
127
|
}
|
|
106
128
|
cursorRowCol(lines, cursor) {
|
|
@@ -124,24 +146,60 @@ export class MultilineEditor {
|
|
|
124
146
|
s.text(0, 0, `${DIM}${ph}${RESET}`);
|
|
125
147
|
return;
|
|
126
148
|
}
|
|
149
|
+
const off = hidden ? 1 : 0;
|
|
150
|
+
const chipColor = this.chipColor?.() || CHIP;
|
|
151
|
+
const chips = this.chips()
|
|
152
|
+
.map((c) => ({ start: c.start - off, end: c.end - off }))
|
|
153
|
+
.filter((c) => c.end > 0);
|
|
127
154
|
const lines = value.split('\n');
|
|
128
155
|
const { row: cr, col: cc } = this.cursorRowCol(lines, cursorIdx);
|
|
129
156
|
const height = Math.max(1, s.height);
|
|
130
157
|
const top = cr >= height ? cr - height + 1 : 0;
|
|
158
|
+
const lineStarts = [];
|
|
159
|
+
let offset = 0;
|
|
160
|
+
for (const line of lines) {
|
|
161
|
+
lineStarts.push(offset);
|
|
162
|
+
offset += line.length + 1;
|
|
163
|
+
}
|
|
131
164
|
for (let r = 0; r < height && top + r < lines.length; r++) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
165
|
+
const idx = top + r;
|
|
166
|
+
const line = lines[idx];
|
|
167
|
+
const isCursorRow = idx === cr && s.focused;
|
|
168
|
+
const hscroll = isCursorRow && cc >= width ? cc - width + 1 : 0;
|
|
169
|
+
s.text(0, r, this.renderRow(line, lineStarts[idx], chips, chipColor, hscroll, width, isCursorRow ? cc : null));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
renderRow(line, lineStart, chips, chipColor, hscroll, width, cursorCol) {
|
|
173
|
+
const inChip = (abs) => chips.some((c) => abs >= c.start && abs < c.end);
|
|
174
|
+
let out = '';
|
|
175
|
+
let yellow = false;
|
|
176
|
+
for (let c = hscroll; c < hscroll + width; c++) {
|
|
177
|
+
const isCursor = cursorCol !== null && c === cursorCol;
|
|
178
|
+
const hasChar = c < line.length;
|
|
179
|
+
if (!hasChar && !isCursor)
|
|
180
|
+
break;
|
|
181
|
+
const ch = hasChar ? line[c] : ' ';
|
|
182
|
+
if (isCursor) {
|
|
183
|
+
if (yellow) {
|
|
184
|
+
out += RESET;
|
|
185
|
+
yellow = false;
|
|
186
|
+
}
|
|
187
|
+
out += `${CURSOR}${ch}${RESET}`;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const wantYellow = hasChar && inChip(lineStart + c);
|
|
191
|
+
if (wantYellow && !yellow) {
|
|
192
|
+
out += chipColor;
|
|
193
|
+
yellow = true;
|
|
141
194
|
}
|
|
142
|
-
else {
|
|
143
|
-
|
|
195
|
+
else if (!wantYellow && yellow) {
|
|
196
|
+
out += RESET;
|
|
197
|
+
yellow = false;
|
|
144
198
|
}
|
|
199
|
+
out += ch;
|
|
145
200
|
}
|
|
201
|
+
if (yellow)
|
|
202
|
+
out += RESET;
|
|
203
|
+
return out;
|
|
146
204
|
}
|
|
147
205
|
}
|
package/esm/tui/chat/picker.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export interface Candidate {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function collectCandidates(cwd: string, agentNames: string[]): Candidate[];
|
|
7
7
|
export declare function rank(query: string, candidates: Candidate[], limit?: number): Candidate[];
|
|
8
|
+
export interface MentionRange {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function mentionRanges(value: string, excludeStart?: number): MentionRange[];
|
|
8
13
|
export interface ActiveMention {
|
|
9
14
|
start: number;
|
|
10
15
|
query: string;
|
package/esm/tui/chat/picker.js
CHANGED
|
@@ -169,6 +169,20 @@ export function rank(query, candidates, limit = 8) {
|
|
|
169
169
|
scored.sort((a, b) => b.score - a.score);
|
|
170
170
|
return scored.slice(0, limit).map((entry) => entry.candidate);
|
|
171
171
|
}
|
|
172
|
+
export function mentionRanges(value, excludeStart) {
|
|
173
|
+
const ranges = [];
|
|
174
|
+
const re = /@[^\s]+/g;
|
|
175
|
+
let match;
|
|
176
|
+
while ((match = re.exec(value)) !== null) {
|
|
177
|
+
const start = match.index;
|
|
178
|
+
if (start === excludeStart)
|
|
179
|
+
continue;
|
|
180
|
+
if (start > 0 && !/\s/.test(value[start - 1] ?? ' '))
|
|
181
|
+
continue;
|
|
182
|
+
ranges.push({ start, end: start + match[0].length });
|
|
183
|
+
}
|
|
184
|
+
return ranges;
|
|
185
|
+
}
|
|
172
186
|
export function activeMention(value, cursor) {
|
|
173
187
|
let start = -1;
|
|
174
188
|
for (let i = cursor - 1; i >= 0; i--) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mu-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./script/index.js",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"@swc/wasm-typescript": "^1.15.0",
|
|
24
24
|
"cli-highlight": "^2.1.11",
|
|
25
25
|
"croner": "^9.0.0",
|
|
26
|
-
"mu-core": "^0.
|
|
27
|
-
"mu-tui": "^0.
|
|
26
|
+
"mu-core": "^0.17.1",
|
|
27
|
+
"mu-tui": "^0.17.1"
|
|
28
28
|
},
|
|
29
29
|
"_generatedBy": "dnt@dev",
|
|
30
30
|
"types": "./esm/index.d.ts"
|
|
@@ -54,12 +54,12 @@ export interface ChatHost {
|
|
|
54
54
|
*/
|
|
55
55
|
banner?: string;
|
|
56
56
|
/**
|
|
57
|
-
* Lean input presentation:
|
|
58
|
-
* model/provider/agent footer inside the input
|
|
59
|
-
* the status bar — leaving a
|
|
57
|
+
* Lean input presentation: keep the surface-background input frame but drop
|
|
58
|
+
* the model/provider/agent footer inside the input and the context readout on
|
|
59
|
+
* the status bar — leaving a framed prompt + editor. The {@link ChatHost.banner}
|
|
60
60
|
* splash is independent and still shown if set. Hosts that want the full
|
|
61
|
-
* information-rich input (
|
|
62
|
-
*
|
|
61
|
+
* information-rich input (model · provider · @agent footer, token/context usage
|
|
62
|
+
* in the status line) leave this unset (the default).
|
|
63
63
|
*/
|
|
64
64
|
minimal?: boolean;
|
|
65
65
|
onExit(code: number): void;
|
|
@@ -89,6 +89,8 @@ export declare class ChatApp {
|
|
|
89
89
|
private running;
|
|
90
90
|
private readonly queue;
|
|
91
91
|
private readonly pendingShell;
|
|
92
|
+
private readonly pastes;
|
|
93
|
+
private pasteSeq;
|
|
92
94
|
private models;
|
|
93
95
|
private paletteCursor;
|
|
94
96
|
private paletteDismissedFor;
|
|
@@ -132,8 +134,11 @@ export declare class ChatApp {
|
|
|
132
134
|
private onTurnComplete;
|
|
133
135
|
private stripFileMentions;
|
|
134
136
|
private send;
|
|
137
|
+
private capturePaste;
|
|
138
|
+
private expandPastes;
|
|
135
139
|
private flushShellContext;
|
|
136
140
|
private submit;
|
|
141
|
+
private clearPastes;
|
|
137
142
|
private enqueueFromInput;
|
|
138
143
|
private onInputChange;
|
|
139
144
|
private intercept;
|
|
@@ -148,7 +153,6 @@ export declare class ChatApp {
|
|
|
148
153
|
private pickerVisible;
|
|
149
154
|
private pickerMove;
|
|
150
155
|
private pickerAccept;
|
|
151
|
-
private deleteMention;
|
|
152
156
|
private pushHistory;
|
|
153
157
|
private navigateHistory;
|
|
154
158
|
private newSession;
|
|
@@ -91,6 +91,8 @@ class ChatApp {
|
|
|
91
91
|
running = false;
|
|
92
92
|
queue = [];
|
|
93
93
|
pendingShell = [];
|
|
94
|
+
pastes = new Map();
|
|
95
|
+
pasteSeq = 0;
|
|
94
96
|
models = [];
|
|
95
97
|
paletteCursor = 0;
|
|
96
98
|
paletteDismissedFor = '__none__';
|
|
@@ -135,6 +137,20 @@ class ChatApp {
|
|
|
135
137
|
onSubmit: (value) => this.submit(value),
|
|
136
138
|
onChange: (value) => this.onInputChange(value),
|
|
137
139
|
});
|
|
140
|
+
this.editor.mentionRanges = (value, cursor) => {
|
|
141
|
+
const ranges = (0, picker_js_1.mentionRanges)(value, (0, picker_js_1.activeMention)(value, cursor)?.start);
|
|
142
|
+
for (const placeholder of this.pastes.keys()) {
|
|
143
|
+
for (let i = value.indexOf(placeholder); i !== -1; i = value.indexOf(placeholder, i + placeholder.length)) {
|
|
144
|
+
ranges.push({ start: i, end: i + placeholder.length });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return ranges;
|
|
148
|
+
};
|
|
149
|
+
this.editor.onPaste = (text) => this.capturePaste(text);
|
|
150
|
+
this.editor.chipColor = () => {
|
|
151
|
+
const bg = this.theme().styles.commandPaletteSelected.bg;
|
|
152
|
+
return bg ? (0, theme_js_1.fgToAnsi)(bg) : '';
|
|
153
|
+
};
|
|
138
154
|
this.scroll = (0, mu_tui_1.scrollView)({ render: (s) => (0, transcript_js_1.transcriptComponent)(this.transcript, this.theme()).render(s) }, { stickyHeader: (info) => this.stickyHeaderView(info), footer: () => this.jumpToBottomHint() });
|
|
139
155
|
this.subScroll = (0, mu_tui_1.scrollView)({ render: (s) => (0, transcript_js_1.transcriptComponent)(this.subTranscript, this.theme()).render(s) });
|
|
140
156
|
this.commands = (0, commands_js_1.buildCommands)(this.commandHost()).filter((c) => {
|
|
@@ -146,6 +162,8 @@ class ChatApp {
|
|
|
146
162
|
});
|
|
147
163
|
this.tui.setRoot({ render: (s) => this.root().render(s) });
|
|
148
164
|
this.tui.setBackgroundColor(this.theme().colors.background);
|
|
165
|
+
this.tui.setToastBackground(this.theme().colors.surface);
|
|
166
|
+
this.tui.setToastForeground(this.theme().colors.text);
|
|
149
167
|
this.tui.setFocus(this.editor);
|
|
150
168
|
this.tui.addInputInterceptor((event) => this.intercept(event));
|
|
151
169
|
this.tui.addGlobalKeybinding({ chord: { key: 'c', ctrl: true }, handler: () => this.onCtrlC() });
|
|
@@ -154,6 +172,8 @@ class ChatApp {
|
|
|
154
172
|
this.tui.addGlobalKeybinding({ chord: { key: 'end', ctrl: true }, handler: () => this.jumpToBottom() });
|
|
155
173
|
this.unsubscribeTheme = this.themeProvider.subscribe(() => {
|
|
156
174
|
this.tui.setBackgroundColor(this.theme().colors.background);
|
|
175
|
+
this.tui.setToastBackground(this.theme().colors.surface);
|
|
176
|
+
this.tui.setToastForeground(this.theme().colors.text);
|
|
157
177
|
this.tui.requestRender(true);
|
|
158
178
|
});
|
|
159
179
|
this.bindSession();
|
|
@@ -465,6 +485,23 @@ class ChatApp {
|
|
|
465
485
|
this.showError(err instanceof Error ? err.message : String(err));
|
|
466
486
|
});
|
|
467
487
|
}
|
|
488
|
+
capturePaste(text) {
|
|
489
|
+
const lines = text.split('\n').length;
|
|
490
|
+
if (lines < 2 && text.length <= 200)
|
|
491
|
+
return undefined;
|
|
492
|
+
const id = ++this.pasteSeq;
|
|
493
|
+
const summary = lines > 1 ? `${lines} lines` : `${text.length} chars`;
|
|
494
|
+
const placeholder = `[pasted #${id}, ${summary}]`;
|
|
495
|
+
this.pastes.set(placeholder, text);
|
|
496
|
+
return placeholder;
|
|
497
|
+
}
|
|
498
|
+
expandPastes(text) {
|
|
499
|
+
let out = text;
|
|
500
|
+
for (const [placeholder, content] of this.pastes) {
|
|
501
|
+
out = out.split(placeholder).join(content);
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
468
505
|
flushShellContext(userText) {
|
|
469
506
|
if (this.pendingShell.length === 0)
|
|
470
507
|
return userText;
|
|
@@ -481,35 +518,47 @@ class ChatApp {
|
|
|
481
518
|
if (this.modelPickerOpen || this.sessionPickerOpen)
|
|
482
519
|
return;
|
|
483
520
|
this.clearError();
|
|
484
|
-
this.
|
|
521
|
+
const text = this.expandPastes(trimmed);
|
|
485
522
|
this.editor.setValue('');
|
|
486
|
-
|
|
487
|
-
|
|
523
|
+
this.clearPastes();
|
|
524
|
+
this.pushHistory(text);
|
|
525
|
+
if (text.startsWith('!') || text.startsWith('$')) {
|
|
526
|
+
this.runShell(text.slice(1).trim());
|
|
488
527
|
return;
|
|
489
528
|
}
|
|
490
|
-
if (
|
|
491
|
-
this.runCommand(
|
|
529
|
+
if (text.startsWith('/')) {
|
|
530
|
+
this.runCommand(text);
|
|
492
531
|
return;
|
|
493
532
|
}
|
|
494
|
-
if (this.tryDispatch(
|
|
533
|
+
if (this.tryDispatch(text))
|
|
495
534
|
return;
|
|
496
535
|
if (this.running) {
|
|
497
|
-
this.queue.push(
|
|
536
|
+
this.queue.push(text);
|
|
498
537
|
this.tui.requestRender();
|
|
499
538
|
return;
|
|
500
539
|
}
|
|
501
|
-
this.send(
|
|
540
|
+
this.send(text);
|
|
541
|
+
}
|
|
542
|
+
clearPastes() {
|
|
543
|
+
this.pastes.clear();
|
|
544
|
+
this.pasteSeq = 0;
|
|
502
545
|
}
|
|
503
546
|
enqueueFromInput() {
|
|
504
547
|
const value = this.editor.getValue().trim();
|
|
505
548
|
if (!value)
|
|
506
549
|
return;
|
|
507
|
-
this.
|
|
550
|
+
const text = this.expandPastes(value);
|
|
508
551
|
this.editor.setValue('');
|
|
509
|
-
this.
|
|
552
|
+
this.clearPastes();
|
|
553
|
+
this.pushHistory(text);
|
|
554
|
+
this.queue.push(text);
|
|
510
555
|
this.tui.requestRender();
|
|
511
556
|
}
|
|
512
557
|
onInputChange(value) {
|
|
558
|
+
for (const placeholder of this.pastes.keys()) {
|
|
559
|
+
if (!value.includes(placeholder))
|
|
560
|
+
this.pastes.delete(placeholder);
|
|
561
|
+
}
|
|
513
562
|
if (value !== this.paletteDismissedFor)
|
|
514
563
|
this.paletteDismissedFor = '__none__';
|
|
515
564
|
const items = this.paletteItems();
|
|
@@ -557,11 +606,6 @@ class ChatApp {
|
|
|
557
606
|
}
|
|
558
607
|
if (key === 'escape' || key === 'esc')
|
|
559
608
|
return this.onEscape();
|
|
560
|
-
if (key === 'backspace') {
|
|
561
|
-
if (this.deleteMention())
|
|
562
|
-
return true;
|
|
563
|
-
return false;
|
|
564
|
-
}
|
|
565
609
|
if (key === 'enter' && event.alt) {
|
|
566
610
|
if (this.running) {
|
|
567
611
|
this.enqueueFromInput();
|
|
@@ -719,25 +763,6 @@ class ChatApp {
|
|
|
719
763
|
this.tui.requestRender();
|
|
720
764
|
return true;
|
|
721
765
|
}
|
|
722
|
-
deleteMention() {
|
|
723
|
-
if (this.pickerMention !== undefined)
|
|
724
|
-
return false;
|
|
725
|
-
const value = this.editor.getValue();
|
|
726
|
-
const cursor = this.editor.cursorPos;
|
|
727
|
-
const re = /@[^\s]+/g;
|
|
728
|
-
let match;
|
|
729
|
-
while ((match = re.exec(value)) !== null) {
|
|
730
|
-
const start = match.index;
|
|
731
|
-
const end = start + match[0].length;
|
|
732
|
-
if (cursor > start && cursor <= end) {
|
|
733
|
-
this.editor.setValue(value.slice(0, start) + value.slice(end));
|
|
734
|
-
this.editor.setCursor(start);
|
|
735
|
-
this.tui.requestRender();
|
|
736
|
-
return true;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
return false;
|
|
740
|
-
}
|
|
741
766
|
pushHistory(text) {
|
|
742
767
|
this.host.history?.append(text);
|
|
743
768
|
if (this.history[this.history.length - 1] !== text)
|
|
@@ -1094,8 +1119,6 @@ class ChatApp {
|
|
|
1094
1119
|
}
|
|
1095
1120
|
inputPanel() {
|
|
1096
1121
|
const inner = this.approvalView() ?? this.editorInner();
|
|
1097
|
-
if (this.minimal)
|
|
1098
|
-
return (0, mu_tui_1.box)(inner, { padding: 0 });
|
|
1099
1122
|
return (0, mu_tui_1.box)(inner, { background: this.theme().colors.surface, padding: 1 });
|
|
1100
1123
|
}
|
|
1101
1124
|
editorInner() {
|
|
@@ -5,12 +5,19 @@ export interface MultilineEditorOptions {
|
|
|
5
5
|
onSubmit?: (value: string) => void;
|
|
6
6
|
onChange?: (value: string) => void;
|
|
7
7
|
}
|
|
8
|
+
interface ChipRange {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
8
12
|
export declare class MultilineEditor implements Component {
|
|
9
13
|
private value;
|
|
10
14
|
private cursor;
|
|
11
15
|
private readonly placeholder;
|
|
12
16
|
private readonly maxRows;
|
|
13
17
|
hiddenPrefix: string;
|
|
18
|
+
chipColor?: () => string;
|
|
19
|
+
mentionRanges?: (value: string, cursor: number) => ChipRange[];
|
|
20
|
+
onPaste?: (text: string) => string | undefined;
|
|
14
21
|
onSubmit?: (value: string) => void;
|
|
15
22
|
onChange?: (value: string) => void;
|
|
16
23
|
constructor(opts?: MultilineEditorOptions);
|
|
@@ -24,8 +31,13 @@ export declare class MultilineEditor implements Component {
|
|
|
24
31
|
private lineStart;
|
|
25
32
|
private lineEnd;
|
|
26
33
|
private insert;
|
|
34
|
+
private chips;
|
|
35
|
+
private moveLeft;
|
|
36
|
+
private moveRight;
|
|
27
37
|
private backspace;
|
|
28
38
|
private deleteForward;
|
|
29
39
|
private cursorRowCol;
|
|
30
40
|
render(s: Surface): void;
|
|
41
|
+
private renderRow;
|
|
31
42
|
}
|
|
43
|
+
export {};
|
|
@@ -5,12 +5,16 @@ const mu_tui_1 = require("mu-tui");
|
|
|
5
5
|
const CURSOR = '\x1b[7m';
|
|
6
6
|
const RESET = '\x1b[0m';
|
|
7
7
|
const DIM = '\x1b[2m';
|
|
8
|
+
const CHIP = '\x1b[33m';
|
|
8
9
|
class MultilineEditor {
|
|
9
10
|
value = '';
|
|
10
11
|
cursor = 0;
|
|
11
12
|
placeholder;
|
|
12
13
|
maxRows;
|
|
13
14
|
hiddenPrefix = '';
|
|
15
|
+
chipColor;
|
|
16
|
+
mentionRanges;
|
|
17
|
+
onPaste;
|
|
14
18
|
onSubmit;
|
|
15
19
|
onChange;
|
|
16
20
|
constructor(opts = {}) {
|
|
@@ -39,7 +43,7 @@ class MultilineEditor {
|
|
|
39
43
|
}
|
|
40
44
|
handleInput(event) {
|
|
41
45
|
if (event.type === 'paste') {
|
|
42
|
-
this.insert(event.text);
|
|
46
|
+
this.insert(this.onPaste?.(event.text) ?? event.text);
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
45
49
|
if (event.type === 'text') {
|
|
@@ -65,10 +69,10 @@ class MultilineEditor {
|
|
|
65
69
|
this.deleteForward();
|
|
66
70
|
return;
|
|
67
71
|
case 'left':
|
|
68
|
-
this.
|
|
72
|
+
this.moveLeft();
|
|
69
73
|
return;
|
|
70
74
|
case 'right':
|
|
71
|
-
this.
|
|
75
|
+
this.moveRight();
|
|
72
76
|
return;
|
|
73
77
|
case 'home':
|
|
74
78
|
this.cursor = this.lineStart();
|
|
@@ -93,17 +97,35 @@ class MultilineEditor {
|
|
|
93
97
|
this.cursor += text.length;
|
|
94
98
|
this.onChange?.(this.value);
|
|
95
99
|
}
|
|
100
|
+
chips() {
|
|
101
|
+
return this.mentionRanges ? this.mentionRanges(this.value, this.cursor) : [];
|
|
102
|
+
}
|
|
103
|
+
moveLeft() {
|
|
104
|
+
const chip = this.chips().find((c) => c.start < this.cursor && this.cursor <= c.end);
|
|
105
|
+
this.cursor = chip ? chip.start : Math.max(0, this.cursor - 1);
|
|
106
|
+
}
|
|
107
|
+
moveRight() {
|
|
108
|
+
const chip = this.chips().find((c) => c.start <= this.cursor && this.cursor < c.end);
|
|
109
|
+
this.cursor = chip ? chip.end : Math.min(this.value.length, this.cursor + 1);
|
|
110
|
+
}
|
|
96
111
|
backspace() {
|
|
97
112
|
if (this.cursor === 0)
|
|
98
113
|
return;
|
|
99
|
-
|
|
100
|
-
this.cursor
|
|
114
|
+
const chip = this.chips().find((c) => c.start < this.cursor && this.cursor <= c.end);
|
|
115
|
+
const from = chip ? chip.start : this.cursor - 1;
|
|
116
|
+
const to = chip ? chip.end : this.cursor;
|
|
117
|
+
this.value = this.value.slice(0, from) + this.value.slice(to);
|
|
118
|
+
this.cursor = from;
|
|
101
119
|
this.onChange?.(this.value);
|
|
102
120
|
}
|
|
103
121
|
deleteForward() {
|
|
104
122
|
if (this.cursor >= this.value.length)
|
|
105
123
|
return;
|
|
106
|
-
|
|
124
|
+
const chip = this.chips().find((c) => c.start <= this.cursor && this.cursor < c.end);
|
|
125
|
+
const from = chip ? chip.start : this.cursor;
|
|
126
|
+
const to = chip ? chip.end : this.cursor + 1;
|
|
127
|
+
this.value = this.value.slice(0, from) + this.value.slice(to);
|
|
128
|
+
this.cursor = from;
|
|
107
129
|
this.onChange?.(this.value);
|
|
108
130
|
}
|
|
109
131
|
cursorRowCol(lines, cursor) {
|
|
@@ -127,25 +149,61 @@ class MultilineEditor {
|
|
|
127
149
|
s.text(0, 0, `${DIM}${ph}${RESET}`);
|
|
128
150
|
return;
|
|
129
151
|
}
|
|
152
|
+
const off = hidden ? 1 : 0;
|
|
153
|
+
const chipColor = this.chipColor?.() || CHIP;
|
|
154
|
+
const chips = this.chips()
|
|
155
|
+
.map((c) => ({ start: c.start - off, end: c.end - off }))
|
|
156
|
+
.filter((c) => c.end > 0);
|
|
130
157
|
const lines = value.split('\n');
|
|
131
158
|
const { row: cr, col: cc } = this.cursorRowCol(lines, cursorIdx);
|
|
132
159
|
const height = Math.max(1, s.height);
|
|
133
160
|
const top = cr >= height ? cr - height + 1 : 0;
|
|
161
|
+
const lineStarts = [];
|
|
162
|
+
let offset = 0;
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
lineStarts.push(offset);
|
|
165
|
+
offset += line.length + 1;
|
|
166
|
+
}
|
|
134
167
|
for (let r = 0; r < height && top + r < lines.length; r++) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
const idx = top + r;
|
|
169
|
+
const line = lines[idx];
|
|
170
|
+
const isCursorRow = idx === cr && s.focused;
|
|
171
|
+
const hscroll = isCursorRow && cc >= width ? cc - width + 1 : 0;
|
|
172
|
+
s.text(0, r, this.renderRow(line, lineStarts[idx], chips, chipColor, hscroll, width, isCursorRow ? cc : null));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
renderRow(line, lineStart, chips, chipColor, hscroll, width, cursorCol) {
|
|
176
|
+
const inChip = (abs) => chips.some((c) => abs >= c.start && abs < c.end);
|
|
177
|
+
let out = '';
|
|
178
|
+
let yellow = false;
|
|
179
|
+
for (let c = hscroll; c < hscroll + width; c++) {
|
|
180
|
+
const isCursor = cursorCol !== null && c === cursorCol;
|
|
181
|
+
const hasChar = c < line.length;
|
|
182
|
+
if (!hasChar && !isCursor)
|
|
183
|
+
break;
|
|
184
|
+
const ch = hasChar ? line[c] : ' ';
|
|
185
|
+
if (isCursor) {
|
|
186
|
+
if (yellow) {
|
|
187
|
+
out += RESET;
|
|
188
|
+
yellow = false;
|
|
189
|
+
}
|
|
190
|
+
out += `${CURSOR}${ch}${RESET}`;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const wantYellow = hasChar && inChip(lineStart + c);
|
|
194
|
+
if (wantYellow && !yellow) {
|
|
195
|
+
out += chipColor;
|
|
196
|
+
yellow = true;
|
|
144
197
|
}
|
|
145
|
-
else {
|
|
146
|
-
|
|
198
|
+
else if (!wantYellow && yellow) {
|
|
199
|
+
out += RESET;
|
|
200
|
+
yellow = false;
|
|
147
201
|
}
|
|
202
|
+
out += ch;
|
|
148
203
|
}
|
|
204
|
+
if (yellow)
|
|
205
|
+
out += RESET;
|
|
206
|
+
return out;
|
|
149
207
|
}
|
|
150
208
|
}
|
|
151
209
|
exports.MultilineEditor = MultilineEditor;
|
|
@@ -5,6 +5,11 @@ export interface Candidate {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function collectCandidates(cwd: string, agentNames: string[]): Candidate[];
|
|
7
7
|
export declare function rank(query: string, candidates: Candidate[], limit?: number): Candidate[];
|
|
8
|
+
export interface MentionRange {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function mentionRanges(value: string, excludeStart?: number): MentionRange[];
|
|
8
13
|
export interface ActiveMention {
|
|
9
14
|
start: number;
|
|
10
15
|
query: string;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.collectCandidates = collectCandidates;
|
|
4
4
|
exports.rank = rank;
|
|
5
|
+
exports.mentionRanges = mentionRanges;
|
|
5
6
|
exports.activeMention = activeMention;
|
|
6
7
|
const node_child_process_1 = require("node:child_process");
|
|
7
8
|
const node_fs_1 = require("node:fs");
|
|
@@ -174,6 +175,20 @@ function rank(query, candidates, limit = 8) {
|
|
|
174
175
|
scored.sort((a, b) => b.score - a.score);
|
|
175
176
|
return scored.slice(0, limit).map((entry) => entry.candidate);
|
|
176
177
|
}
|
|
178
|
+
function mentionRanges(value, excludeStart) {
|
|
179
|
+
const ranges = [];
|
|
180
|
+
const re = /@[^\s]+/g;
|
|
181
|
+
let match;
|
|
182
|
+
while ((match = re.exec(value)) !== null) {
|
|
183
|
+
const start = match.index;
|
|
184
|
+
if (start === excludeStart)
|
|
185
|
+
continue;
|
|
186
|
+
if (start > 0 && !/\s/.test(value[start - 1] ?? ' '))
|
|
187
|
+
continue;
|
|
188
|
+
ranges.push({ start, end: start + match[0].length });
|
|
189
|
+
}
|
|
190
|
+
return ranges;
|
|
191
|
+
}
|
|
177
192
|
function activeMention(value, cursor) {
|
|
178
193
|
let start = -1;
|
|
179
194
|
for (let i = cursor - 1; i >= 0; i--) {
|