@yagni-app/code-staging 1.0.0-staging.1183.1 → 1.0.0-staging.1185.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/dist/extension/askAdvisorTool.d.ts +7 -0
- package/dist/extension/askAdvisorTool.js +11 -3
- package/dist/extension/askYagniTool.js +2 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/condensedTools.d.ts +93 -0
- package/dist/extension/condensedTools.js +392 -0
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/footer.d.ts +2 -0
- package/dist/extension/footer.js +21 -8
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +42 -2
- package/dist/extension/subagents.d.ts +10 -0
- package/dist/extension/subagents.js +11 -3
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- package/dist/extension/webFetchTool.js +2 -0
- package/dist/extension/workingLine.d.ts +49 -0
- package/dist/extension/workingLine.js +116 -0
- package/package.json +2 -2
|
@@ -29,6 +29,7 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
|
|
|
29
29
|
import { type Component } from "@earendil-works/pi-tui";
|
|
30
30
|
import { Type } from "typebox";
|
|
31
31
|
import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
32
|
+
import type { WorkingLineHandle } from "./workingLine.js";
|
|
32
33
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
33
34
|
import { type PipelineStage } from "./pipeline/types.js";
|
|
34
35
|
import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
|
|
@@ -49,6 +50,12 @@ export interface MakeAskAdvisorToolOptions {
|
|
|
49
50
|
limits?: AdvisorLimits;
|
|
50
51
|
/** Injectable so tests never spawn a child. */
|
|
51
52
|
runStage?: typeof defaultRunStage;
|
|
53
|
+
/**
|
|
54
|
+
* The session working-line manager (workingLine.ts). When present, live
|
|
55
|
+
* consult progress goes through it (so the elapsed/token suffix survives);
|
|
56
|
+
* absent, the tool falls back to ui.setWorkingMessage directly.
|
|
57
|
+
*/
|
|
58
|
+
workingLine?: WorkingLineHandle;
|
|
52
59
|
}
|
|
53
60
|
/**
|
|
54
61
|
* Assemble the consult brief. The advisor's persona already tells it not to take
|
|
@@ -125,6 +125,8 @@ export function makeAskAdvisorTool(opts) {
|
|
|
125
125
|
"The advice comes back as plain text: act on it, and call record_decision when it settles a product-intent call so the next agent inherits it.",
|
|
126
126
|
],
|
|
127
127
|
parameters,
|
|
128
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
129
|
+
renderShell: "self",
|
|
128
130
|
renderCall: renderAdvisorCall,
|
|
129
131
|
renderResult: renderSubagentResult,
|
|
130
132
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -159,9 +161,12 @@ export function makeAskAdvisorTool(opts) {
|
|
|
159
161
|
},
|
|
160
162
|
});
|
|
161
163
|
const working = formatWorkingMessage([progress], now);
|
|
162
|
-
if (
|
|
164
|
+
if (working !== lastWorking) {
|
|
163
165
|
lastWorking = working;
|
|
164
|
-
|
|
166
|
+
if (opts.workingLine)
|
|
167
|
+
opts.workingLine.setActivity(working);
|
|
168
|
+
else
|
|
169
|
+
ui?.setWorkingMessage?.(working);
|
|
165
170
|
}
|
|
166
171
|
};
|
|
167
172
|
emit();
|
|
@@ -181,7 +186,10 @@ export function makeAskAdvisorTool(opts) {
|
|
|
181
186
|
}
|
|
182
187
|
finally {
|
|
183
188
|
// Restore the default "Working…" text whether we resolved or threw.
|
|
184
|
-
|
|
189
|
+
if (opts.workingLine)
|
|
190
|
+
opts.workingLine.setActivity(undefined);
|
|
191
|
+
else
|
|
192
|
+
ui?.setWorkingMessage?.();
|
|
185
193
|
}
|
|
186
194
|
const cost = result.usage?.cost ?? 0;
|
|
187
195
|
const state = opts.state.record(cost);
|
|
@@ -56,6 +56,8 @@ export function makeAskYagniTool(opts) {
|
|
|
56
56
|
"Answers carry a standing: treat a confirmed decision as settled; when you lean on an unverified assumption or an inference, say so where the work is reviewed; when there is no recorded position, follow the answer's instruction to record the assumption you proceed on.",
|
|
57
57
|
],
|
|
58
58
|
parameters,
|
|
59
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
60
|
+
renderShell: "self",
|
|
59
61
|
renderCall(args, theme) {
|
|
60
62
|
const t = theme;
|
|
61
63
|
let text = `${t.fg("toolTitle", t.bold("ask_yagni"))} ${t.fg("dim", clipLine(args?.question ?? "…", 100))}`;
|
|
@@ -109,7 +109,28 @@ export declare class ChipEditor extends CustomEditor {
|
|
|
109
109
|
/** Drop all stashed images. Called after a successful submit so a sent image
|
|
110
110
|
* is not re-attached to the next message. */
|
|
111
111
|
clearStash(): void;
|
|
112
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Re-style chip tokens AND lay down the Kimi-style prompt box: a rounded
|
|
114
|
+
* border on all four sides, with a bold `›` caret on the first content
|
|
115
|
+
* line and continuation lines indented so wrapped text aligns under it.
|
|
116
|
+
*
|
|
117
|
+
* The base editor renders full-width lines carrying its own 1-column left
|
|
118
|
+
* padding, so we render it narrower, strip that padding, and re-wrap each
|
|
119
|
+
* line in the frame. Exact column layout (0-indexed):
|
|
120
|
+
*
|
|
121
|
+
* 0 ╭ │ ╰ border
|
|
122
|
+
* 1 space
|
|
123
|
+
* 2 › (first line) / space (continuation)
|
|
124
|
+
* 3 space
|
|
125
|
+
* 4… text — same column on every line
|
|
126
|
+
* width-2 space
|
|
127
|
+
* width-1 │ border
|
|
128
|
+
*
|
|
129
|
+
* Box-drawing glyphs are drawn centered in their cell while text glyphs
|
|
130
|
+
* start at the cell's left bearing, so no whole-column position lands the
|
|
131
|
+
* border ink exactly on the footer's text column: column 0 reads a hair
|
|
132
|
+
* outside it, column 1 a hair inside. Column 0 is the accepted tradeoff.
|
|
133
|
+
*/
|
|
113
134
|
render(width: number): string[];
|
|
114
135
|
}
|
|
115
136
|
/**
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* the extension's pi imports to the same module instance pi uses.
|
|
23
23
|
*/
|
|
24
24
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import { matchesKey } from "@earendil-works/pi-tui";
|
|
25
|
+
import { matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import { readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
28
28
|
import { tmpdir } from "node:os";
|
|
@@ -381,11 +381,64 @@ export class ChipEditor extends CustomEditor {
|
|
|
381
381
|
clearStash() {
|
|
382
382
|
this.stashed = [];
|
|
383
383
|
}
|
|
384
|
-
/**
|
|
384
|
+
/**
|
|
385
|
+
* Re-style chip tokens AND lay down the Kimi-style prompt box: a rounded
|
|
386
|
+
* border on all four sides, with a bold `›` caret on the first content
|
|
387
|
+
* line and continuation lines indented so wrapped text aligns under it.
|
|
388
|
+
*
|
|
389
|
+
* The base editor renders full-width lines carrying its own 1-column left
|
|
390
|
+
* padding, so we render it narrower, strip that padding, and re-wrap each
|
|
391
|
+
* line in the frame. Exact column layout (0-indexed):
|
|
392
|
+
*
|
|
393
|
+
* 0 ╭ │ ╰ border
|
|
394
|
+
* 1 space
|
|
395
|
+
* 2 › (first line) / space (continuation)
|
|
396
|
+
* 3 space
|
|
397
|
+
* 4… text — same column on every line
|
|
398
|
+
* width-2 space
|
|
399
|
+
* width-1 │ border
|
|
400
|
+
*
|
|
401
|
+
* Box-drawing glyphs are drawn centered in their cell while text glyphs
|
|
402
|
+
* start at the cell's left bearing, so no whole-column position lands the
|
|
403
|
+
* border ink exactly on the footer's text column: column 0 reads a hair
|
|
404
|
+
* outside it, column 1 a hair inside. Column 0 is the accepted tradeoff.
|
|
405
|
+
*/
|
|
385
406
|
render(width) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
407
|
+
const styled = super.render(Math.max(1, width - 4)).map((line) => line.replace(CHIP_RE, (m) => `${CHIP_ON}${m}${CHIP_OFF}`));
|
|
408
|
+
const border = (s) => this.borderColor(s);
|
|
409
|
+
const CARET = "\u001b[1m›\u001b[22m";
|
|
410
|
+
const FIRST_PREFIX = `${border("│")} ${CARET} `;
|
|
411
|
+
const NEXT_PREFIX = `${border("│")} `;
|
|
412
|
+
const SUFFIX = ` ${border("│")}`;
|
|
413
|
+
const contentWidth = Math.max(1, width - 6);
|
|
414
|
+
const out = [];
|
|
415
|
+
let borderCount = 0;
|
|
416
|
+
let firstContentLine = true;
|
|
417
|
+
for (const line of styled) {
|
|
418
|
+
const stripped = stripTerminalSequences(line);
|
|
419
|
+
if (/^─+$/.test(stripped) || /^─*\s*[↑↓]/.test(stripped)) {
|
|
420
|
+
borderCount += 1;
|
|
421
|
+
const [left, right] = borderCount === 1 ? ["╭", "╮"] : ["╰", "╯"];
|
|
422
|
+
// One corner + one dash on each side lands the row at `width`.
|
|
423
|
+
out.push(`${border(`${left}─`)}${line}${border(`─${right}`)}`);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
// Strip the base editor's own left padding (the frame replaces it),
|
|
427
|
+
// then fit the body to the content width exactly so no line can
|
|
428
|
+
// exceed the terminal width.
|
|
429
|
+
let body = line.startsWith(" ") ? line.slice(1) : line;
|
|
430
|
+
if (visibleWidth(body) > contentWidth)
|
|
431
|
+
body = truncateToWidth(body, contentWidth, "");
|
|
432
|
+
const pad = " ".repeat(Math.max(0, contentWidth - visibleWidth(body)));
|
|
433
|
+
if (borderCount !== 1) {
|
|
434
|
+
// Autocomplete rows (after the bottom border): align under the text.
|
|
435
|
+
out.push(`${" ".repeat(4)}${body}${pad}${" ".repeat(2)}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
out.push(`${firstContentLine ? FIRST_PREFIX : NEXT_PREFIX}${body}${pad}${SUFFIX}`);
|
|
439
|
+
firstContentLine = false;
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
389
442
|
}
|
|
390
443
|
}
|
|
391
444
|
/**
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condensed, Claude Code-style rendering for pi's seven built-in tools.
|
|
3
|
+
*
|
|
4
|
+
* pi resolves renderers per slot (`toolDefinition.renderCall ?? builtIn.renderCall`),
|
|
5
|
+
* so re-registering a built-in by name with the SAME factory-made definition
|
|
6
|
+
* spread underneath swaps ONLY the presentation: name, description, parameters,
|
|
7
|
+
* prompt metadata, and execution semantics stay byte-identical with the
|
|
8
|
+
* built-in (execution delegates to the same `create*ToolDefinition` the session
|
|
9
|
+
* would have built, including the settings-driven bash shellPath/commandPrefix
|
|
10
|
+
* and read image auto-resize). pi 0.84.1 registers such overrides silently —
|
|
11
|
+
* there is no built-in-override startup warning in this version (verified
|
|
12
|
+
* against dist; the extensions.md claim is stale).
|
|
13
|
+
*
|
|
14
|
+
* Rendering contract per row (renderShell "self", so no tinted Box):
|
|
15
|
+
* - renderCall paints the TITLE slot: a live "● Tool(arg)" line while running,
|
|
16
|
+
* a "● Tool(arg)" line for visible rows, the run summary line for the tail
|
|
17
|
+
* of a completed quiet run, or nothing (a zero-line component removes the
|
|
18
|
+
* row entirely, including its spacer).
|
|
19
|
+
* - renderResult paints the BODY slot: condensed write/edit previews, bash
|
|
20
|
+
* error/partial output tails, or nothing. Expanded (ctrl+o) shows full
|
|
21
|
+
* output for every row.
|
|
22
|
+
*
|
|
23
|
+
* String assembly is pure over {@link RenderTheme} (subagentRender.ts's
|
|
24
|
+
* pattern) so tests run against plain text. Renderer exceptions are swallowed
|
|
25
|
+
* by pi (degrading to the built-in fallback), so everything stays total.
|
|
26
|
+
*/
|
|
27
|
+
import { SettingsManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
28
|
+
import type { RenderTheme } from "./subagentRender.js";
|
|
29
|
+
import { ToolRunTracker } from "./toolRuns.js";
|
|
30
|
+
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
31
|
+
export declare const WRITE_PREVIEW_LINES = 8;
|
|
32
|
+
/** Visual lines of a diff shown collapsed. */
|
|
33
|
+
export declare const DIFF_PREVIEW_LINES = 12;
|
|
34
|
+
/** Output tail lines shown under a failed shell command. */
|
|
35
|
+
export declare const ERROR_TAIL_LINES = 5;
|
|
36
|
+
/** Output tail lines shown under a still-running shell command. */
|
|
37
|
+
export declare const PARTIAL_TAIL_LINES = 3;
|
|
38
|
+
declare const BUILTIN_NAMES: readonly ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
39
|
+
type BuiltinName = (typeof BUILTIN_NAMES)[number];
|
|
40
|
+
export type RowStatus = "running" | "ok" | "error";
|
|
41
|
+
/** Home-collapse, and prefer cwd-relative for paths inside the project. */
|
|
42
|
+
export declare function displayPath(rawPath: string, cwd?: string): string;
|
|
43
|
+
/** Whether a tool-call path targets the session scratchpad. */
|
|
44
|
+
export declare function isScratchpadPath(rawPath: unknown, scratchpadDir: string | undefined, cwd?: string): boolean;
|
|
45
|
+
/** The one argument worth showing in a title: path, command, or pattern. */
|
|
46
|
+
export declare function primaryArg(name: BuiltinName, args: Record<string, unknown> | undefined, cwd?: string): string;
|
|
47
|
+
/** `● Tool(arg)` — the visible-row (and live-row) title line. */
|
|
48
|
+
export declare function formatRowTitle(name: BuiltinName, args: Record<string, unknown> | undefined, status: RowStatus, theme: RenderTheme, cwd?: string): string;
|
|
49
|
+
/** Extract "Command exited with code N" from a bash error body, if present. */
|
|
50
|
+
export declare function splitBashError(outputText: string): {
|
|
51
|
+
output: string;
|
|
52
|
+
exitLine: string | undefined;
|
|
53
|
+
};
|
|
54
|
+
/** `⎿ exit line` + a dim tail of output under a failed shell command. */
|
|
55
|
+
export declare function formatBashErrorBody(outputText: string, theme: RenderTheme): string[];
|
|
56
|
+
/** A dim tail of live output under a still-running shell command. */
|
|
57
|
+
export declare function formatBashPartialBody(outputText: string, theme: RenderTheme): string[];
|
|
58
|
+
/** Count added lines in a unified patch (edit summaries surface "+N"). */
|
|
59
|
+
export declare function countPatchAdditions(patch: string | undefined): number;
|
|
60
|
+
/**
|
|
61
|
+
* `⎿ Wrote N lines to path` + a numbered, syntax-highlighted preview.
|
|
62
|
+
* `highlight` is injectable so tests stay independent of pi's theme state.
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatWriteBody(rawPath: string, content: string, expanded: boolean, theme: RenderTheme, cwd?: string, highlight?: (code: string, filePath: string) => string[]): string[];
|
|
65
|
+
/**
|
|
66
|
+
* `⎿ Updated path (+A -R)` + a colored diff preview.
|
|
67
|
+
* `paintDiff` is injectable for the same reason as `highlight` above.
|
|
68
|
+
*/
|
|
69
|
+
export declare function formatEditBody(rawPath: string, details: {
|
|
70
|
+
diff?: string;
|
|
71
|
+
patch?: string;
|
|
72
|
+
} | undefined, expanded: boolean, theme: RenderTheme, cwd?: string, paintDiff?: (diffText: string) => string): string[];
|
|
73
|
+
/** Expanded body: the raw output, dimmed and capped. */
|
|
74
|
+
export declare function formatExpandedOutput(outputText: string, theme: RenderTheme): string[];
|
|
75
|
+
export interface RegisterCondensedToolsDeps {
|
|
76
|
+
/** Session scratchpad dir; writes/edits under it aggregate instead of rendering. */
|
|
77
|
+
scratchpadDir?: string;
|
|
78
|
+
/** Registration-time cwd (defaults to process.cwd()). */
|
|
79
|
+
cwd?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Settings loader seam. The default mirrors what pi's session does when it
|
|
82
|
+
* builds base tools: bash gets shellPath + shellCommandPrefix, read gets
|
|
83
|
+
* image autoResize. Injectable so tests never touch the config dir.
|
|
84
|
+
*/
|
|
85
|
+
loadSettings?: (cwd: string) => SettingsManager;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Re-register the seven built-ins with condensed renderers. Returns the
|
|
89
|
+
* tracker so callers (and tests) can feed message boundaries into it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function registerCondensedTools(pi: ExtensionAPI, deps?: RegisterCondensedToolsDeps): ToolRunTracker;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=condensedTools.d.ts.map
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condensed, Claude Code-style rendering for pi's seven built-in tools.
|
|
3
|
+
*
|
|
4
|
+
* pi resolves renderers per slot (`toolDefinition.renderCall ?? builtIn.renderCall`),
|
|
5
|
+
* so re-registering a built-in by name with the SAME factory-made definition
|
|
6
|
+
* spread underneath swaps ONLY the presentation: name, description, parameters,
|
|
7
|
+
* prompt metadata, and execution semantics stay byte-identical with the
|
|
8
|
+
* built-in (execution delegates to the same `create*ToolDefinition` the session
|
|
9
|
+
* would have built, including the settings-driven bash shellPath/commandPrefix
|
|
10
|
+
* and read image auto-resize). pi 0.84.1 registers such overrides silently —
|
|
11
|
+
* there is no built-in-override startup warning in this version (verified
|
|
12
|
+
* against dist; the extensions.md claim is stale).
|
|
13
|
+
*
|
|
14
|
+
* Rendering contract per row (renderShell "self", so no tinted Box):
|
|
15
|
+
* - renderCall paints the TITLE slot: a live "● Tool(arg)" line while running,
|
|
16
|
+
* a "● Tool(arg)" line for visible rows, the run summary line for the tail
|
|
17
|
+
* of a completed quiet run, or nothing (a zero-line component removes the
|
|
18
|
+
* row entirely, including its spacer).
|
|
19
|
+
* - renderResult paints the BODY slot: condensed write/edit previews, bash
|
|
20
|
+
* error/partial output tails, or nothing. Expanded (ctrl+o) shows full
|
|
21
|
+
* output for every row.
|
|
22
|
+
*
|
|
23
|
+
* String assembly is pure over {@link RenderTheme} (subagentRender.ts's
|
|
24
|
+
* pattern) so tests run against plain text. Renderer exceptions are swallowed
|
|
25
|
+
* by pi (degrading to the built-in fallback), so everything stays total.
|
|
26
|
+
*/
|
|
27
|
+
import * as os from "node:os";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, getAgentDir, getLanguageFromPath, highlightCode, renderDiff, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
31
|
+
import { isQuiet, kindForTool, ToolRunTracker } from "./toolRuns.js";
|
|
32
|
+
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
33
|
+
export const WRITE_PREVIEW_LINES = 8;
|
|
34
|
+
/** Visual lines of a diff shown collapsed. */
|
|
35
|
+
export const DIFF_PREVIEW_LINES = 12;
|
|
36
|
+
/** Output tail lines shown under a failed shell command. */
|
|
37
|
+
export const ERROR_TAIL_LINES = 5;
|
|
38
|
+
/** Output tail lines shown under a still-running shell command. */
|
|
39
|
+
export const PARTIAL_TAIL_LINES = 3;
|
|
40
|
+
/** Hard cap on expanded body lines so a huge output cannot stall a repaint. */
|
|
41
|
+
const EXPANDED_MAX_LINES = 1000;
|
|
42
|
+
/** Title arg preview width. */
|
|
43
|
+
const ARG_PREVIEW_MAX = 96;
|
|
44
|
+
const BUILTIN_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
45
|
+
const TITLE_BY_NAME = {
|
|
46
|
+
read: "Read",
|
|
47
|
+
bash: "Bash",
|
|
48
|
+
edit: "Edit",
|
|
49
|
+
write: "Write",
|
|
50
|
+
grep: "Grep",
|
|
51
|
+
find: "Find",
|
|
52
|
+
ls: "List",
|
|
53
|
+
};
|
|
54
|
+
/** Collapse whitespace and clip to `max`, appending an ellipsis when cut. */
|
|
55
|
+
function clip(text, max) {
|
|
56
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
57
|
+
if (collapsed.length <= max)
|
|
58
|
+
return collapsed;
|
|
59
|
+
return `${collapsed.slice(0, max - 1)}…`;
|
|
60
|
+
}
|
|
61
|
+
/** Forward slashes for comparison and display, on every platform. */
|
|
62
|
+
function slashed(p) {
|
|
63
|
+
return p.replace(/\\/g, "/");
|
|
64
|
+
}
|
|
65
|
+
/** Home-collapse, and prefer cwd-relative for paths inside the project. */
|
|
66
|
+
export function displayPath(rawPath, cwd) {
|
|
67
|
+
if (!rawPath)
|
|
68
|
+
return rawPath;
|
|
69
|
+
let display = slashed(rawPath);
|
|
70
|
+
if (cwd) {
|
|
71
|
+
// Resolve BOTH sides so a POSIX-style cwd and Windows' drive-letter
|
|
72
|
+
// absolute paths compare in the same space.
|
|
73
|
+
const absolute = slashed(path.resolve(cwd, rawPath));
|
|
74
|
+
const base = slashed(path.resolve(cwd)).replace(/\/+$/, "");
|
|
75
|
+
if (absolute.startsWith(`${base}/`))
|
|
76
|
+
return absolute.slice(base.length + 1);
|
|
77
|
+
display = absolute;
|
|
78
|
+
}
|
|
79
|
+
const home = slashed(os.homedir());
|
|
80
|
+
return display.startsWith(home) ? `~${display.slice(home.length)}` : display;
|
|
81
|
+
}
|
|
82
|
+
/** Whether a tool-call path targets the session scratchpad. */
|
|
83
|
+
export function isScratchpadPath(rawPath, scratchpadDir, cwd) {
|
|
84
|
+
if (typeof rawPath !== "string" || !rawPath || !scratchpadDir)
|
|
85
|
+
return false;
|
|
86
|
+
const absolute = slashed(path.resolve(cwd ?? process.cwd(), rawPath));
|
|
87
|
+
const base = slashed(path.resolve(scratchpadDir)).replace(/\/+$/, "");
|
|
88
|
+
return absolute === base || absolute.startsWith(`${base}/`);
|
|
89
|
+
}
|
|
90
|
+
/** The one argument worth showing in a title: path, command, or pattern. */
|
|
91
|
+
export function primaryArg(name, args, cwd) {
|
|
92
|
+
const a = args ?? {};
|
|
93
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
94
|
+
switch (name) {
|
|
95
|
+
case "bash":
|
|
96
|
+
return clip(str(a.command) || "…", ARG_PREVIEW_MAX);
|
|
97
|
+
case "grep": {
|
|
98
|
+
const pattern = str(a.pattern) || "…";
|
|
99
|
+
const where = str(a.path);
|
|
100
|
+
return clip(where ? `${pattern} in ${displayPath(where, cwd)}` : pattern, ARG_PREVIEW_MAX);
|
|
101
|
+
}
|
|
102
|
+
case "find":
|
|
103
|
+
return clip(str(a.pattern) || "…", ARG_PREVIEW_MAX);
|
|
104
|
+
case "ls":
|
|
105
|
+
return clip(displayPath(str(a.path) || ".", cwd), ARG_PREVIEW_MAX);
|
|
106
|
+
default: {
|
|
107
|
+
const p = str(a.path);
|
|
108
|
+
let display = p ? displayPath(p, cwd) : "…";
|
|
109
|
+
if (name === "read" && (a.offset !== undefined || a.limit !== undefined)) {
|
|
110
|
+
const start = typeof a.offset === "number" ? a.offset : 1;
|
|
111
|
+
const end = typeof a.limit === "number" ? start + a.limit - 1 : undefined;
|
|
112
|
+
display += `:${start}${end !== undefined ? `-${end}` : ""}`;
|
|
113
|
+
}
|
|
114
|
+
return clip(display, ARG_PREVIEW_MAX);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function statusDot(status, theme) {
|
|
119
|
+
if (status === "error")
|
|
120
|
+
return theme.fg("error", "●");
|
|
121
|
+
if (status === "running")
|
|
122
|
+
return theme.fg("accent", "●");
|
|
123
|
+
return theme.fg("success", "●");
|
|
124
|
+
}
|
|
125
|
+
/** `● Tool(arg)` — the visible-row (and live-row) title line. */
|
|
126
|
+
export function formatRowTitle(name, args, status, theme, cwd) {
|
|
127
|
+
const title = theme.bold(theme.fg("toolTitle", TITLE_BY_NAME[name]));
|
|
128
|
+
return `${statusDot(status, theme)} ${title}(${theme.fg("accent", primaryArg(name, args, cwd))})`;
|
|
129
|
+
}
|
|
130
|
+
/** Extract "Command exited with code N" from a bash error body, if present. */
|
|
131
|
+
export function splitBashError(outputText) {
|
|
132
|
+
const match = outputText.match(/\n*(Command exited with code \d+)\s*$/);
|
|
133
|
+
if (!match)
|
|
134
|
+
return { output: outputText.trimEnd(), exitLine: undefined };
|
|
135
|
+
return { output: outputText.slice(0, match.index).trimEnd(), exitLine: match[1] };
|
|
136
|
+
}
|
|
137
|
+
function indent(lines, pad) {
|
|
138
|
+
return lines.map((line) => pad + line);
|
|
139
|
+
}
|
|
140
|
+
/** `⎿ exit line` + a dim tail of output under a failed shell command. */
|
|
141
|
+
export function formatBashErrorBody(outputText, theme) {
|
|
142
|
+
const { output, exitLine } = splitBashError(outputText);
|
|
143
|
+
const lines = [];
|
|
144
|
+
lines.push(` ${theme.fg("muted", "⎿")} ${theme.fg("error", exitLine ?? "Command failed")}`);
|
|
145
|
+
const tail = output.split("\n").filter((l) => l.length > 0).slice(-ERROR_TAIL_LINES);
|
|
146
|
+
lines.push(...indent(tail.map((l) => theme.fg("toolOutput", l)), " "));
|
|
147
|
+
return lines;
|
|
148
|
+
}
|
|
149
|
+
/** A dim tail of live output under a still-running shell command. */
|
|
150
|
+
export function formatBashPartialBody(outputText, theme) {
|
|
151
|
+
const tail = outputText.split("\n").filter((l) => l.length > 0).slice(-PARTIAL_TAIL_LINES);
|
|
152
|
+
return indent(tail.map((l) => theme.fg("dim", l)), " ");
|
|
153
|
+
}
|
|
154
|
+
/** Count added lines in a unified patch (edit summaries surface "+N"). */
|
|
155
|
+
export function countPatchAdditions(patch) {
|
|
156
|
+
if (!patch)
|
|
157
|
+
return 0;
|
|
158
|
+
let added = 0;
|
|
159
|
+
for (const line of patch.split("\n")) {
|
|
160
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
161
|
+
added += 1;
|
|
162
|
+
}
|
|
163
|
+
return added;
|
|
164
|
+
}
|
|
165
|
+
function countPatchRemovals(patch) {
|
|
166
|
+
if (!patch)
|
|
167
|
+
return 0;
|
|
168
|
+
let removed = 0;
|
|
169
|
+
for (const line of patch.split("\n")) {
|
|
170
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
171
|
+
removed += 1;
|
|
172
|
+
}
|
|
173
|
+
return removed;
|
|
174
|
+
}
|
|
175
|
+
const EXPAND_HINT = "(ctrl+o to expand)";
|
|
176
|
+
/**
|
|
177
|
+
* `⎿ Wrote N lines to path` + a numbered, syntax-highlighted preview.
|
|
178
|
+
* `highlight` is injectable so tests stay independent of pi's theme state.
|
|
179
|
+
*/
|
|
180
|
+
export function formatWriteBody(rawPath, content, expanded, theme, cwd, highlight = highlightForPath) {
|
|
181
|
+
const allLines = content.replace(/\n$/, "").split("\n");
|
|
182
|
+
const total = allLines.length;
|
|
183
|
+
const shownCount = expanded ? Math.min(total, EXPANDED_MAX_LINES) : Math.min(total, WRITE_PREVIEW_LINES);
|
|
184
|
+
const head = ` ${theme.fg("muted", "⎿")} Wrote ${total} ${total === 1 ? "line" : "lines"} to ${theme.fg("accent", displayPath(rawPath, cwd))}`;
|
|
185
|
+
const shown = allLines.slice(0, shownCount);
|
|
186
|
+
const painted = highlight(shown.join("\n"), rawPath);
|
|
187
|
+
const numberWidth = String(shownCount).length;
|
|
188
|
+
const body = painted
|
|
189
|
+
.slice(0, shownCount)
|
|
190
|
+
.map((line, i) => ` ${theme.fg("dim", String(i + 1).padStart(numberWidth))} ${line}`);
|
|
191
|
+
const lines = [head, ...body];
|
|
192
|
+
if (total > shownCount) {
|
|
193
|
+
lines.push(` ${theme.fg("muted", `… +${total - shownCount} lines ${EXPAND_HINT}`)}`);
|
|
194
|
+
}
|
|
195
|
+
return lines;
|
|
196
|
+
}
|
|
197
|
+
/** Default highlighter: pi's theme-backed syntax highlighting, failing to plain lines. */
|
|
198
|
+
function highlightForPath(code, filePath) {
|
|
199
|
+
try {
|
|
200
|
+
return highlightCode(code, getLanguageFromPath(filePath));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return code.split("\n");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* `⎿ Updated path (+A -R)` + a colored diff preview.
|
|
208
|
+
* `paintDiff` is injectable for the same reason as `highlight` above.
|
|
209
|
+
*/
|
|
210
|
+
export function formatEditBody(rawPath, details, expanded, theme, cwd, paintDiff = defaultPaintDiff) {
|
|
211
|
+
const added = countPatchAdditions(details?.patch);
|
|
212
|
+
const removed = countPatchRemovals(details?.patch);
|
|
213
|
+
const counts = added || removed
|
|
214
|
+
? ` (${[added ? theme.fg("success", `+${added}`) : "", removed ? theme.fg("error", `-${removed}`) : ""]
|
|
215
|
+
.filter(Boolean)
|
|
216
|
+
.join(" ")})`
|
|
217
|
+
: "";
|
|
218
|
+
const head = ` ${theme.fg("muted", "⎿")} Updated ${theme.fg("accent", displayPath(rawPath, cwd))}${counts}`;
|
|
219
|
+
const diffText = details?.diff;
|
|
220
|
+
if (!diffText)
|
|
221
|
+
return [head];
|
|
222
|
+
const diffLines = paintDiff(diffText).split("\n");
|
|
223
|
+
const cap = expanded ? EXPANDED_MAX_LINES : DIFF_PREVIEW_LINES;
|
|
224
|
+
const lines = [head, ...indent(diffLines.slice(0, cap), " ")];
|
|
225
|
+
if (diffLines.length > cap) {
|
|
226
|
+
lines.push(` ${theme.fg("muted", `… +${diffLines.length - cap} lines ${EXPAND_HINT}`)}`);
|
|
227
|
+
}
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
function defaultPaintDiff(diffText) {
|
|
231
|
+
try {
|
|
232
|
+
return renderDiff(diffText);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return diffText;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Expanded body: the raw output, dimmed and capped. */
|
|
239
|
+
export function formatExpandedOutput(outputText, theme) {
|
|
240
|
+
const lines = outputText.replace(/\n$/, "").split("\n");
|
|
241
|
+
const shown = lines.slice(0, EXPANDED_MAX_LINES).map((l) => theme.fg("toolOutput", l));
|
|
242
|
+
if (lines.length > EXPANDED_MAX_LINES) {
|
|
243
|
+
shown.push(theme.fg("muted", `… +${lines.length - EXPANDED_MAX_LINES} lines`));
|
|
244
|
+
}
|
|
245
|
+
return shown;
|
|
246
|
+
}
|
|
247
|
+
function textOf(result) {
|
|
248
|
+
return (result?.content ?? [])
|
|
249
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
250
|
+
.map((c) => c.text)
|
|
251
|
+
.join("\n");
|
|
252
|
+
}
|
|
253
|
+
function hasImages(result) {
|
|
254
|
+
return (result?.content ?? []).some((c) => c.type === "image");
|
|
255
|
+
}
|
|
256
|
+
function empty() {
|
|
257
|
+
return new Container();
|
|
258
|
+
}
|
|
259
|
+
function textComponent(lines) {
|
|
260
|
+
return lines.length === 0 ? empty() : new Text(lines.join("\n"), 0, 0);
|
|
261
|
+
}
|
|
262
|
+
function defaultLoadSettings(cwd) {
|
|
263
|
+
return SettingsManager.create(cwd, getAgentDir());
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Re-register the seven built-ins with condensed renderers. Returns the
|
|
267
|
+
* tracker so callers (and tests) can feed message boundaries into it.
|
|
268
|
+
*/
|
|
269
|
+
export function registerCondensedTools(pi, deps = {}) {
|
|
270
|
+
const tracker = new ToolRunTracker();
|
|
271
|
+
const registrationCwd = deps.cwd ?? process.cwd();
|
|
272
|
+
// Assistant/user prose breaks a run; toolResult messages must not (they are
|
|
273
|
+
// interleaved between the very rows a run aggregates).
|
|
274
|
+
pi.on("message_start", (event) => {
|
|
275
|
+
const role = event.message?.role;
|
|
276
|
+
if (role === "assistant" || role === "user")
|
|
277
|
+
tracker.markBreak();
|
|
278
|
+
});
|
|
279
|
+
let toolOptions;
|
|
280
|
+
try {
|
|
281
|
+
const settings = (deps.loadSettings ?? defaultLoadSettings)(registrationCwd);
|
|
282
|
+
toolOptions = {
|
|
283
|
+
bash: { shellPath: settings.getShellPath(), commandPrefix: settings.getShellCommandPrefix() },
|
|
284
|
+
read: { autoResizeImages: settings.getImageAutoResize() },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
toolOptions = { bash: {}, read: {} };
|
|
289
|
+
}
|
|
290
|
+
const buildDefinitions = (cwd) => ({
|
|
291
|
+
read: createReadToolDefinition(cwd, toolOptions.read),
|
|
292
|
+
bash: createBashToolDefinition(cwd, toolOptions.bash),
|
|
293
|
+
edit: createEditToolDefinition(cwd),
|
|
294
|
+
write: createWriteToolDefinition(cwd),
|
|
295
|
+
grep: createGrepToolDefinition(cwd),
|
|
296
|
+
find: createFindToolDefinition(cwd),
|
|
297
|
+
ls: createLsToolDefinition(cwd),
|
|
298
|
+
});
|
|
299
|
+
const definitionsByCwd = new Map();
|
|
300
|
+
const definitionsFor = (cwd) => {
|
|
301
|
+
let defs = definitionsByCwd.get(cwd);
|
|
302
|
+
if (!defs) {
|
|
303
|
+
defs = buildDefinitions(cwd);
|
|
304
|
+
definitionsByCwd.set(cwd, defs);
|
|
305
|
+
}
|
|
306
|
+
return defs;
|
|
307
|
+
};
|
|
308
|
+
definitionsByCwd.set(registrationCwd, buildDefinitions(registrationCwd));
|
|
309
|
+
for (const name of BUILTIN_NAMES) {
|
|
310
|
+
const base = definitionsFor(registrationCwd)[name];
|
|
311
|
+
const kind = kindForTool(name);
|
|
312
|
+
const upsertFromCall = (context) => {
|
|
313
|
+
const args = context.args;
|
|
314
|
+
return tracker.upsert(context.toolCallId, {
|
|
315
|
+
kind,
|
|
316
|
+
scratchpad: isScratchpadPath(args?.path, deps.scratchpadDir, context.cwd),
|
|
317
|
+
error: context.isError,
|
|
318
|
+
final: !context.isPartial,
|
|
319
|
+
invalidate: context.invalidate,
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
pi.registerTool({
|
|
323
|
+
...base,
|
|
324
|
+
renderShell: "self",
|
|
325
|
+
execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
326
|
+
const cwd = ctx?.cwd ?? registrationCwd;
|
|
327
|
+
return definitionsFor(cwd)[name].execute(toolCallId, params, signal, onUpdate, ctx);
|
|
328
|
+
},
|
|
329
|
+
renderCall(args, theme, context) {
|
|
330
|
+
const slice = context;
|
|
331
|
+
const row = upsertFromCall(slice);
|
|
332
|
+
const status = slice.isError ? "error" : slice.isPartial ? "running" : "ok";
|
|
333
|
+
const argsRecord = args;
|
|
334
|
+
if (slice.expanded || !isQuiet(row)) {
|
|
335
|
+
return new Text(formatRowTitle(name, argsRecord, status, theme, slice.cwd), 0, 0);
|
|
336
|
+
}
|
|
337
|
+
if (!row.final) {
|
|
338
|
+
return new Text(formatRowTitle(name, argsRecord, "running", theme, slice.cwd), 0, 0);
|
|
339
|
+
}
|
|
340
|
+
const summary = tracker.summaryFor(row.id);
|
|
341
|
+
return summary ? new Text(theme.fg("muted", summary), 0, 0) : empty();
|
|
342
|
+
},
|
|
343
|
+
renderResult(result, options, theme, context) {
|
|
344
|
+
const slice = context;
|
|
345
|
+
const args = (slice.args ?? {});
|
|
346
|
+
const row = tracker.upsert(slice.toolCallId, {
|
|
347
|
+
kind,
|
|
348
|
+
final: !options.isPartial,
|
|
349
|
+
error: slice.isError,
|
|
350
|
+
images: hasImages(result),
|
|
351
|
+
added: name === "write" && typeof args.content === "string"
|
|
352
|
+
? args.content.replace(/\n$/, "").split("\n").length
|
|
353
|
+
: name === "edit"
|
|
354
|
+
? countPatchAdditions(result.details?.patch)
|
|
355
|
+
: 0,
|
|
356
|
+
});
|
|
357
|
+
if (options.expanded) {
|
|
358
|
+
if (name === "write" && typeof args.path === "string" && typeof args.content === "string" && !slice.isError) {
|
|
359
|
+
return textComponent(formatWriteBody(args.path, args.content, true, theme, slice.cwd));
|
|
360
|
+
}
|
|
361
|
+
if (name === "edit" && typeof args.path === "string" && !slice.isError && result.details) {
|
|
362
|
+
return textComponent(formatEditBody(args.path, result.details, true, theme, slice.cwd));
|
|
363
|
+
}
|
|
364
|
+
const output = textOf(result);
|
|
365
|
+
return output ? textComponent(formatExpandedOutput(output, theme)) : empty();
|
|
366
|
+
}
|
|
367
|
+
if (name === "bash" && options.isPartial) {
|
|
368
|
+
return textComponent(formatBashPartialBody(textOf(result), theme));
|
|
369
|
+
}
|
|
370
|
+
if (isQuiet(row))
|
|
371
|
+
return empty();
|
|
372
|
+
if (slice.isError) {
|
|
373
|
+
if (name === "bash")
|
|
374
|
+
return textComponent(formatBashErrorBody(textOf(result), theme));
|
|
375
|
+
const message = textOf(result).split("\n").filter(Boolean).slice(-ERROR_TAIL_LINES);
|
|
376
|
+
return textComponent(message.map((l, i) => (i === 0 ? ` ${theme.fg("muted", "⎿")} ${theme.fg("error", l)}` : ` ${theme.fg("toolOutput", l)}`)));
|
|
377
|
+
}
|
|
378
|
+
if (name === "write" && typeof args.path === "string" && typeof args.content === "string") {
|
|
379
|
+
return textComponent(formatWriteBody(args.path, args.content, false, theme, slice.cwd));
|
|
380
|
+
}
|
|
381
|
+
if (name === "edit" && typeof args.path === "string") {
|
|
382
|
+
return textComponent(formatEditBody(args.path, result.details, false, theme, slice.cwd));
|
|
383
|
+
}
|
|
384
|
+
// Visible for another reason (e.g. an image read): title already
|
|
385
|
+
// painted by renderCall; images render themselves below the row.
|
|
386
|
+
return empty();
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return tracker;
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=condensedTools.js.map
|