march-cli 0.1.29 → 0.1.30
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/package.json +1 -1
- package/src/cli/commands/help-command.mjs +1 -1
- package/src/cli/fallback-ui.mjs +0 -2
- package/src/cli/input/autocomplete.mjs +0 -1
- package/src/cli/slash-commands.mjs +5 -6
- package/src/cli/tui/input/mouse-selection-controller.mjs +2 -3
- package/src/cli/tui/layout/main-pane-layout.mjs +3 -2
- package/src/cli/tui/output/selectable-copy.mjs +107 -0
- package/src/cli/tui/output-buffer.mjs +57 -93
- package/src/cli/tui/selection-screen.mjs +30 -0
- package/src/cli/ui.mjs +3 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function formatHelpLines() {
|
|
2
2
|
return [
|
|
3
|
-
"Commands: /new, /exit, /help, /hotkeys, /templates, /export jsonl, /export html, /export gist <jsonl|html>, /settings, /extensions, /providers, /providers <name>, /model, /models, /session, /status, /shell, /shell spawn [name], /save, /name, /copy
|
|
3
|
+
"Commands: /new, /exit, /help, /hotkeys, /templates, /export jsonl, /export html, /export gist <jsonl|html>, /settings, /extensions, /providers, /providers <name>, /model, /models, /session, /status, /shell, /shell spawn [name], /save, /name, /copy",
|
|
4
4
|
"Sessions: /session opens previous sessions and restores the selected one.",
|
|
5
5
|
"Shortcuts: Tab = toggle Do/Discuss, Esc = abort turn, Ctrl+C = abort turn / press twice to exit when idle, Ctrl+O = toggle tool output, Alt+S = shell pane, Alt+N = next shell, Alt+K/J = shell scroll, PageUp/PageDown = output scroll, Ctrl+G = external editor, Shift+Tab = thinking selector, Ctrl+T = thinking selector, Ctrl+L = model selector",
|
|
6
6
|
];
|
package/src/cli/fallback-ui.mjs
CHANGED
|
@@ -58,7 +58,6 @@ export function createJsonUI() {
|
|
|
58
58
|
getInputText: () => "",
|
|
59
59
|
insertTextAtCursor: () => {},
|
|
60
60
|
openExternalEditor: () => {},
|
|
61
|
-
toggleMouse: () => false,
|
|
62
61
|
toggleToolOutput: () => false,
|
|
63
62
|
requestExit: () => {},
|
|
64
63
|
close: () => {},
|
|
@@ -148,7 +147,6 @@ export function createPlainUI() {
|
|
|
148
147
|
getInputText: () => "",
|
|
149
148
|
insertTextAtCursor: () => {},
|
|
150
149
|
openExternalEditor: () => {},
|
|
151
|
-
toggleMouse: () => false,
|
|
152
150
|
toggleToolOutput: () => false,
|
|
153
151
|
requestExit: () => {},
|
|
154
152
|
close: () => {},
|
|
@@ -14,7 +14,6 @@ const MARCH_COMMANDS = [
|
|
|
14
14
|
{ name: "copy", description: "Copy last assistant response to clipboard" },
|
|
15
15
|
{ name: "thinking", description: "Open thinking selector" },
|
|
16
16
|
{ name: "thinking list", description: "List available thinking levels" },
|
|
17
|
-
{ name: "mouse", description: "Toggle mouse wheel and TUI selection copy" },
|
|
18
17
|
{ name: "hotkeys", description: "Show keyboard shortcuts and input prefixes" },
|
|
19
18
|
{ name: "templates", description: "List project prompt templates" },
|
|
20
19
|
{ name: "export jsonl", description: "Export current session turns as JSONL" },
|
|
@@ -105,12 +105,6 @@ export async function handleSlashCommand(trimmed, {
|
|
|
105
105
|
return { handled: true };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
if (trimmed === "/mouse") {
|
|
109
|
-
const on = ui.toggleMouse();
|
|
110
|
-
ui.writeln(on ? "Mouse tracking: ON (wheel scroll and TUI selection copy enabled)" : "Mouse tracking: OFF (native terminal selection enabled)");
|
|
111
|
-
return { handled: true };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
108
|
if (trimmed === "/status") {
|
|
115
109
|
for (const line of statusCommand({
|
|
116
110
|
runner,
|
|
@@ -145,6 +139,11 @@ export async function handleSlashCommand(trimmed, {
|
|
|
145
139
|
return { handled: true };
|
|
146
140
|
}
|
|
147
141
|
|
|
142
|
+
if (trimmed === "/mouse") {
|
|
143
|
+
ui.writeln("Mouse selection is always enabled.");
|
|
144
|
+
return { handled: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
148
147
|
const sessionSourceCommand = await handleSessionSourceCommand(trimmed, {
|
|
149
148
|
ui,
|
|
150
149
|
runner,
|
|
@@ -44,8 +44,7 @@ export function createMouseSelectionController({
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return {
|
|
47
|
-
handleMouseInput(data
|
|
48
|
-
if (!mouseOn) return undefined;
|
|
47
|
+
handleMouseInput(data) {
|
|
49
48
|
const mouse = parseMouseEvent(data);
|
|
50
49
|
if (mouse?.type === "scroll") {
|
|
51
50
|
if (shellDrawer.isVisible?.() && mouse.col > Math.floor((terminal.columns || 80) * 0.64)) {
|
|
@@ -76,7 +75,7 @@ export function createMouseSelectionController({
|
|
|
76
75
|
|
|
77
76
|
handleCopyKey(data) {
|
|
78
77
|
if (data !== "\x03") return undefined;
|
|
79
|
-
const text = selection.text();
|
|
78
|
+
const text = selection.copyText?.() ?? selection.text();
|
|
80
79
|
if (!text) return undefined;
|
|
81
80
|
selection.clear();
|
|
82
81
|
copySelectionText(text);
|
|
@@ -18,11 +18,12 @@ export class MainPaneLayout {
|
|
|
18
18
|
const fixedHeight = statusTopLines.length + editorLines.length + statusBottomLines.length;
|
|
19
19
|
const viewportHeight = Math.max(1, (this.terminal?.rows || 30) - fixedHeight);
|
|
20
20
|
this.output.setViewportHeight(viewportHeight);
|
|
21
|
-
const
|
|
21
|
+
const outputView = this.output.renderSelectable?.(safeWidth) ?? { lines: this.output.render(safeWidth), copyText: null };
|
|
22
|
+
const outputLines = outputView.lines;
|
|
22
23
|
const outputTop = Math.max(0, viewportHeight - outputLines.length);
|
|
23
24
|
const editorTop = viewportHeight + statusTopLines.length;
|
|
24
25
|
this.selection?.setRegions?.([
|
|
25
|
-
{ id: "output", topRow: outputTop, leftCol: 0, width: safeWidth, lines: outputLines },
|
|
26
|
+
{ id: "output", topRow: outputTop, leftCol: 0, width: safeWidth, lines: outputLines, copyText: outputView.copyText },
|
|
26
27
|
{ id: "editor", topRow: editorTop, leftCol: 0, width: safeWidth, lines: editorLines },
|
|
27
28
|
]);
|
|
28
29
|
const selectedOutputLines = this.selection?.applyRegion?.("output", outputLines) ?? outputLines;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import { renderMarkdown } from "../markdown-renderer.mjs";
|
|
4
|
+
|
|
5
|
+
export function appendSelectableEntries(entries, block, lines, width) {
|
|
6
|
+
if (block.type !== "markdown") {
|
|
7
|
+
for (const line of lines) entries.push({ line, source: null, codeSource: null, baseRow: entries.length });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const source = { kind: "markdown", text: block.text, startRow: entries.length, endRow: entries.length + lines.length - 1 };
|
|
11
|
+
const fragmentRanges = renderedFragmentRanges(block.text, width, entries.length);
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
const baseRow = entries.length;
|
|
14
|
+
const fragmentSource = fragmentRanges.find((range) => baseRow >= range.startRow && baseRow <= range.endRow) ?? null;
|
|
15
|
+
const codeSource = fragmentSource?.kind === "code" ? fragmentSource : null;
|
|
16
|
+
entries.push({ line, source, codeSource, fragmentSource, baseRow });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sliceEntriesWithTail(baseEntries, tailLine, range) {
|
|
21
|
+
if (!range) return tailLine == null ? baseEntries : [...baseEntries, { line: tailLine, source: null, codeSource: null, baseRow: baseEntries.length }];
|
|
22
|
+
const { start, end } = range;
|
|
23
|
+
const visible = baseEntries.slice(start, Math.min(end, baseEntries.length));
|
|
24
|
+
if (tailLine != null && end > baseEntries.length) visible.push({ line: tailLine, source: null, codeSource: null, baseRow: baseEntries.length });
|
|
25
|
+
return visible;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function copySourceTextForRange(entries, range) {
|
|
29
|
+
if (!range) return "";
|
|
30
|
+
const selected = trimEmptyBoundaryEntries(entries.slice(range.start.row, range.end.row + 1));
|
|
31
|
+
const codeText = copyCompleteCodeSource(selected, entries, range);
|
|
32
|
+
if (codeText) return codeText;
|
|
33
|
+
const fragmentText = copyCompleteFragmentSource(selected, entries, range);
|
|
34
|
+
if (fragmentText) return fragmentText;
|
|
35
|
+
if (!selected.length || selected.some((entry) => !entry.source)) return "";
|
|
36
|
+
const sources = uniqueSources(selected, "source");
|
|
37
|
+
if (!sources.length || !sources.every((source) => sourceIsFullySelected(source, entries, range, "source"))) return "";
|
|
38
|
+
return sources.map((source) => source.text).join("\n\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function trimEmptyBoundaryEntries(entries) {
|
|
42
|
+
let start = 0;
|
|
43
|
+
let end = entries.length;
|
|
44
|
+
while (start < end && !entries[start].source && stripAnsi(entries[start].line).trim() === "") start += 1;
|
|
45
|
+
while (end > start && !entries[end - 1].source && stripAnsi(entries[end - 1].line).trim() === "") end -= 1;
|
|
46
|
+
return entries.slice(start, end);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function uniqueSources(entries, key) {
|
|
50
|
+
const result = [];
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const source = entry[key];
|
|
53
|
+
if (!source || result.includes(source)) continue;
|
|
54
|
+
result.push(source);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function copyCompleteCodeSource(selected, entries, range) {
|
|
60
|
+
if (!selected.length || selected.some((entry) => !entry.codeSource)) return "";
|
|
61
|
+
const sources = uniqueSources(selected, "codeSource");
|
|
62
|
+
if (sources.length !== 1 || !sourceIsFullySelected(sources[0], entries, range, "codeSource")) return "";
|
|
63
|
+
return sources[0].text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function copyCompleteFragmentSource(selected, entries, range) {
|
|
67
|
+
if (!selected.length || selected.some((entry) => !entry.fragmentSource)) return "";
|
|
68
|
+
const sources = uniqueSources(selected, "fragmentSource");
|
|
69
|
+
if (sources.length !== 1 || !sourceIsFullySelected(sources[0], entries, range, "fragmentSource")) return "";
|
|
70
|
+
return sources[0].text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sourceIsFullySelected(source, entries, range, key) {
|
|
74
|
+
const startIndex = entries.findIndex((entry) => entry[key] === source && entry.baseRow === source.startRow);
|
|
75
|
+
const endIndex = entries.findLastIndex((entry) => entry[key] === source && entry.baseRow === source.endRow);
|
|
76
|
+
if (startIndex < 0 || endIndex < 0) return false;
|
|
77
|
+
if (range.start.row > startIndex || range.end.row < endIndex) return false;
|
|
78
|
+
const lastLine = stripAnsi(entries[endIndex]?.line ?? "");
|
|
79
|
+
const coversStart = range.start.row < startIndex || range.start.col <= 0;
|
|
80
|
+
const coversEnd = range.end.row > endIndex || range.end.col >= visibleWidth(lastLine);
|
|
81
|
+
return coversStart && coversEnd;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderedFragmentRanges(markdown, width, baseRow) {
|
|
85
|
+
let tokens = [];
|
|
86
|
+
try { tokens = marked.lexer(String(markdown ?? "")); } catch { return []; }
|
|
87
|
+
let row = baseRow;
|
|
88
|
+
const ranges = [];
|
|
89
|
+
for (const token of tokens) {
|
|
90
|
+
const raw = token.raw ?? token.text ?? "";
|
|
91
|
+
const lineCount = renderMarkdown(raw, width).length;
|
|
92
|
+
const range = sourceRangeForToken(token, raw, row, lineCount);
|
|
93
|
+
if (range) ranges.push(range);
|
|
94
|
+
row += lineCount;
|
|
95
|
+
}
|
|
96
|
+
return ranges;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sourceRangeForToken(token, raw, row, lineCount) {
|
|
100
|
+
if (token.type === "code") return { kind: "code", text: String(token.text ?? ""), startRow: row, endRow: row + lineCount - 1 };
|
|
101
|
+
if (token.type === "table") return { kind: "table", text: String(raw).trimEnd(), startRow: row, endRow: row + lineCount - 1 };
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stripAnsi(text) {
|
|
106
|
+
return String(text ?? "").replace(/\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g, "");
|
|
107
|
+
}
|
|
@@ -4,12 +4,9 @@ import { renderMarkdown, renderStreamingMarkdown } from "./markdown-renderer.mjs
|
|
|
4
4
|
import { renderEditDiffBlock } from "./tui-diff-rendering.mjs";
|
|
5
5
|
import { OutputScrollState } from "./output/scroll-state.mjs";
|
|
6
6
|
import { appendTextLines, wrapLine } from "./output/text-line-renderer.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { appendSelectableEntries, copySourceTextForRange, sliceEntriesWithTail } from "./output/selectable-copy.mjs";
|
|
8
8
|
|
|
9
9
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
10
|
function currentTextToBlocks(textLines, sealed, cache = null) {
|
|
14
11
|
const blocks = [];
|
|
15
12
|
for (let i = 0; i < textLines.length;) {
|
|
@@ -72,13 +69,11 @@ export class OutputBuffer {
|
|
|
72
69
|
this._activeThinking = null;
|
|
73
70
|
this.overlayStatus = null;
|
|
74
71
|
this.scrollState = new OutputScrollState();
|
|
75
|
-
this._segmentLinesCache = new Map();
|
|
76
72
|
this._baseLinesCache = new Map();
|
|
73
|
+
this._baseEntriesCache = new Map();
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
get scrollOffset() {
|
|
80
|
-
return this.scrollState.offset;
|
|
81
|
-
}
|
|
76
|
+
get scrollOffset() { return this.scrollState.offset; }
|
|
82
77
|
|
|
83
78
|
clear() {
|
|
84
79
|
this.segments = [];
|
|
@@ -90,27 +85,19 @@ export class OutputBuffer {
|
|
|
90
85
|
this._activeThinking = null;
|
|
91
86
|
this.overlayStatus = null;
|
|
92
87
|
this.scrollState.clear();
|
|
93
|
-
this._segmentLinesCache = new Map();
|
|
94
88
|
this._baseLinesCache = new Map();
|
|
89
|
+
this._baseEntriesCache = new Map();
|
|
95
90
|
}
|
|
96
91
|
|
|
97
|
-
write(text) {
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
writeMarkdown(text) {
|
|
102
|
-
this._writeText(text, true);
|
|
103
|
-
}
|
|
92
|
+
write(text) { this._writeText(text, false); }
|
|
93
|
+
writeMarkdown(text) { this._writeText(text, true); }
|
|
104
94
|
|
|
105
95
|
_writeText(text, markdown) {
|
|
106
96
|
this.overlayStatus = null;
|
|
107
97
|
this._invalidateBaseLines();
|
|
108
98
|
const current = this.currentText.at(-1);
|
|
109
|
-
if (current.markdown !== markdown && current.text !== "") {
|
|
110
|
-
|
|
111
|
-
} else {
|
|
112
|
-
current.markdown = markdown;
|
|
113
|
-
}
|
|
99
|
+
if (current.markdown !== markdown && current.text !== "") this.currentText.push({ text: "", markdown });
|
|
100
|
+
else current.markdown = markdown;
|
|
114
101
|
const parts = text.split("\n");
|
|
115
102
|
this.currentText[this.currentText.length - 1].text += parts[0];
|
|
116
103
|
for (let i = 1; i < parts.length; i++) this.currentText.push({ text: parts[i], markdown });
|
|
@@ -134,10 +121,9 @@ export class OutputBuffer {
|
|
|
134
121
|
startThinking() {
|
|
135
122
|
this.overlayStatus = null;
|
|
136
123
|
this._flushText();
|
|
137
|
-
|
|
138
|
-
this.segments.push(
|
|
139
|
-
this.
|
|
140
|
-
this._activeThinking = seg;
|
|
124
|
+
this._activeThinking = { type: "thinking", tokens: 0, content: [] };
|
|
125
|
+
this.segments.push(this._activeThinking);
|
|
126
|
+
this._invalidateBaseLines();
|
|
141
127
|
}
|
|
142
128
|
|
|
143
129
|
appendThinking(text) {
|
|
@@ -151,25 +137,24 @@ export class OutputBuffer {
|
|
|
151
137
|
}
|
|
152
138
|
|
|
153
139
|
endThinking(tokens) {
|
|
154
|
-
if (this._activeThinking)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
140
|
+
if (!this._activeThinking) return;
|
|
141
|
+
this._activeThinking.tokens = tokens;
|
|
142
|
+
this._activeThinking = null;
|
|
143
|
+
this._invalidateBaseLines();
|
|
159
144
|
}
|
|
160
145
|
|
|
161
146
|
addThinkingBlock(tokens, content) {
|
|
162
147
|
this.overlayStatus = null;
|
|
163
148
|
this._flushText();
|
|
164
149
|
this.segments.push({ type: "thinking", tokens, content: content.split("\n") });
|
|
165
|
-
this.
|
|
150
|
+
this._invalidateBaseLines();
|
|
166
151
|
}
|
|
167
152
|
|
|
168
153
|
addBlock(block) {
|
|
169
154
|
this.overlayStatus = null;
|
|
170
155
|
this._flushText();
|
|
171
156
|
this.segments.push(block);
|
|
172
|
-
this.
|
|
157
|
+
this._invalidateBaseLines();
|
|
173
158
|
}
|
|
174
159
|
|
|
175
160
|
setOverlayStatus(lines) {
|
|
@@ -182,14 +167,12 @@ export class OutputBuffer {
|
|
|
182
167
|
this._invalidateBaseLines();
|
|
183
168
|
}
|
|
184
169
|
|
|
185
|
-
sealCurrentText() {
|
|
186
|
-
return this._flushText();
|
|
187
|
-
}
|
|
170
|
+
sealCurrentText() { return this._flushText(); }
|
|
188
171
|
|
|
189
172
|
_flushText() {
|
|
190
173
|
if (this.currentText.length <= 1 && this.currentText[0].text === "") return false;
|
|
191
174
|
this.segments.push(...currentTextToBlocks(this.currentText, true));
|
|
192
|
-
this.
|
|
175
|
+
this._invalidateBaseLines();
|
|
193
176
|
this.currentText = [{ text: "", markdown: false }];
|
|
194
177
|
this.currentTextCache = new Map();
|
|
195
178
|
return true;
|
|
@@ -200,60 +183,44 @@ export class OutputBuffer {
|
|
|
200
183
|
if (text !== undefined) this.spinnerText = text;
|
|
201
184
|
}
|
|
202
185
|
|
|
203
|
-
tick() {
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
getScrollStep() {
|
|
212
|
-
return this.scrollState.getStep();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
getMaxScrollOffset() {
|
|
216
|
-
return this.scrollState.getMaxOffset();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
setViewportHeight(height) {
|
|
220
|
-
this.scrollState.setViewportHeight(height);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
resetScroll() {
|
|
224
|
-
this.scrollState.reset();
|
|
225
|
-
}
|
|
186
|
+
tick() { this.spinnerIdx = (this.spinnerIdx + 1) % SPINNER_FRAMES.length; }
|
|
187
|
+
scroll(delta, options) { return this.scrollState.scroll(delta, options); }
|
|
188
|
+
getScrollStep() { return this.scrollState.getStep(); }
|
|
189
|
+
getMaxScrollOffset() { return this.scrollState.getMaxOffset(); }
|
|
190
|
+
setViewportHeight(height) { this.scrollState.setViewportHeight(height); }
|
|
191
|
+
resetScroll() { this.scrollState.reset(); }
|
|
226
192
|
|
|
227
193
|
setToolCardsExpanded(expanded) {
|
|
228
194
|
let changed = false;
|
|
229
195
|
for (const seg of this.segments) {
|
|
230
|
-
if (seg.type !== "tool-card") continue;
|
|
231
|
-
if (seg.expanded === expanded) continue;
|
|
196
|
+
if (seg.type !== "tool-card" || seg.expanded === expanded) continue;
|
|
232
197
|
seg.expanded = expanded;
|
|
233
198
|
changed = true;
|
|
234
199
|
}
|
|
235
|
-
if (changed) this.
|
|
200
|
+
if (changed) this._invalidateBaseLines();
|
|
236
201
|
return changed;
|
|
237
202
|
}
|
|
238
203
|
|
|
239
|
-
invalidate() {
|
|
240
|
-
this._invalidateSegmentLines();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
_invalidateSegmentLines() {
|
|
244
|
-
this._segmentLinesCache.clear();
|
|
245
|
-
this._invalidateBaseLines();
|
|
246
|
-
}
|
|
204
|
+
invalidate() { this._invalidateBaseLines(); }
|
|
247
205
|
|
|
248
206
|
_invalidateBaseLines() {
|
|
249
207
|
this._baseLinesCache.clear();
|
|
208
|
+
this._baseEntriesCache.clear();
|
|
250
209
|
}
|
|
251
210
|
|
|
252
211
|
render(width) {
|
|
253
|
-
|
|
212
|
+
return this.renderSelectable(width).lines;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
renderSelectable(width) {
|
|
216
|
+
const baseEntries = this._renderBaseEntries(width);
|
|
254
217
|
const tailLine = this.spinning ? this._spinnerLine() : null;
|
|
255
|
-
this.scrollState.setTotalLines(
|
|
256
|
-
|
|
218
|
+
this.scrollState.setTotalLines(baseEntries.length + (tailLine == null ? 0 : 1));
|
|
219
|
+
const entries = sliceEntriesWithTail(baseEntries, tailLine, this.scrollState.sliceRange());
|
|
220
|
+
return {
|
|
221
|
+
lines: entries.map((entry) => entry.line),
|
|
222
|
+
copyText: (range) => copySourceTextForRange(entries, range),
|
|
223
|
+
};
|
|
257
224
|
}
|
|
258
225
|
|
|
259
226
|
_spinnerLine() {
|
|
@@ -263,31 +230,28 @@ export class OutputBuffer {
|
|
|
263
230
|
_renderBaseLines(width) {
|
|
264
231
|
const cached = this._baseLinesCache.get(width);
|
|
265
232
|
if (cached) return cached;
|
|
266
|
-
const lines =
|
|
267
|
-
const dynamicStart = this._cachedSegmentPrefixCount();
|
|
268
|
-
for (const seg of this.segments.slice(dynamicStart)) for (const line of renderBlock(seg, width)) lines.push(line);
|
|
269
|
-
for (const block of currentTextToBlocks(this.currentText, false, this.currentTextCache)) for (const line of renderBlock(block, width)) lines.push(line);
|
|
270
|
-
if (this.overlayStatus) for (const line of renderBlock(this.overlayStatus, width)) lines.push(line);
|
|
233
|
+
const lines = this._renderBaseEntries(width).map((entry) => entry.line);
|
|
271
234
|
this._baseLinesCache.set(width, lines);
|
|
272
235
|
return lines;
|
|
273
236
|
}
|
|
274
237
|
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
for (const line of renderBlock(this.segments[i], width)) lines.push(line);
|
|
283
|
-
}
|
|
284
|
-
this._segmentLinesCache.set(width, { prefixCount, lines });
|
|
285
|
-
return lines;
|
|
238
|
+
_renderBaseEntries(width) {
|
|
239
|
+
const cached = this._baseEntriesCache.get(width);
|
|
240
|
+
if (cached) return cached;
|
|
241
|
+
const entries = [];
|
|
242
|
+
for (const block of this._blocksForRender()) appendBlockEntries(entries, block, width);
|
|
243
|
+
this._baseEntriesCache.set(width, entries);
|
|
244
|
+
return entries;
|
|
286
245
|
}
|
|
287
246
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
247
|
+
_blocksForRender() {
|
|
248
|
+
const blocks = [...this.segments];
|
|
249
|
+
blocks.push(...currentTextToBlocks(this.currentText, false, this.currentTextCache));
|
|
250
|
+
if (this.overlayStatus) blocks.push(this.overlayStatus);
|
|
251
|
+
return blocks;
|
|
292
252
|
}
|
|
293
253
|
}
|
|
254
|
+
|
|
255
|
+
function appendBlockEntries(entries, block, width) {
|
|
256
|
+
appendSelectableEntries(entries, block, renderBlock(block, width), width);
|
|
257
|
+
}
|
|
@@ -73,6 +73,19 @@ export class ScreenSelection {
|
|
|
73
73
|
return hadSelection;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
copyText() {
|
|
77
|
+
const sourceText = this.sourceText();
|
|
78
|
+
return sourceText || this.text();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
sourceText() {
|
|
82
|
+
const range = this.range();
|
|
83
|
+
if (!range) return "";
|
|
84
|
+
const region = this._singleRegionForRange(range);
|
|
85
|
+
if (!region?.copyText) return "";
|
|
86
|
+
return region.copyText(localRange(range, region)) || "";
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
text() {
|
|
77
90
|
const range = this.range();
|
|
78
91
|
if (!range) return "";
|
|
@@ -110,6 +123,15 @@ export class ScreenSelection {
|
|
|
110
123
|
return this._plainLines.get(row);
|
|
111
124
|
}
|
|
112
125
|
|
|
126
|
+
_singleRegionForRange(range) {
|
|
127
|
+
const matches = this.regions.filter((region) => {
|
|
128
|
+
const start = region.docStart;
|
|
129
|
+
const end = region.docStart + region.lines.length - 1;
|
|
130
|
+
return range.start.row >= start && range.end.row <= end;
|
|
131
|
+
});
|
|
132
|
+
return matches.length === 1 ? matches[0] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
113
135
|
range() {
|
|
114
136
|
if (!this.anchor || !this.focus) return null;
|
|
115
137
|
const [start, end] = comparePoints(this.anchor, this.focus) <= 0
|
|
@@ -133,6 +155,14 @@ function normalizeRegion(region, index) {
|
|
|
133
155
|
topRow: Math.max(0, Math.trunc(region.topRow ?? 0)),
|
|
134
156
|
leftCol: Math.max(0, Math.trunc(region.leftCol ?? 0)),
|
|
135
157
|
width,
|
|
158
|
+
copyText: typeof region.copyText === "function" ? region.copyText : null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function localRange(range, region) {
|
|
163
|
+
return {
|
|
164
|
+
start: { row: range.start.row - region.docStart, col: range.start.col },
|
|
165
|
+
end: { row: range.end.row - region.docStart, col: range.end.col },
|
|
136
166
|
};
|
|
137
167
|
}
|
|
138
168
|
|
package/src/cli/ui.mjs
CHANGED
|
@@ -58,7 +58,6 @@ export function createTuiUI({
|
|
|
58
58
|
tui.setFocus(editor);
|
|
59
59
|
|
|
60
60
|
let started = false;
|
|
61
|
-
let mouseOn = true;
|
|
62
61
|
let toolsExpanded = false;
|
|
63
62
|
const activeToolBlocks = [];
|
|
64
63
|
const renderScheduler = createRenderScheduler({ requestRender: () => tui.requestRender() });
|
|
@@ -98,7 +97,7 @@ export function createTuiUI({
|
|
|
98
97
|
function ensureStarted() {
|
|
99
98
|
if (!started) {
|
|
100
99
|
tui.addInputListener((data) => {
|
|
101
|
-
const mouseResult = mouseSelectionController.handleMouseInput(data
|
|
100
|
+
const mouseResult = mouseSelectionController.handleMouseInput(data);
|
|
102
101
|
if (mouseResult) return mouseResult;
|
|
103
102
|
const copyKeyResult = mouseSelectionController.handleCopyKey(data);
|
|
104
103
|
if (copyKeyResult) return copyKeyResult;
|
|
@@ -118,7 +117,7 @@ export function createTuiUI({
|
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
function openExternalEditor() {
|
|
121
|
-
runTuiExternalEditor({ terminal, tui, editor, output, requestRender, mouseOn: () =>
|
|
120
|
+
runTuiExternalEditor({ terminal, tui, editor, output, requestRender, mouseOn: () => true });
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
function toggleToolOutput() {
|
|
@@ -240,18 +239,6 @@ export function createTuiUI({
|
|
|
240
239
|
requestRender();
|
|
241
240
|
},
|
|
242
241
|
|
|
243
|
-
toggleMouse: () => {
|
|
244
|
-
if (mouseOn) {
|
|
245
|
-
terminal.write("\x1b[?1002l\x1b[?1006l");
|
|
246
|
-
mouseOn = false;
|
|
247
|
-
return false;
|
|
248
|
-
} else {
|
|
249
|
-
terminal.write("\x1b[?1002h\x1b[?1006h");
|
|
250
|
-
mouseOn = true;
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
|
|
255
242
|
requestPermission: async ({ toolName, params, category }) => {
|
|
256
243
|
ensureStarted();
|
|
257
244
|
spinnerStatus.stop();
|
|
@@ -281,7 +268,7 @@ export function createTuiUI({
|
|
|
281
268
|
retryStatus.stop();
|
|
282
269
|
if (started) {
|
|
283
270
|
await terminal.drainInput?.();
|
|
284
|
-
|
|
271
|
+
terminal.write("\x1b[?1002l\x1b[?1006l");
|
|
285
272
|
tui.stop();
|
|
286
273
|
terminal.write("\x1b[?1049l");
|
|
287
274
|
}
|