march-cli 0.1.11 → 0.1.13
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/agent/command-exec-tool.mjs +42 -8
- package/src/agent/runner/runner-utils.mjs +6 -0
- package/src/agent/runner.mjs +16 -16
- package/src/agent/runtime/ipc/ipc-peer.mjs +99 -0
- package/src/agent/runtime/ipc/process-ipc-transport.mjs +16 -0
- package/src/agent/runtime/remote-runner-client.mjs +73 -0
- package/src/agent/runtime/remote-ui-client.mjs +19 -0
- package/src/agent/runtime/runner-ipc-target.mjs +126 -0
- package/src/agent/runtime/runner-process-client.mjs +47 -0
- package/src/agent/runtime/runner-process-entry.mjs +93 -0
- package/src/agent/runtime/ui-event-bridge.mjs +85 -0
- package/src/agent/tool-names.mjs +1 -1
- package/src/agent/tool-summary.mjs +112 -0
- package/src/agent/tools.mjs +0 -3
- package/src/agent/turn/turn-events.mjs +46 -0
- package/src/agent/turn/turn-runner.mjs +2 -1
- package/src/cli/commands/copy-command.mjs +16 -2
- package/src/cli/commands/status-command.mjs +7 -4
- package/src/cli/commands/thinking-command.mjs +10 -3
- package/src/cli/repl-loop.mjs +3 -1
- package/src/cli/startup/create-runtime-runner.mjs +61 -0
- package/src/cli/startup/startup-banner.mjs +64 -10
- package/src/cli/tui/layout/main-pane-layout.mjs +16 -7
- package/src/cli/tui/selection-screen.mjs +83 -34
- package/src/cli/tui/status/status-bar.mjs +154 -18
- package/src/cli/tui/syntax/highlighting.mjs +7 -24
- package/src/cli/tui/syntax/languages.mjs +1 -1
- package/src/cli/tui/tool-rendering.mjs +3 -113
- package/src/cli/tui/tui-handlers.mjs +1 -1
- package/src/cli/tui/ui-theme.mjs +14 -5
- package/src/cli/ui.mjs +1 -1
- package/src/context/engine.mjs +10 -9
- package/src/context/profiles.mjs +39 -0
- package/src/main.mjs +35 -29
- package/src/agent/find-tool.mjs +0 -112
- package/src/context/center-memory.mjs +0 -14
|
@@ -9,19 +9,28 @@ export class MainPaneLayout {
|
|
|
9
9
|
|
|
10
10
|
render(width) {
|
|
11
11
|
const safeWidth = Math.max(1, Math.trunc(width));
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
12
|
+
const statusTopLines = this.statusBar.renderTop?.(safeWidth) ?? this.statusBar.render(safeWidth);
|
|
13
|
+
const rawEditorLines = this.editor.render(safeWidth);
|
|
14
|
+
const editorLines = this.statusBar.renderInputLines?.(rawEditorLines, safeWidth)
|
|
15
|
+
?? rawEditorLines.map((line) => this.statusBar.renderInputLine?.(line, safeWidth) ?? line);
|
|
16
|
+
const statusBottomLines = this.statusBar.renderBottom?.(safeWidth) ?? [];
|
|
17
|
+
const fixedHeight = statusTopLines.length + editorLines.length + statusBottomLines.length;
|
|
15
18
|
const viewportHeight = Math.max(1, (this.terminal?.rows || 30) - fixedHeight);
|
|
16
19
|
this.output.setViewportHeight(viewportHeight);
|
|
17
20
|
const outputLines = this.output.render(safeWidth);
|
|
18
21
|
const outputTop = Math.max(0, viewportHeight - outputLines.length);
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
const editorTop = viewportHeight + statusTopLines.length;
|
|
23
|
+
this.selection?.setRegions?.([
|
|
24
|
+
{ id: "output", topRow: outputTop, leftCol: 0, width: safeWidth, lines: outputLines },
|
|
25
|
+
{ id: "editor", topRow: editorTop, leftCol: 0, width: safeWidth, lines: editorLines },
|
|
26
|
+
]);
|
|
27
|
+
const selectedOutputLines = this.selection?.applyRegion?.("output", outputLines) ?? outputLines;
|
|
28
|
+
const selectedEditorLines = this.selection?.applyRegion?.("editor", editorLines) ?? editorLines;
|
|
21
29
|
return [
|
|
22
30
|
...padToHeight(selectedOutputLines, viewportHeight),
|
|
23
|
-
...
|
|
24
|
-
...
|
|
31
|
+
...statusTopLines,
|
|
32
|
+
...selectedEditorLines,
|
|
33
|
+
...statusBottomLines,
|
|
25
34
|
];
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -9,30 +9,37 @@ export class ScreenSelection {
|
|
|
9
9
|
this.active = false;
|
|
10
10
|
this.anchor = null;
|
|
11
11
|
this.focus = null;
|
|
12
|
-
this.
|
|
13
|
-
this._plainLines =
|
|
12
|
+
this.regions = [];
|
|
13
|
+
this._plainLines = new Map();
|
|
14
14
|
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: 0 };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
setLines(lines) {
|
|
18
|
-
this.
|
|
19
|
-
this._plainLines = [];
|
|
20
|
-
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: this.lines.length };
|
|
18
|
+
this.setViewport({ topRow: 0, leftCol: 0, width: Infinity, lines });
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
setViewport({ topRow = 0, leftCol = 0, width = Infinity, lines = [] } = {}) {
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
this.setRegions([{ id: "default", topRow, leftCol, width, lines }]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setRegions(regions = []) {
|
|
26
|
+
let docRow = 0;
|
|
27
|
+
this.regions = regions
|
|
28
|
+
.map((region, index) => normalizeRegion(region, index))
|
|
29
|
+
.filter((region) => region.lines.length > 0)
|
|
30
|
+
.sort((a, b) => a.topRow - b.topRow || a.leftCol - b.leftCol)
|
|
31
|
+
.map((region) => {
|
|
32
|
+
const normalized = { ...region, docStart: docRow };
|
|
33
|
+
docRow += region.lines.length;
|
|
34
|
+
return normalized;
|
|
35
|
+
});
|
|
36
|
+
this.lines = this.regions.flatMap((region) => region.lines);
|
|
37
|
+
this._plainLines = new Map();
|
|
38
|
+
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: this.lines.length };
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
start(point) {
|
|
35
|
-
const normalized = normalizePoint(point, this.
|
|
42
|
+
const normalized = normalizePoint(point, this.regions, true);
|
|
36
43
|
if (!normalized) {
|
|
37
44
|
this.clear();
|
|
38
45
|
return false;
|
|
@@ -45,13 +52,13 @@ export class ScreenSelection {
|
|
|
45
52
|
|
|
46
53
|
update(point) {
|
|
47
54
|
if (!this.active || !this.anchor) return false;
|
|
48
|
-
this.focus = normalizePoint(point, this.
|
|
55
|
+
this.focus = normalizePoint(point, this.regions, true) ?? this.focus;
|
|
49
56
|
return true;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
finish(point, { clear = true } = {}) {
|
|
53
60
|
if (!this.active || !this.anchor) return "";
|
|
54
|
-
this.focus = normalizePoint(point, this.
|
|
61
|
+
this.focus = normalizePoint(point, this.regions, true) ?? this.focus;
|
|
55
62
|
const text = this.text();
|
|
56
63
|
if (clear) this.clear();
|
|
57
64
|
else this.active = false;
|
|
@@ -80,21 +87,27 @@ export class ScreenSelection {
|
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
apply(lines) {
|
|
90
|
+
return this.applyRegion("default", lines);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
applyRegion(id, lines) {
|
|
83
94
|
const range = this.range();
|
|
84
|
-
|
|
95
|
+
const region = this.regions.find((candidate) => candidate.id === id);
|
|
96
|
+
if (!range || !region) return lines;
|
|
85
97
|
return lines.map((line, row) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
98
|
+
const docRow = region.docStart + row;
|
|
99
|
+
if (docRow < range.start.row || docRow > range.end.row) return line;
|
|
100
|
+
const plain = this._plainLine(docRow);
|
|
101
|
+
const startCol = docRow === range.start.row ? range.start.col : 0;
|
|
102
|
+
const endCol = docRow === range.end.row ? range.end.col : visibleWidth(plain);
|
|
90
103
|
if (endCol <= startCol) return line;
|
|
91
104
|
return highlightAnsiLine(line, startCol, endCol);
|
|
92
105
|
});
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
_plainLine(row) {
|
|
96
|
-
if (this._plainLines
|
|
97
|
-
return this._plainLines
|
|
109
|
+
if (!this._plainLines.has(row)) this._plainLines.set(row, stripAnsi(this.lines[row] ?? ""));
|
|
110
|
+
return this._plainLines.get(row);
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
range() {
|
|
@@ -111,19 +124,55 @@ export function stripAnsi(text) {
|
|
|
111
124
|
return String(text ?? "").replace(CONTROL_RE, "");
|
|
112
125
|
}
|
|
113
126
|
|
|
114
|
-
function
|
|
127
|
+
function normalizeRegion(region, index) {
|
|
128
|
+
const lines = [...(region.lines ?? [])];
|
|
129
|
+
const width = Number.isFinite(region.width) ? Math.max(1, Math.trunc(region.width)) : Infinity;
|
|
130
|
+
return {
|
|
131
|
+
id: region.id ?? `region-${index}`,
|
|
132
|
+
lines,
|
|
133
|
+
topRow: Math.max(0, Math.trunc(region.topRow ?? 0)),
|
|
134
|
+
leftCol: Math.max(0, Math.trunc(region.leftCol ?? 0)),
|
|
135
|
+
width,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizePoint({ row, col }, regions, clamp) {
|
|
115
140
|
const screenRow = Math.trunc(row) - 1;
|
|
116
141
|
const screenCol = Math.trunc(col) - 1;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
if (regions.length === 0) return null;
|
|
143
|
+
|
|
144
|
+
for (const region of regions) {
|
|
145
|
+
const localRow = screenRow - region.topRow;
|
|
146
|
+
const localCol = screenCol - region.leftCol;
|
|
147
|
+
const maxCol = Number.isFinite(region.width) ? region.width : Infinity;
|
|
148
|
+
if (localRow >= 0 && localRow < region.lines.length) {
|
|
149
|
+
if (!clamp && (localCol < 0 || localCol > maxCol)) return null;
|
|
150
|
+
return {
|
|
151
|
+
row: region.docStart + localRow,
|
|
152
|
+
col: clampNumber(localCol, 0, maxCol),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!clamp) return null;
|
|
158
|
+
const first = regions[0];
|
|
159
|
+
const last = regions.at(-1);
|
|
160
|
+
if (screenRow < first.topRow) return { row: first.docStart, col: 0 };
|
|
161
|
+
if (screenRow > last.topRow + last.lines.length - 1) {
|
|
162
|
+
return { row: last.docStart + last.lines.length - 1, col: last.width };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let nearest = null;
|
|
166
|
+
for (const region of regions) {
|
|
167
|
+
const beforeDistance = Math.abs(screenRow - region.topRow);
|
|
168
|
+
const afterDistance = Math.abs(screenRow - (region.topRow + region.lines.length - 1));
|
|
169
|
+
const before = { row: region.docStart, col: 0, distance: beforeDistance };
|
|
170
|
+
const after = { row: region.docStart + region.lines.length - 1, col: region.width, distance: afterDistance };
|
|
171
|
+
for (const candidate of [before, after]) {
|
|
172
|
+
if (!nearest || candidate.distance < nearest.distance) nearest = candidate;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return nearest ? { row: nearest.row, col: nearest.col } : null;
|
|
127
176
|
}
|
|
128
177
|
|
|
129
178
|
function comparePoints(a, b) {
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { statusBar, R } from "../ui-theme.mjs";
|
|
2
|
+
import { modeLabel, statusBar, R } from "../ui-theme.mjs";
|
|
3
3
|
|
|
4
4
|
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
5
5
|
const DEFAULT_STATUS_TEXT = "March";
|
|
6
|
+
const DEFAULT_HELP_TEXT = "/ commands · ? help";
|
|
7
|
+
const INPUT_BG = "\x1b[48;2;32;34;38m";
|
|
8
|
+
const INPUT_PROMPT = "▌";
|
|
6
9
|
|
|
7
10
|
export class StatusBar {
|
|
8
|
-
constructor(text = DEFAULT_STATUS_TEXT) {
|
|
11
|
+
constructor(text = DEFAULT_STATUS_TEXT, { cwd = process.cwd(), helpText = DEFAULT_HELP_TEXT } = {}) {
|
|
9
12
|
this.text = normalizeStatusText(text);
|
|
13
|
+
this.cwd = normalizeStatusText(cwd);
|
|
14
|
+
this.helpText = normalizeStatusText(helpText);
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
setText(text) {
|
|
@@ -16,22 +21,135 @@ export class StatusBar {
|
|
|
16
21
|
return true;
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
setCwd(cwd) {
|
|
25
|
+
const next = normalizeStatusText(cwd);
|
|
26
|
+
if (next === this.cwd) return false;
|
|
27
|
+
this.cwd = next;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
invalidate() {}
|
|
20
32
|
|
|
21
33
|
render(width) {
|
|
34
|
+
return this.renderTop(width);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
renderTop(width) {
|
|
22
38
|
if (width <= 0) return [""];
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
return [
|
|
39
|
+
const { left, innerWidth, right } = insetForWidth(width);
|
|
40
|
+
const parts = statusParts(this.text);
|
|
41
|
+
const cwdName = currentDirectoryName(this.cwd);
|
|
42
|
+
const lsp = formatLspStatus(parts.lsp);
|
|
43
|
+
const leftText = [cwdName, lsp, parts.context].filter(Boolean).join(" • ");
|
|
44
|
+
const line = composeMetaLine({ left: leftText, right: "", width: innerWidth });
|
|
45
|
+
return [`${left}${line}${right}`, ""];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
renderInputLines(lines, width) {
|
|
49
|
+
if (width <= 0) return [""];
|
|
50
|
+
const { left, innerWidth, right } = insetForWidth(width);
|
|
51
|
+
const contentLines = lines.filter((line) => !isEditorChromeLine(line));
|
|
52
|
+
const visibleLines = contentLines.length > 0 ? contentLines : [""];
|
|
53
|
+
const paintWidth = inputPaintWidth(innerWidth);
|
|
54
|
+
const inputPadding = `${left}${renderInputPaddingLine(paintWidth)}${right}`;
|
|
55
|
+
const inputContent = visibleLines.map((line, index) =>
|
|
56
|
+
`${left}${this.renderInputLine(line, innerWidth, { isFirst: index === 0 })}${right}`,
|
|
57
|
+
);
|
|
58
|
+
return [inputPadding, ...inputContent, inputPadding];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
renderInputLine(line, width, { isFirst = true } = {}) {
|
|
62
|
+
if (width <= 0) return "";
|
|
63
|
+
const paintWidth = inputPaintWidth(width);
|
|
64
|
+
const prompt = isFirst ? statusBar.prompt(INPUT_PROMPT) : " ";
|
|
65
|
+
const promptWidth = visibleWidth(stripAnsi(INPUT_PROMPT));
|
|
66
|
+
const maxContentWidth = Math.max(0, paintWidth - promptWidth - 2);
|
|
67
|
+
const content = clipToWidth(line, maxContentWidth);
|
|
68
|
+
return applyInputBackground(padToWidth(`${prompt}${content}`, paintWidth));
|
|
30
69
|
}
|
|
70
|
+
|
|
71
|
+
renderBottom(width) {
|
|
72
|
+
if (width <= 0) return [""];
|
|
73
|
+
const { left: insetLeft, innerWidth, right: insetRight } = insetForWidth(width);
|
|
74
|
+
const parts = statusParts(this.text);
|
|
75
|
+
const mode = formatModeLabel(parts.mode || DEFAULT_STATUS_TEXT);
|
|
76
|
+
const activity = parts.activity ? statusBar.muted(`${parts.activity} · `) : "";
|
|
77
|
+
const right = [parts.model, parts.thinking].filter(Boolean).join(" • ");
|
|
78
|
+
const line = composeMetaLine({ left: `${activity}${mode}`, right, width: innerWidth, muteLeft: false });
|
|
79
|
+
return ["", `${insetLeft}${line}${insetRight}`];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function insetForWidth(width) {
|
|
84
|
+
const safeWidth = Math.max(1, Math.trunc(width));
|
|
85
|
+
return { left: "", innerWidth: safeWidth, right: "" };
|
|
31
86
|
}
|
|
32
87
|
|
|
33
|
-
function
|
|
34
|
-
|
|
88
|
+
function composeMetaLine({ left, right, width, muteLeft = true }) {
|
|
89
|
+
const safeWidth = Math.max(1, Math.trunc(width));
|
|
90
|
+
const rightWidth = visibleWidth(stripAnsi(right));
|
|
91
|
+
const colorLeft = (text) => (muteLeft ? statusBar.muted(text) : text);
|
|
92
|
+
if (!right) return colorLeft(padToWidth(clipToWidth(left, safeWidth), safeWidth));
|
|
93
|
+
if (rightWidth >= safeWidth) return statusBar.muted(padToWidth(clipToWidth(right, safeWidth), safeWidth));
|
|
94
|
+
|
|
95
|
+
const maxLeftWidth = Math.max(0, safeWidth - rightWidth - 1);
|
|
96
|
+
const fittedLeft = maxLeftWidth > 0 ? clipToWidth(left, maxLeftWidth) : "";
|
|
97
|
+
const gap = Math.max(1, safeWidth - visibleWidth(stripAnsi(fittedLeft)) - rightWidth);
|
|
98
|
+
return `${colorLeft(fittedLeft)}${" ".repeat(gap)}${statusBar.muted(right)}${R}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderInputPaddingLine(width) {
|
|
102
|
+
return applyInputBackground(" ".repeat(Math.max(1, Math.trunc(width))));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function inputPaintWidth(width) {
|
|
106
|
+
const safeWidth = Math.max(1, Math.trunc(width));
|
|
107
|
+
return safeWidth > 1 ? safeWidth - 1 : safeWidth;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function applyInputBackground(line) {
|
|
111
|
+
return `${INPUT_BG}${String(line).replaceAll(R, `${R}${INPUT_BG}`)}${R}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isEditorChromeLine(line) {
|
|
115
|
+
const plain = stripAnsi(line).trim();
|
|
116
|
+
return plain.length > 0 && (/^─+$/.test(plain) || /^─+\s[↑↓].*more\s─*$/.test(plain));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function currentDirectoryName(path) {
|
|
120
|
+
const normalized = normalizeStatusText(path);
|
|
121
|
+
const parts = normalized.split(/[\\/]+/).filter(Boolean);
|
|
122
|
+
return parts.at(-1) || normalized || DEFAULT_STATUS_TEXT;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatLspStatus(lsp) {
|
|
126
|
+
if (!lsp) return "LSP off";
|
|
127
|
+
const server = lsp.replace(/^lsp:/, "").replace(/[✓✗]$/u, "").trim();
|
|
128
|
+
return server ? `LSP [${server}]` : "LSP off";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatModeLabel(mode) {
|
|
132
|
+
const label = normalizeStatusText(mode);
|
|
133
|
+
const color = modeLabel[label.toLowerCase()] || modeLabel.fallback;
|
|
134
|
+
return color(label);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function statusParts(text) {
|
|
138
|
+
const segments = plainSegments(text);
|
|
139
|
+
const runtime = segments.find((segment) => segment.includes("·")) || "";
|
|
140
|
+
const [model = "", thinking = ""] = runtime.split("·").map((part) => part.trim());
|
|
141
|
+
const lsp = segments.find((segment) => segment.startsWith("lsp:")) || "";
|
|
142
|
+
const context = [...segments].reverse().find((segment) => /^\d+(?:\.\d+)?[KM]?$/.test(segment)) || "";
|
|
143
|
+
const activity = segments.find((segment) => /(?:Working|Aborted)$/.test(segment)) || "";
|
|
144
|
+
const mode = segments.find((segment) => segment && segment !== runtime && segment !== lsp && segment !== activity && segment !== context) || segments[0] || "";
|
|
145
|
+
return { mode, model, thinking, lsp, activity, context };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function plainSegments(text) {
|
|
149
|
+
return stripAnsi(normalizeStatusText(text))
|
|
150
|
+
.split(" | ")
|
|
151
|
+
.map((segment) => segment.trim())
|
|
152
|
+
.filter(Boolean);
|
|
35
153
|
}
|
|
36
154
|
|
|
37
155
|
export function normalizeStatusText(text) {
|
|
@@ -64,15 +182,15 @@ export function fitStatusText(text, width) {
|
|
|
64
182
|
}
|
|
65
183
|
|
|
66
184
|
export function clipToWidth(text, width) {
|
|
67
|
-
// For ANSI-containing text, build output character by character and measure plain width
|
|
68
185
|
let output = "";
|
|
69
186
|
let plainWidth = 0;
|
|
70
|
-
|
|
71
|
-
for (
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
187
|
+
const chars = Array.from(String(text || ""));
|
|
188
|
+
for (let index = 0; index < chars.length; index += 1) {
|
|
189
|
+
const ch = chars[index];
|
|
190
|
+
if (ch === "\x1b") {
|
|
191
|
+
const { sequence, nextIndex } = readAnsiSequence(chars, index);
|
|
192
|
+
output += sequence;
|
|
193
|
+
index = nextIndex;
|
|
76
194
|
continue;
|
|
77
195
|
}
|
|
78
196
|
const charWidth = visibleWidth(ch);
|
|
@@ -83,6 +201,24 @@ export function clipToWidth(text, width) {
|
|
|
83
201
|
return output;
|
|
84
202
|
}
|
|
85
203
|
|
|
204
|
+
function readAnsiSequence(chars, startIndex) {
|
|
205
|
+
let sequence = chars[startIndex];
|
|
206
|
+
let index = startIndex;
|
|
207
|
+
const intro = chars[startIndex + 1];
|
|
208
|
+
if (!intro) return { sequence, nextIndex: index };
|
|
209
|
+
sequence += intro;
|
|
210
|
+
index += 1;
|
|
211
|
+
if (intro !== "[") return { sequence, nextIndex: index };
|
|
212
|
+
|
|
213
|
+
while (index + 1 < chars.length) {
|
|
214
|
+
index += 1;
|
|
215
|
+
const ch = chars[index];
|
|
216
|
+
sequence += ch;
|
|
217
|
+
if (/[\x40-\x7e]/.test(ch)) break;
|
|
218
|
+
}
|
|
219
|
+
return { sequence, nextIndex: index };
|
|
220
|
+
}
|
|
221
|
+
|
|
86
222
|
function stripAnsi(text) {
|
|
87
223
|
return String(text ?? "").replace(ANSI_RE, "");
|
|
88
224
|
}
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
} from "./languages.mjs";
|
|
20
20
|
|
|
21
21
|
const RESOURCE_DIR = join(dirname(fileURLToPath(import.meta.url)), "tree-sitter");
|
|
22
|
-
const ENCODER = new TextEncoder();
|
|
23
22
|
|
|
24
23
|
let initPromise;
|
|
25
24
|
let initialized = false;
|
|
@@ -87,11 +86,10 @@ function treeSitterRuns(text, lang) {
|
|
|
87
86
|
if (!initialized || !parser || !text) return null;
|
|
88
87
|
try {
|
|
89
88
|
const tree = parser.parse(text);
|
|
90
|
-
const byteToIndex = buildByteToIndex(text);
|
|
91
89
|
const scopes = Array.from({ length: text.length }, () => ({ scope: "default", priority: 0 }));
|
|
92
90
|
const query = queries.get(lang);
|
|
93
|
-
if (query) applyQueryScopes(query, tree.rootNode,
|
|
94
|
-
collectNodeScopes(tree.rootNode,
|
|
91
|
+
if (query) applyQueryScopes(query, tree.rootNode, scopes);
|
|
92
|
+
collectNodeScopes(tree.rootNode, scopes);
|
|
95
93
|
return scopesToRuns(text, scopes);
|
|
96
94
|
} catch {
|
|
97
95
|
return null;
|
|
@@ -109,11 +107,11 @@ function loadHighlightQuery(language, queryFile) {
|
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
function applyQueryScopes(query, rootNode,
|
|
110
|
+
function applyQueryScopes(query, rootNode, scopes) {
|
|
113
111
|
for (const capture of query.captures(rootNode)) {
|
|
114
112
|
const scope = captureScope(capture.name);
|
|
115
113
|
if (!scope) continue;
|
|
116
|
-
applyScope(scopes,
|
|
114
|
+
applyScope(scopes, capture.node.startIndex, capture.node.endIndex, scope);
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
|
|
@@ -135,10 +133,10 @@ function captureScope(name) {
|
|
|
135
133
|
return null;
|
|
136
134
|
}
|
|
137
135
|
|
|
138
|
-
function collectNodeScopes(node,
|
|
136
|
+
function collectNodeScopes(node, scopes) {
|
|
139
137
|
const scope = classifyNode(node);
|
|
140
|
-
if (scope) applyScope(scopes,
|
|
141
|
-
for (const child of node.children ?? []) collectNodeScopes(child,
|
|
138
|
+
if (scope) applyScope(scopes, node.startIndex, node.endIndex, scope);
|
|
139
|
+
for (const child of node.children ?? []) collectNodeScopes(child, scopes);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
function classifyNode(node) {
|
|
@@ -259,19 +257,4 @@ export function styleSyntax(text, scope = "default", bg = "") {
|
|
|
259
257
|
return `\x1b[${codes.join(";")}m${text}${R}`;
|
|
260
258
|
}
|
|
261
259
|
|
|
262
|
-
function buildByteToIndex(text) {
|
|
263
|
-
const map = [];
|
|
264
|
-
let byte = 0;
|
|
265
|
-
for (let index = 0; index < text.length;) {
|
|
266
|
-
const codePoint = text.codePointAt(index);
|
|
267
|
-
const char = String.fromCodePoint(codePoint);
|
|
268
|
-
const bytes = ENCODER.encode(char).length;
|
|
269
|
-
for (let i = 0; i < bytes; i++) map[byte + i] = index;
|
|
270
|
-
byte += bytes;
|
|
271
|
-
index += char.length;
|
|
272
|
-
}
|
|
273
|
-
map[byte] = text.length;
|
|
274
|
-
return map;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
260
|
void initializeTreeSitterHighlighting();
|
|
@@ -25,7 +25,7 @@ export const LANG_ALIASES = new Map([
|
|
|
25
25
|
["cs", "csharp"], ["css", "css"], ["cts", "typescript"], ["csharp", "csharp"], ["cxx", "cpp"],
|
|
26
26
|
["diff", "diff"], ["go", "go"], ["h", "c"], ["hh", "cpp"], ["htm", "html"], ["html", "html"],
|
|
27
27
|
["hpp", "cpp"], ["hxx", "cpp"], ["java", "java"], ["javascript", "javascript"], ["js", "javascript"],
|
|
28
|
-
["json", "json"], ["jsonc", "json"], ["jsx", "
|
|
28
|
+
["json", "json"], ["jsonc", "json"], ["jsx", "tsx"], ["mjs", "javascript"], ["mts", "typescript"],
|
|
29
29
|
["patch", "diff"], ["php", "php"], ["py", "python"], ["python", "python"], ["rb", "ruby"],
|
|
30
30
|
["rs", "rust"], ["ruby", "ruby"], ["rust", "rust"], ["sh", "bash"], ["toml", "toml"],
|
|
31
31
|
["ts", "typescript"], ["tsx", "tsx"], ["typescript", "typescript"], ["yaml", "yaml"], ["yml", "yaml"],
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { extractToolOutput } from "../tool-output.mjs";
|
|
2
|
+
import { formatToolStartLine, formatToolSuccessSummary } from "../../agent/tool-summary.mjs";
|
|
2
3
|
import { dim, red } from "./ui-theme.mjs";
|
|
3
4
|
|
|
5
|
+
export { formatToolStartLine, formatToolSuccessSummary } from "../../agent/tool-summary.mjs";
|
|
6
|
+
|
|
4
7
|
const TOOL_BODY_LIMIT = 40;
|
|
5
8
|
const TOOL_ERROR_LIMIT = 6;
|
|
6
9
|
|
|
@@ -44,58 +47,6 @@ export function writeToolEnd({
|
|
|
44
47
|
return true;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
export function formatToolStartLine(name, args = {}) {
|
|
48
|
-
if (name === "edit_file") {
|
|
49
|
-
const path = compactPath(args?.path ?? "");
|
|
50
|
-
const editCount = Array.isArray(args?.edits) ? args.edits.length : 0;
|
|
51
|
-
const mode = args?.mode ?? "patch";
|
|
52
|
-
const summary = mode === "patch" ? `${editCount} edit${editCount === 1 ? "" : "s"}` : mode;
|
|
53
|
-
return joinToolParts("◆", name, [path, summary]);
|
|
54
|
-
}
|
|
55
|
-
if (name === "command_exec") return joinToolParts("◆", name, [compactText(args?.command ?? "")]);
|
|
56
|
-
if (name === "terminal_send") return joinToolParts("◆", name, [args?.shell_id, formatTerminalSendAction(args)]);
|
|
57
|
-
if (name?.startsWith?.("terminal_")) return joinToolParts("◆", name, [args?.shell_id, formatTerminalDetails(args)]);
|
|
58
|
-
if (name === "external_web_search") return joinToolParts("◆", name, [quoteCompact(args?.query ?? "")]);
|
|
59
|
-
if (name === "web_fetch") return joinToolParts("◆", name, [compactText(args?.url ?? "")]);
|
|
60
|
-
if (name === "context_stats") return joinToolParts("◆", name, []);
|
|
61
|
-
if (name === "read") {
|
|
62
|
-
const path = compactPath(args?.path ?? args?.filePath ?? "");
|
|
63
|
-
return joinToolParts("→", name, [path, formatReadRange(args)]);
|
|
64
|
-
}
|
|
65
|
-
if (name === "grep") {
|
|
66
|
-
const path = compactPath(args?.path ?? "");
|
|
67
|
-
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
68
|
-
}
|
|
69
|
-
if (name === "glob") {
|
|
70
|
-
const path = compactPath(args?.path ?? "");
|
|
71
|
-
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
72
|
-
}
|
|
73
|
-
if (name === "find") {
|
|
74
|
-
const path = compactPath(args?.path ?? "");
|
|
75
|
-
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
76
|
-
}
|
|
77
|
-
return joinToolParts("◆", name, [formatSmallOptions(args)]);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function formatToolSuccessSummary(name, result, out = "") {
|
|
81
|
-
if (name === "grep") {
|
|
82
|
-
const matches = result?.details?.results?.length ?? countMatchLines(out);
|
|
83
|
-
return `${matches} match${matches === 1 ? "" : "es"}`;
|
|
84
|
-
}
|
|
85
|
-
if (name === "glob") {
|
|
86
|
-
const matches = Array.isArray(result?.details?.matches) ? result.details.matches.length : countNonEmptyLines(out);
|
|
87
|
-
return `${matches} file${matches === 1 ? "" : "s"}`;
|
|
88
|
-
}
|
|
89
|
-
if (name === "find") {
|
|
90
|
-
const matches = result?.details?.count ?? countNonEmptyLines(out);
|
|
91
|
-
return `${matches} file${matches === 1 ? "" : "s"}`;
|
|
92
|
-
}
|
|
93
|
-
if (name === "memory_open") {
|
|
94
|
-
return compactText(result?.details?.entry?.name ?? compactPath(result?.details?.path ?? ""));
|
|
95
|
-
}
|
|
96
|
-
return "";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
50
|
function formatToolEndCard({ name, isError, result, extractToolOutputImpl }) {
|
|
100
51
|
const out = extractToolOutputImpl(result);
|
|
101
52
|
if (isError) {
|
|
@@ -134,64 +85,3 @@ function writeStructuredLines(output, block) {
|
|
|
134
85
|
for (const line of lines) output.writeln(line);
|
|
135
86
|
}
|
|
136
87
|
}
|
|
137
|
-
|
|
138
|
-
function joinToolParts(icon, name, parts) {
|
|
139
|
-
const clean = parts.map((part) => String(part ?? "").trim()).filter(Boolean);
|
|
140
|
-
return `${icon} ${name}${clean.length ? ` · ${clean.join(" · ")}` : ""}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function formatReadRange(args = {}) {
|
|
144
|
-
if (args.offset == null && args.limit == null) return "";
|
|
145
|
-
if (args.offset != null && args.limit != null) return `lines ${args.offset}-${Number(args.offset) + Number(args.limit) - 1}`;
|
|
146
|
-
if (args.offset != null) return `from line ${args.offset}`;
|
|
147
|
-
return `limit ${args.limit}`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function formatTerminalSendAction(args = {}) {
|
|
151
|
-
const hasText = typeof args.text === "string" && args.text.length > 0;
|
|
152
|
-
const key = args.key ? String(args.key) : "";
|
|
153
|
-
if (hasText && key) return `text+${key}`;
|
|
154
|
-
if (hasText) return args.text.includes("\n") || args.text.includes("\r") ? "text+enter" : "text";
|
|
155
|
-
return key || "send";
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function formatTerminalDetails(args = {}) {
|
|
159
|
-
const details = [];
|
|
160
|
-
if (args.pattern) details.push(quoteCompact(args.pattern));
|
|
161
|
-
if (args.cols && args.rows) details.push(`${args.cols}x${args.rows}`);
|
|
162
|
-
if (args.command) details.push(compactText(args.command));
|
|
163
|
-
return details.join(" · ");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function formatSmallOptions(args = {}) {
|
|
167
|
-
const parts = [];
|
|
168
|
-
for (const [key, value] of Object.entries(args ?? {})) {
|
|
169
|
-
if (value == null || typeof value === "object") continue;
|
|
170
|
-
parts.push(`${key}=${compactText(value)}`);
|
|
171
|
-
if (parts.length >= 2) break;
|
|
172
|
-
}
|
|
173
|
-
return parts.join(", ");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function compactPath(path) {
|
|
177
|
-
return String(path ?? "").split(/[/\\]/).filter(Boolean).slice(-4).join("\\");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function quoteCompact(value) {
|
|
181
|
-
return JSON.stringify(compactText(value));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function compactText(value, limit = 80) {
|
|
185
|
-
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
186
|
-
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function countMatchLines(text) {
|
|
190
|
-
const match = String(text ?? "").match(/(\d+)\s+matches?\b/i);
|
|
191
|
-
if (match) return Number(match[1]);
|
|
192
|
-
return countNonEmptyLines(text);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function countNonEmptyLines(text) {
|
|
196
|
-
return String(text ?? "").split("\n").filter(Boolean).length;
|
|
197
|
-
}
|
|
@@ -56,7 +56,7 @@ export function wireTuiHandlers({
|
|
|
56
56
|
ui.writeln(brightBlack(`● thinking: unchanged`));
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
ui.writeln(brightBlack(`● thinking: ${runner.setThinkingLevel(item.level)}`));
|
|
59
|
+
ui.writeln(brightBlack(`● thinking: ${await runner.setThinkingLevel(item.level)}`));
|
|
60
60
|
refreshStatusBar();
|
|
61
61
|
} catch (err) {
|
|
62
62
|
ui.writeln(`Error: ${err.message}`);
|