march-cli 0.1.10 → 0.1.12
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/tool-names.mjs +1 -1
- package/src/agent/tools.mjs +0 -3
- package/src/cli/commands/status-command.mjs +1 -12
- package/src/cli/shell/shell-drawer.mjs +1 -1
- package/src/cli/tui/output/visible-lines.mjs +8 -0
- package/src/cli/tui/output-buffer.mjs +30 -21
- package/src/cli/tui/render/stream-delta-buffer.mjs +46 -0
- package/src/cli/tui/selection-screen.mjs +12 -4
- package/src/cli/tui/syntax/highlighting.mjs +7 -24
- package/src/cli/tui/syntax/languages.mjs +1 -1
- package/src/cli/ui.mjs +16 -17
- package/src/context/system-core/base.md +1 -1
- package/src/shell/runtime.mjs +9 -1
- package/src/agent/find-tool.mjs +0 -112
package/package.json
CHANGED
package/src/agent/tool-names.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MARCH_BASE_TOOL_NAMES = ["grep", "ls"];
|
|
1
|
+
export const MARCH_BASE_TOOL_NAMES = ["grep", "find", "ls"];
|
package/src/agent/tools.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { Type } from "typebox";
|
|
|
3
3
|
import { createCommandExecTool } from "./command-exec-tool.mjs";
|
|
4
4
|
import { createContextStatsTool } from "./context-stats-tool.mjs";
|
|
5
5
|
import { createEditFileTool } from "./file-edit-tool.mjs";
|
|
6
|
-
import { createFindTool } from "./find-tool.mjs";
|
|
7
6
|
import { createReadFileTool } from "./read-file-tool.mjs";
|
|
8
7
|
import { toolText } from "./tool-result.mjs";
|
|
9
8
|
import { createShellTools } from "../shell/tools.mjs";
|
|
@@ -14,7 +13,6 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
|
|
|
14
13
|
const commandExecTool = createCommandExecTool({ cwd });
|
|
15
14
|
const contextStatsTool = createContextStatsTool({ engine });
|
|
16
15
|
const editFileTool = createEditFileTool({ engine, ui, lspService });
|
|
17
|
-
const findTool = createFindTool({ cwd });
|
|
18
16
|
const readFileTool = createReadFileTool({ engine });
|
|
19
17
|
|
|
20
18
|
const tools = [
|
|
@@ -22,7 +20,6 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
|
|
|
22
20
|
contextStatsTool,
|
|
23
21
|
commandExecTool,
|
|
24
22
|
editFileTool,
|
|
25
|
-
findTool,
|
|
26
23
|
...createShellTools(shellRuntime),
|
|
27
24
|
...memoryTools,
|
|
28
25
|
...mcpTools,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { MODES, formatModeLabel } from "../input/mode-state.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { PREFIX, R } from "../tui/ui-theme.mjs";
|
|
4
4
|
|
|
5
5
|
export function statusCommand({
|
|
6
6
|
runner,
|
|
@@ -23,11 +23,6 @@ export function statusCommand({
|
|
|
23
23
|
|
|
24
24
|
export function statusBarLine({
|
|
25
25
|
runner,
|
|
26
|
-
sessionState,
|
|
27
|
-
sessionSource = "pi",
|
|
28
|
-
extensionDiagnostics = [],
|
|
29
|
-
lifecycleState = null,
|
|
30
|
-
gitBranch = getGitBranch(runner.engine.cwd),
|
|
31
26
|
mode = MODES.DO,
|
|
32
27
|
contextTokens = null,
|
|
33
28
|
activity = null,
|
|
@@ -35,12 +30,6 @@ export function statusBarLine({
|
|
|
35
30
|
}) {
|
|
36
31
|
return formatStatusBarLine({
|
|
37
32
|
engine: runner.engine,
|
|
38
|
-
sessionState,
|
|
39
|
-
sessionStats: runner.getSessionStats?.() ?? null,
|
|
40
|
-
sessionSource,
|
|
41
|
-
extensionDiagnostics,
|
|
42
|
-
lifecycleState,
|
|
43
|
-
gitBranch,
|
|
44
33
|
mode,
|
|
45
34
|
contextTokens,
|
|
46
35
|
activity,
|
|
@@ -137,7 +137,7 @@ export class ShellDrawer {
|
|
|
137
137
|
|
|
138
138
|
getOutputLines(shellId = this.getSelectedShell()?.id) {
|
|
139
139
|
if (!shellId || !this.shellRuntime) return [];
|
|
140
|
-
const snapshot = this.shellRuntime.snapshotShell(shellId);
|
|
140
|
+
const snapshot = this.shellRuntime.snapshotShellScreen?.(shellId) ?? this.shellRuntime.snapshotShell(shellId);
|
|
141
141
|
return formatAnsiLines(snapshot);
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function sliceLinesWithTail(baseLines, tailLine, range) {
|
|
2
|
+
if (!range) return tailLine == null ? baseLines : [...baseLines, tailLine];
|
|
3
|
+
|
|
4
|
+
const { start, end } = range;
|
|
5
|
+
const visible = baseLines.slice(start, Math.min(end, baseLines.length));
|
|
6
|
+
if (tailLine != null && end > baseLines.length) visible.push(tailLine);
|
|
7
|
+
return visible;
|
|
8
|
+
}
|
|
@@ -4,6 +4,7 @@ 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 { sliceLinesWithTail } from "./output/visible-lines.mjs";
|
|
7
8
|
|
|
8
9
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
10
|
|
|
@@ -72,6 +73,7 @@ export class OutputBuffer {
|
|
|
72
73
|
this.overlayStatus = null;
|
|
73
74
|
this.scrollState = new OutputScrollState();
|
|
74
75
|
this._segmentLinesCache = new Map();
|
|
76
|
+
this._baseLinesCache = new Map();
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
get scrollOffset() {
|
|
@@ -89,6 +91,7 @@ export class OutputBuffer {
|
|
|
89
91
|
this.overlayStatus = null;
|
|
90
92
|
this.scrollState.clear();
|
|
91
93
|
this._segmentLinesCache = new Map();
|
|
94
|
+
this._baseLinesCache = new Map();
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
write(text) {
|
|
@@ -101,6 +104,7 @@ export class OutputBuffer {
|
|
|
101
104
|
|
|
102
105
|
_writeText(text, markdown) {
|
|
103
106
|
this.overlayStatus = null;
|
|
107
|
+
this._invalidateBaseLines();
|
|
104
108
|
const current = this.currentText.at(-1);
|
|
105
109
|
if (current.markdown !== markdown && current.text !== "") {
|
|
106
110
|
this.currentText.push({ text: "", markdown });
|
|
@@ -114,6 +118,7 @@ export class OutputBuffer {
|
|
|
114
118
|
|
|
115
119
|
writeln(text) {
|
|
116
120
|
this.overlayStatus = null;
|
|
121
|
+
this._invalidateBaseLines();
|
|
117
122
|
this.currentText[this.currentText.length - 1].text += text;
|
|
118
123
|
this.currentText.push({ text: "", markdown: false });
|
|
119
124
|
}
|
|
@@ -122,6 +127,7 @@ export class OutputBuffer {
|
|
|
122
127
|
const current = this.currentText.at(-1);
|
|
123
128
|
if (!current || current.text === "") return false;
|
|
124
129
|
this.currentText.push({ text: "", markdown: false });
|
|
130
|
+
this._invalidateBaseLines();
|
|
125
131
|
return true;
|
|
126
132
|
}
|
|
127
133
|
|
|
@@ -141,12 +147,14 @@ export class OutputBuffer {
|
|
|
141
147
|
if (lastIdx >= 0) this._activeThinking.content[lastIdx] += parts[0];
|
|
142
148
|
else this._activeThinking.content.push(parts[0]);
|
|
143
149
|
for (let i = 1; i < parts.length; i++) this._activeThinking.content.push(parts[i]);
|
|
150
|
+
this._invalidateBaseLines();
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
endThinking(tokens) {
|
|
147
154
|
if (this._activeThinking) {
|
|
148
155
|
this._activeThinking.tokens = tokens;
|
|
149
156
|
this._activeThinking = null;
|
|
157
|
+
this._invalidateSegmentLines();
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
|
|
@@ -166,10 +174,12 @@ export class OutputBuffer {
|
|
|
166
174
|
|
|
167
175
|
setOverlayStatus(lines) {
|
|
168
176
|
this.overlayStatus = Array.isArray(lines) ? { type: "status", lines } : null;
|
|
177
|
+
this._invalidateBaseLines();
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
clearOverlayStatus() {
|
|
172
181
|
this.overlayStatus = null;
|
|
182
|
+
this._invalidateBaseLines();
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
sealCurrentText() {
|
|
@@ -232,34 +242,33 @@ export class OutputBuffer {
|
|
|
232
242
|
|
|
233
243
|
_invalidateSegmentLines() {
|
|
234
244
|
this._segmentLinesCache.clear();
|
|
245
|
+
this._invalidateBaseLines();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_invalidateBaseLines() {
|
|
249
|
+
this._baseLinesCache.clear();
|
|
235
250
|
}
|
|
236
251
|
|
|
237
252
|
render(width) {
|
|
238
|
-
const
|
|
239
|
-
this.
|
|
240
|
-
this.scrollState.setTotalLines(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
253
|
+
const baseLines = this._renderBaseLines(width);
|
|
254
|
+
const tailLine = this.spinning ? this._spinnerLine() : null;
|
|
255
|
+
this.scrollState.setTotalLines(baseLines.length + (tailLine == null ? 0 : 1));
|
|
256
|
+
return sliceLinesWithTail(baseLines, tailLine, this.scrollState.sliceRange());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_spinnerLine() {
|
|
260
|
+
return brightBlack(`${SPINNER_FRAMES[this.spinnerIdx]} ${this.spinnerText}`);
|
|
245
261
|
}
|
|
246
262
|
|
|
247
|
-
|
|
263
|
+
_renderBaseLines(width) {
|
|
264
|
+
const cached = this._baseLinesCache.get(width);
|
|
265
|
+
if (cached) return cached;
|
|
248
266
|
const lines = [...this._renderCachedSegmentLines(width)];
|
|
249
267
|
const dynamicStart = this._cachedSegmentPrefixCount();
|
|
250
|
-
for (const seg of this.segments.slice(dynamicStart))
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
for (const line of renderBlock(block, width)) lines.push(line);
|
|
255
|
-
}
|
|
256
|
-
if (this.overlayStatus) {
|
|
257
|
-
for (const line of renderBlock(this.overlayStatus, width)) lines.push(line);
|
|
258
|
-
}
|
|
259
|
-
if (this.spinning) {
|
|
260
|
-
const frame = SPINNER_FRAMES[this.spinnerIdx];
|
|
261
|
-
lines.push(brightBlack(`${frame} ${this.spinnerText}`));
|
|
262
|
-
}
|
|
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);
|
|
271
|
+
this._baseLinesCache.set(width, lines);
|
|
263
272
|
return lines;
|
|
264
273
|
}
|
|
265
274
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createStreamDeltaBuffer({
|
|
2
|
+
writeText,
|
|
3
|
+
writeThinking,
|
|
4
|
+
renderSoon,
|
|
5
|
+
delayMs = 16,
|
|
6
|
+
setTimeoutImpl = setTimeout,
|
|
7
|
+
clearTimeoutImpl = clearTimeout,
|
|
8
|
+
} = {}) {
|
|
9
|
+
const queued = [];
|
|
10
|
+
let timer = null;
|
|
11
|
+
|
|
12
|
+
function append(kind, delta) {
|
|
13
|
+
if (!delta) return;
|
|
14
|
+
const last = queued.at(-1);
|
|
15
|
+
if (last?.kind === kind) last.text += delta;
|
|
16
|
+
else queued.push({ kind, text: delta });
|
|
17
|
+
schedule();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function schedule() {
|
|
21
|
+
if (timer) return;
|
|
22
|
+
timer = setTimeoutImpl(() => flush(), delayMs);
|
|
23
|
+
timer.unref?.();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function flush({ notify = true } = {}) {
|
|
27
|
+
if (timer) {
|
|
28
|
+
clearTimeoutImpl(timer);
|
|
29
|
+
timer = null;
|
|
30
|
+
}
|
|
31
|
+
if (!queued.length) return false;
|
|
32
|
+
const batch = queued.splice(0);
|
|
33
|
+
for (const item of batch) {
|
|
34
|
+
if (item.kind === "thinking") writeThinking(item.text);
|
|
35
|
+
else writeText(item.text);
|
|
36
|
+
}
|
|
37
|
+
if (notify) renderSoon();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
text: (delta) => append("text", delta),
|
|
43
|
+
thinking: (delta) => append("thinking", delta),
|
|
44
|
+
flush,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -10,16 +10,19 @@ export class ScreenSelection {
|
|
|
10
10
|
this.anchor = null;
|
|
11
11
|
this.focus = null;
|
|
12
12
|
this.lines = [];
|
|
13
|
+
this._plainLines = [];
|
|
13
14
|
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: 0 };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
setLines(lines) {
|
|
17
|
-
this.lines = lines
|
|
18
|
+
this.lines = [...lines];
|
|
19
|
+
this._plainLines = [];
|
|
18
20
|
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: this.lines.length };
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
setViewport({ topRow = 0, leftCol = 0, width = Infinity, lines = [] } = {}) {
|
|
22
|
-
this.lines = lines
|
|
24
|
+
this.lines = lines;
|
|
25
|
+
this._plainLines = [];
|
|
23
26
|
this.viewport = {
|
|
24
27
|
topRow: Math.max(0, Math.trunc(topRow)),
|
|
25
28
|
leftCol: Math.max(0, Math.trunc(leftCol)),
|
|
@@ -68,7 +71,7 @@ export class ScreenSelection {
|
|
|
68
71
|
if (!range) return "";
|
|
69
72
|
const selected = [];
|
|
70
73
|
for (let row = range.start.row; row <= range.end.row; row += 1) {
|
|
71
|
-
const line = this.
|
|
74
|
+
const line = this._plainLine(row);
|
|
72
75
|
const startCol = row === range.start.row ? range.start.col : 0;
|
|
73
76
|
const endCol = row === range.end.row ? range.end.col : visibleWidth(line);
|
|
74
77
|
selected.push(sliceColumns(line, startCol, endCol));
|
|
@@ -81,7 +84,7 @@ export class ScreenSelection {
|
|
|
81
84
|
if (!range) return lines;
|
|
82
85
|
return lines.map((line, row) => {
|
|
83
86
|
if (row < range.start.row || row > range.end.row) return line;
|
|
84
|
-
const plain =
|
|
87
|
+
const plain = this._plainLine(row);
|
|
85
88
|
const startCol = row === range.start.row ? range.start.col : 0;
|
|
86
89
|
const endCol = row === range.end.row ? range.end.col : visibleWidth(plain);
|
|
87
90
|
if (endCol <= startCol) return line;
|
|
@@ -89,6 +92,11 @@ export class ScreenSelection {
|
|
|
89
92
|
});
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
_plainLine(row) {
|
|
96
|
+
if (this._plainLines[row] == null) this._plainLines[row] = stripAnsi(this.lines[row] ?? "");
|
|
97
|
+
return this._plainLines[row];
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
range() {
|
|
93
101
|
if (!this.anchor || !this.focus) return null;
|
|
94
102
|
const [start, end] = comparePoints(this.anchor, this.focus) <= 0
|
|
@@ -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"],
|
package/src/cli/ui.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { writeMemoryHint } from "./tui/recall-rendering.mjs";
|
|
|
24
24
|
import { writeToolEnd, writeToolStart } from "./tui/tool-rendering.mjs";
|
|
25
25
|
import { EDITOR_THEME, brightBlack } from "./tui/ui-theme.mjs";
|
|
26
26
|
import { createRenderScheduler } from "./tui/render/render-scheduler.mjs";
|
|
27
|
+
import { createStreamDeltaBuffer } from "./tui/render/stream-delta-buffer.mjs";
|
|
27
28
|
import { writeTranscriptToOutput } from "../session/transcript.mjs";
|
|
28
29
|
|
|
29
30
|
export { buildMarchCommands, MarchAutocompleteProvider } from "./input/autocomplete.mjs";
|
|
@@ -61,8 +62,9 @@ export function createTuiUI({
|
|
|
61
62
|
let toolsExpanded = false;
|
|
62
63
|
const activeToolBlocks = [];
|
|
63
64
|
const renderScheduler = createRenderScheduler({ requestRender: () => tui.requestRender() });
|
|
64
|
-
const
|
|
65
|
-
|
|
65
|
+
const streamDeltas = createStreamDeltaBuffer({ writeText: (delta) => output.writeMarkdown(delta), writeThinking: (delta) => output.appendThinking(delta), renderSoon: renderScheduler.renderSoon });
|
|
66
|
+
const flushStreamDeltas = () => streamDeltas.flush({ notify: false });
|
|
67
|
+
const requestRender = () => { flushStreamDeltas(); renderScheduler.renderNow(); };
|
|
66
68
|
const spinnerStatus = createSpinnerStatusController({ output, requestRender });
|
|
67
69
|
const retryStatus = createRetryStatusController({ output, requestRender, stopSpinner: spinnerStatus.stop });
|
|
68
70
|
const shellDrawerControls = createShellDrawerControls({ shellDrawer, output, requestRender });
|
|
@@ -169,12 +171,10 @@ export function createTuiUI({
|
|
|
169
171
|
retryStatus.stop(); output.startThinking(); requestRender();
|
|
170
172
|
},
|
|
171
173
|
|
|
172
|
-
thinkingDelta: (delta) =>
|
|
173
|
-
output.appendThinking(delta);
|
|
174
|
-
renderScheduler.renderSoon();
|
|
175
|
-
},
|
|
174
|
+
thinkingDelta: (delta) => streamDeltas.thinking(delta),
|
|
176
175
|
|
|
177
176
|
thinkingEnd: (tokens) => {
|
|
177
|
+
flushStreamDeltas();
|
|
178
178
|
output.endThinking(tokens);
|
|
179
179
|
requestRender();
|
|
180
180
|
},
|
|
@@ -186,7 +186,7 @@ export function createTuiUI({
|
|
|
186
186
|
toggleLastThinking: () => false,
|
|
187
187
|
|
|
188
188
|
toolStart: (name, args) => {
|
|
189
|
-
ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); activeToolBlocks.push(writeToolStart({ output, name, args })); requestRender();
|
|
189
|
+
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); activeToolBlocks.push(writeToolStart({ output, name, args })); requestRender();
|
|
190
190
|
},
|
|
191
191
|
|
|
192
192
|
toolEnd: (name, isError, result) => {
|
|
@@ -194,30 +194,26 @@ export function createTuiUI({
|
|
|
194
194
|
},
|
|
195
195
|
|
|
196
196
|
textDelta: (delta) => {
|
|
197
|
-
ensureStarted(); retryStatus.stop(); spinnerStatus.stop();
|
|
198
|
-
output.writeMarkdown(delta);
|
|
199
|
-
renderScheduler.renderSoon();
|
|
197
|
+
ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); streamDeltas.text(delta);
|
|
200
198
|
},
|
|
201
199
|
assistantReplyEnd: () => {
|
|
202
200
|
ensureStarted();
|
|
201
|
+
flushStreamDeltas();
|
|
203
202
|
const changed = output.ensureNewline();
|
|
204
203
|
if (output.sealCurrentText() || changed) requestRender();
|
|
205
204
|
},
|
|
206
205
|
status: (text) => {
|
|
207
|
-
ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); output.setOverlayStatus([brightBlack(`● ${text}`)]); requestRender();
|
|
206
|
+
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.setOverlayStatus([brightBlack(`● ${text}`)]); requestRender();
|
|
208
207
|
},
|
|
209
208
|
memoryHint: ({ hints }) => {
|
|
210
|
-
ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeMemoryHint({ output, hints }); requestRender();
|
|
209
|
+
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeMemoryHint({ output, hints }); requestRender();
|
|
211
210
|
},
|
|
212
211
|
|
|
213
212
|
clearOutput: () => {
|
|
214
|
-
ensureStarted(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); requestRender();
|
|
213
|
+
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); requestRender();
|
|
215
214
|
},
|
|
216
|
-
|
|
217
215
|
restoreTranscript: (turns) => {
|
|
218
|
-
ensureStarted(); spinnerStatus.stop(); retryStatus.stop(); output.clear();
|
|
219
|
-
writeTranscriptToOutput(output, turns);
|
|
220
|
-
requestRender();
|
|
216
|
+
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); writeTranscriptToOutput(output, turns); requestRender();
|
|
221
217
|
},
|
|
222
218
|
|
|
223
219
|
setStatusBar: (text) => {
|
|
@@ -229,6 +225,7 @@ export function createTuiUI({
|
|
|
229
225
|
},
|
|
230
226
|
|
|
231
227
|
turnEnd: () => {
|
|
228
|
+
flushStreamDeltas();
|
|
232
229
|
const changed = output.ensureNewline();
|
|
233
230
|
if (output.sealCurrentText() || changed) requestRender();
|
|
234
231
|
},
|
|
@@ -238,6 +235,7 @@ export function createTuiUI({
|
|
|
238
235
|
|
|
239
236
|
editDiff: (path, diffLines) => {
|
|
240
237
|
ensureStarted();
|
|
238
|
+
flushStreamDeltas();
|
|
241
239
|
spinnerStatus.stop();
|
|
242
240
|
writeEditDiff({ output, path, diffLines });
|
|
243
241
|
requestRender();
|
|
@@ -278,6 +276,7 @@ export function createTuiUI({
|
|
|
278
276
|
toggleShellDrawer: () => shellDrawerControls.toggle(),
|
|
279
277
|
requestExit: () => inputController.requestExit(),
|
|
280
278
|
close: async () => {
|
|
279
|
+
flushStreamDeltas();
|
|
281
280
|
renderScheduler.clearPending();
|
|
282
281
|
spinnerStatus.stop();
|
|
283
282
|
retryStatus.stop();
|
|
@@ -58,7 +58,7 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
|
|
|
58
58
|
- To edit an existing memory, use memory_open(id) to get its path, then edit_file with mode="patch" for targeted edits.
|
|
59
59
|
- Use memory_save() to create memories or update whole fields. Before creating a new memory, first search/open related memories and merge updates into an existing memory when they share the same topic, project, or decision thread; prefer modifying the existing memory file over creating a scattered new one. Tags are the primary retrieval key for future recall. Prefer lowercase kebab-case tags like 'march-cli', 'tooling', 'permissions'.
|
|
60
60
|
- When learning multiple related external workflows or skills, maintain memory as an evolving domain library: start with the specific source name when only one item exists, then rename and rewrite the memory title/description as the scope grows; merge new related learnings into the same memory, preserving each source's unique traits while distilling reusable principles.
|
|
61
|
-
- Distinguish "migrating a Skill to memory" from "learning a Skill": migration preserves the complete Skill folder under memory_root/skills/ and creates a memory index
|
|
61
|
+
- Distinguish "migrating a Skill to memory" from "learning a Skill": migration preserves the complete Skill folder under memory_root/skills/ and creates a memory entry as its index; that memory should describe what the Skill is for and reference the copied Skill folder path so future recall knows how to use it. Learning only reads and internalizes the Skill's methods, scenarios, and principles into ordinary memory without copying source files. Infer the action from the user's wording, and ask when ambiguous.
|
|
62
62
|
- Unlike memory hints, this system-core center is always visible in every model call. Only update the center for instructions that must always be followed; use memory for contextual, project-specific, or recall-dependent knowledge.
|
|
63
63
|
- If execution takes a meaningful detour, create or update a memory after the task. A detour means the initial plan or assumption failed, multiple approaches were tried, and the final successful path contains reusable project knowledge. Record the failed assumption, what was tried, and the successful approach. Prefer updating an existing related memory over creating a new one.
|
|
64
64
|
</memory_system>
|
package/src/shell/runtime.mjs
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
normalizeSize,
|
|
11
11
|
publicShell,
|
|
12
12
|
requireShell,
|
|
13
|
-
stripAnsi,
|
|
14
13
|
touch,
|
|
15
14
|
uniqueName,
|
|
16
15
|
} from "./runtime-state.mjs";
|
|
@@ -194,6 +193,14 @@ export function createShellRuntime({
|
|
|
194
193
|
};
|
|
195
194
|
}
|
|
196
195
|
|
|
196
|
+
function snapshotShellScreen(id) {
|
|
197
|
+
const shell = requireShell(shells, id);
|
|
198
|
+
return {
|
|
199
|
+
shell: publicShell(shell),
|
|
200
|
+
screen: shell.screen?.snapshot?.() ?? null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
197
204
|
function clearShell(id) {
|
|
198
205
|
const shell = requireShell(shells, id);
|
|
199
206
|
shell.rawChunks = [];
|
|
@@ -238,6 +245,7 @@ export function createShellRuntime({
|
|
|
238
245
|
getShell,
|
|
239
246
|
searchShell,
|
|
240
247
|
snapshotShell,
|
|
248
|
+
snapshotShellScreen,
|
|
241
249
|
clearShell,
|
|
242
250
|
dispose,
|
|
243
251
|
};
|
package/src/agent/find-tool.mjs
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
-
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Type } from "typebox";
|
|
5
|
-
import { toolText } from "./tool-result.mjs";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_LIMIT = 1000;
|
|
8
|
-
const DEFAULT_IGNORES = new Set([".git", "node_modules"]);
|
|
9
|
-
|
|
10
|
-
export function createFindTool({ cwd }) {
|
|
11
|
-
return defineTool({
|
|
12
|
-
name: "find",
|
|
13
|
-
label: "Find Files",
|
|
14
|
-
description: "Find files by glob pattern. Pattern is matched relative to the search directory. Basename-only patterns like '*.mjs' search recursively, so find('*.mjs', path:'src') and find('src/**/*.mjs') both work.",
|
|
15
|
-
parameters: Type.Object({
|
|
16
|
-
pattern: Type.String({ description: "Glob pattern to match files, e.g. '*.mjs', '**/*.json', or 'src/**/*.test.mjs'" }),
|
|
17
|
-
path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
|
|
18
|
-
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default 1000)" })),
|
|
19
|
-
}),
|
|
20
|
-
execute: async (_toolCallId, params) => executeFind({ cwd, ...params }),
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function executeFind({ cwd, pattern, path = ".", limit = DEFAULT_LIMIT }) {
|
|
25
|
-
const searchRoot = resolveSearchRoot(cwd, path);
|
|
26
|
-
const trimmedPattern = String(pattern ?? "").trim().replaceAll("\\", "/");
|
|
27
|
-
if (!trimmedPattern) return toolText("Error: pattern is required", { error: true });
|
|
28
|
-
const effectivePattern = normalizePattern(trimmedPattern);
|
|
29
|
-
|
|
30
|
-
const max = Math.max(1, Number(limit) || DEFAULT_LIMIT);
|
|
31
|
-
let files;
|
|
32
|
-
try {
|
|
33
|
-
files = listFiles(searchRoot);
|
|
34
|
-
} catch (err) {
|
|
35
|
-
return toolText(`Error: ${err.message}`, { error: true });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const matches = [];
|
|
39
|
-
for (const file of files) {
|
|
40
|
-
const rel = toPosix(relative(searchRoot, file));
|
|
41
|
-
if (!matchesGlob(effectivePattern, rel)) continue;
|
|
42
|
-
matches.push(rel);
|
|
43
|
-
if (matches.length >= max) break;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (matches.length === 0) return toolText("No files found matching pattern", { pattern: trimmedPattern, effectivePattern, path: searchRoot, count: 0 });
|
|
47
|
-
const limitHint = matches.length >= max ? `\n\n[Results truncated to ${max}. Increase limit or refine pattern.]` : "";
|
|
48
|
-
return toolText(`${matches.join("\n")}${limitHint}`, {
|
|
49
|
-
pattern: trimmedPattern,
|
|
50
|
-
effectivePattern: effectivePattern === trimmedPattern ? undefined : effectivePattern,
|
|
51
|
-
path: searchRoot,
|
|
52
|
-
count: matches.length,
|
|
53
|
-
resultLimitReached: matches.length >= max ? max : undefined,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function normalizePattern(pattern) {
|
|
58
|
-
if (pattern.includes("/") || pattern.includes("**")) return pattern;
|
|
59
|
-
return `**/${pattern}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function resolveSearchRoot(cwd, path) {
|
|
63
|
-
const raw = String(path || ".");
|
|
64
|
-
return isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function listFiles(root) {
|
|
68
|
-
const out = [];
|
|
69
|
-
walk(root, out);
|
|
70
|
-
return out.sort((a, b) => toPosix(a).localeCompare(toPosix(b)));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function walk(dir, out) {
|
|
74
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
-
if (entry.isDirectory() && DEFAULT_IGNORES.has(entry.name)) continue;
|
|
76
|
-
const path = resolve(dir, entry.name);
|
|
77
|
-
if (entry.isDirectory()) walk(path, out);
|
|
78
|
-
else if (entry.isFile()) out.push(path);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function matchesGlob(pattern, candidate) {
|
|
83
|
-
return matchSegments(splitGlob(pattern), splitGlob(candidate));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function matchSegments(patternSegments, candidateSegments) {
|
|
87
|
-
if (patternSegments.length === 0) return candidateSegments.length === 0;
|
|
88
|
-
const [head, ...tail] = patternSegments;
|
|
89
|
-
if (head === "**") {
|
|
90
|
-
if (matchSegments(tail, candidateSegments)) return true;
|
|
91
|
-
return candidateSegments.length > 0 && matchSegments(patternSegments, candidateSegments.slice(1));
|
|
92
|
-
}
|
|
93
|
-
if (candidateSegments.length === 0) return false;
|
|
94
|
-
return matchSegment(head, candidateSegments[0]) && matchSegments(tail, candidateSegments.slice(1));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function matchSegment(pattern, candidate) {
|
|
98
|
-
const regex = new RegExp(`^${escapeRegex(pattern).replaceAll("\\*", "[^/]*").replaceAll("\\?", "[^/]")}$`);
|
|
99
|
-
return regex.test(candidate);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function splitGlob(value) {
|
|
103
|
-
return String(value).split("/").filter(Boolean);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function toPosix(value) {
|
|
107
|
-
return String(value).replaceAll("\\", "/");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function escapeRegex(value) {
|
|
111
|
-
return String(value).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
|
|
112
|
-
}
|