@oh-my-pi/pi-coding-agent 8.2.2 → 8.3.0
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/CHANGELOG.md +14 -0
- package/package.json +6 -6
- package/src/extensibility/extensions/wrapper.ts +4 -0
- package/src/extensibility/hooks/tool-wrapper.ts +4 -0
- package/src/lsp/index.ts +6 -4
- package/src/lsp/render.ts +115 -19
- package/src/lsp/types.ts +1 -0
- package/src/modes/components/tool-execution.ts +22 -6
- package/src/patch/applicator.ts +7 -3
- package/src/patch/fuzzy.ts +13 -3
- package/src/tools/bash.ts +1 -0
- package/src/tools/fetch.ts +1 -0
- package/src/tools/write.ts +82 -83
- package/src/tui/output-block.ts +26 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [8.3.0] - 2026-01-25
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Added request parameter tracking to LSP tool rendering for better diagnostics visibility
|
|
9
|
+
- Added async diff computation and Kitty protocol support to tool execution rendering
|
|
10
|
+
- Refactored patch applicator with improved fuzzy matching (7-pass sequence matching with Levenshtein distance) and indentation adjustment
|
|
11
|
+
- Added inline rendering flag to bash and fetch tool renderers
|
|
12
|
+
- Extracted constants for preview formatting to improve code maintainability
|
|
13
|
+
- Exposed mergeCallAndResult and inline rendering options from tools to their wrappers
|
|
14
|
+
- Added timeout validation and normalization for tool timeout parameters
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Fixed output block border rendering (bottom-right corner was missing)
|
|
18
|
+
- Added background control parameter to output block rendering
|
|
5
19
|
## [8.2.2] - 2026-01-24
|
|
6
20
|
|
|
7
21
|
### Removed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"test": "bun test"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@oh-my-pi/omp-stats": "8.
|
|
79
|
-
"@oh-my-pi/pi-agent-core": "8.
|
|
80
|
-
"@oh-my-pi/pi-ai": "8.
|
|
81
|
-
"@oh-my-pi/pi-tui": "8.
|
|
82
|
-
"@oh-my-pi/pi-utils": "8.
|
|
78
|
+
"@oh-my-pi/omp-stats": "8.3.0",
|
|
79
|
+
"@oh-my-pi/pi-agent-core": "8.3.0",
|
|
80
|
+
"@oh-my-pi/pi-ai": "8.3.0",
|
|
81
|
+
"@oh-my-pi/pi-tui": "8.3.0",
|
|
82
|
+
"@oh-my-pi/pi-utils": "8.3.0",
|
|
83
83
|
"@openai/agents": "^0.4.3",
|
|
84
84
|
"@sinclair/typebox": "^0.34.46",
|
|
85
85
|
"ajv": "^8.17.1",
|
|
@@ -79,6 +79,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
79
79
|
parameters: TParameters;
|
|
80
80
|
renderCall?: AgentTool<TParameters, TDetails>["renderCall"];
|
|
81
81
|
renderResult?: AgentTool<TParameters, TDetails>["renderResult"];
|
|
82
|
+
mergeCallAndResult?: boolean;
|
|
83
|
+
inline?: boolean;
|
|
82
84
|
|
|
83
85
|
constructor(
|
|
84
86
|
private tool: AgentTool<TParameters, TDetails>,
|
|
@@ -90,6 +92,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
90
92
|
this.parameters = tool.parameters;
|
|
91
93
|
this.renderCall = tool.renderCall;
|
|
92
94
|
this.renderResult = tool.renderResult;
|
|
95
|
+
this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
|
|
96
|
+
this.inline = (tool as { inline?: boolean }).inline;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
async execute(
|
|
@@ -23,6 +23,8 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
23
23
|
parameters: TParameters;
|
|
24
24
|
renderCall?: AgentTool<TParameters, TDetails>["renderCall"];
|
|
25
25
|
renderResult?: AgentTool<TParameters, TDetails>["renderResult"];
|
|
26
|
+
mergeCallAndResult?: boolean;
|
|
27
|
+
inline?: boolean;
|
|
26
28
|
|
|
27
29
|
constructor(
|
|
28
30
|
private tool: AgentTool<TParameters, TDetails>,
|
|
@@ -34,6 +36,8 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
34
36
|
this.parameters = tool.parameters;
|
|
35
37
|
this.renderCall = tool.renderCall;
|
|
36
38
|
this.renderResult = tool.renderResult;
|
|
39
|
+
this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
|
|
40
|
+
this.inline = (tool as { inline?: boolean }).inline;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
async execute(
|
package/src/lsp/index.ts
CHANGED
|
@@ -955,6 +955,8 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
955
955
|
public readonly parameters = lspSchema;
|
|
956
956
|
public readonly renderCall = renderCall;
|
|
957
957
|
public readonly renderResult = renderResult;
|
|
958
|
+
public readonly mergeCallAndResult = true;
|
|
959
|
+
public readonly inline = true;
|
|
958
960
|
|
|
959
961
|
private readonly session: ToolSession;
|
|
960
962
|
|
|
@@ -1011,7 +1013,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1011
1013
|
const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
|
|
1012
1014
|
return {
|
|
1013
1015
|
content: [{ type: "text", text: output }],
|
|
1014
|
-
details: { action, success: true },
|
|
1016
|
+
details: { action, success: true, request: params },
|
|
1015
1017
|
};
|
|
1016
1018
|
}
|
|
1017
1019
|
|
|
@@ -1025,7 +1027,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1025
1027
|
text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
|
|
1026
1028
|
},
|
|
1027
1029
|
],
|
|
1028
|
-
details: { action, success: true },
|
|
1030
|
+
details: { action, success: true, request: params },
|
|
1029
1031
|
};
|
|
1030
1032
|
}
|
|
1031
1033
|
|
|
@@ -1636,13 +1638,13 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1636
1638
|
|
|
1637
1639
|
return {
|
|
1638
1640
|
content: [{ type: "text", text: output }],
|
|
1639
|
-
details: { serverName, action, success: true },
|
|
1641
|
+
details: { serverName, action, success: true, request: params },
|
|
1640
1642
|
};
|
|
1641
1643
|
} catch (err) {
|
|
1642
1644
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1643
1645
|
return {
|
|
1644
1646
|
content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
|
|
1645
|
-
details: { serverName, action, success: false },
|
|
1647
|
+
details: { serverName, action, success: false, request: params },
|
|
1646
1648
|
};
|
|
1647
1649
|
}
|
|
1648
1650
|
}
|
package/src/lsp/render.ts
CHANGED
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
* - Grouped references and symbols
|
|
8
8
|
* - Collapsible/expandable views
|
|
9
9
|
*/
|
|
10
|
-
import type {
|
|
10
|
+
import type { RenderResultOptions } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
12
12
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
13
13
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
formatExpandHint,
|
|
16
|
+
formatMoreItems,
|
|
17
|
+
formatStatusIcon,
|
|
18
|
+
shortenPath,
|
|
19
|
+
TRUNCATE_LENGTHS,
|
|
20
|
+
truncate,
|
|
21
|
+
} from "../tools/render-utils";
|
|
15
22
|
import { renderOutputBlock, renderStatusLine } from "../tui";
|
|
16
23
|
import type { LspParams, LspToolDetails } from "./types";
|
|
17
24
|
|
|
@@ -23,15 +30,74 @@ import type { LspParams, LspToolDetails } from "./types";
|
|
|
23
30
|
* Render the LSP tool call in the TUI.
|
|
24
31
|
* Shows: "lsp <operation> <file/filecount>"
|
|
25
32
|
*/
|
|
26
|
-
export function renderCall(args:
|
|
27
|
-
const
|
|
33
|
+
export function renderCall(args: LspParams, theme: Theme): Text {
|
|
34
|
+
const actionLabel = (args.action ?? "request").replace(/_/g, " ");
|
|
35
|
+
const queryPreview = args.query ? truncate(args.query, TRUNCATE_LENGTHS.SHORT, theme.format.ellipsis) : undefined;
|
|
36
|
+
const replacementPreview = args.replacement
|
|
37
|
+
? truncate(args.replacement, TRUNCATE_LENGTHS.SHORT, theme.format.ellipsis)
|
|
38
|
+
: undefined;
|
|
39
|
+
|
|
40
|
+
let target: string | undefined;
|
|
41
|
+
let hasFileTarget = false;
|
|
42
|
+
|
|
43
|
+
if (args.file) {
|
|
44
|
+
target = shortenPath(args.file);
|
|
45
|
+
hasFileTarget = true;
|
|
46
|
+
} else if (args.files?.length === 1) {
|
|
47
|
+
target = shortenPath(args.files[0]);
|
|
48
|
+
hasFileTarget = true;
|
|
49
|
+
} else if (args.files?.length) {
|
|
50
|
+
target = `${args.files.length} files`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (hasFileTarget && args.line !== undefined) {
|
|
54
|
+
const col = args.column !== undefined ? `:${args.column}` : "";
|
|
55
|
+
target += `:${args.line}${col}`;
|
|
56
|
+
if (args.end_line !== undefined) {
|
|
57
|
+
const endCol = args.end_character !== undefined ? `:${args.end_character}` : "";
|
|
58
|
+
target += `-${args.end_line}${endCol}`;
|
|
59
|
+
}
|
|
60
|
+
} else if (!target && args.line !== undefined) {
|
|
61
|
+
const col = args.column !== undefined ? `:${args.column}` : "";
|
|
62
|
+
target = `line ${args.line}${col}`;
|
|
63
|
+
if (args.end_line !== undefined) {
|
|
64
|
+
const endCol = args.end_character !== undefined ? `:${args.end_character}` : "";
|
|
65
|
+
target += `-${args.end_line}${endCol}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
28
69
|
const meta: string[] = [];
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
70
|
+
if (queryPreview && target) meta.push(`query:${queryPreview}`);
|
|
71
|
+
if (args.new_name) meta.push(`new:${args.new_name}`);
|
|
72
|
+
if (replacementPreview) meta.push(`replace:${replacementPreview}`);
|
|
73
|
+
if (args.kind) meta.push(`kind:${args.kind}`);
|
|
74
|
+
if (args.apply !== undefined) meta.push(`apply:${args.apply ? "true" : "false"}`);
|
|
75
|
+
if (args.action_index !== undefined) meta.push(`action:${args.action_index}`);
|
|
76
|
+
if (args.include_declaration !== undefined) {
|
|
77
|
+
meta.push(`include_decl:${args.include_declaration ? "true" : "false"}`);
|
|
78
|
+
}
|
|
79
|
+
if (args.end_line !== undefined && args.line === undefined) {
|
|
80
|
+
const endCol = args.end_character !== undefined ? `:${args.end_character}` : "";
|
|
81
|
+
meta.push(`end:${args.end_line}${endCol}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const descriptionParts = [actionLabel];
|
|
85
|
+
if (target) {
|
|
86
|
+
descriptionParts.push(target);
|
|
87
|
+
} else if (queryPreview) {
|
|
88
|
+
descriptionParts.push(queryPreview);
|
|
33
89
|
}
|
|
34
|
-
|
|
90
|
+
|
|
91
|
+
const text = renderStatusLine(
|
|
92
|
+
{
|
|
93
|
+
icon: "pending",
|
|
94
|
+
title: "LSP",
|
|
95
|
+
description: descriptionParts.join(" "),
|
|
96
|
+
meta,
|
|
97
|
+
},
|
|
98
|
+
theme,
|
|
99
|
+
);
|
|
100
|
+
|
|
35
101
|
return new Text(text, 0, 0);
|
|
36
102
|
}
|
|
37
103
|
|
|
@@ -44,14 +110,15 @@ export function renderCall(args: unknown, theme: Theme): Text {
|
|
|
44
110
|
* Detects hover, diagnostics, references, symbols, etc. and formats accordingly.
|
|
45
111
|
*/
|
|
46
112
|
export function renderResult(
|
|
47
|
-
result:
|
|
113
|
+
result: { content: Array<{ type: string; text?: string }>; details?: LspToolDetails; isError?: boolean },
|
|
48
114
|
options: RenderResultOptions,
|
|
49
115
|
theme: Theme,
|
|
50
116
|
args?: LspParams & { file?: string; files?: string[] },
|
|
51
117
|
): Component {
|
|
52
118
|
const content = result.content?.[0];
|
|
53
119
|
if (!content || content.type !== "text" || !("text" in content) || !content.text) {
|
|
54
|
-
const
|
|
120
|
+
const icon = formatStatusIcon("warning", theme, options.spinnerFrame);
|
|
121
|
+
const header = `${icon} LSP`;
|
|
55
122
|
return new Text([header, theme.fg("dim", "No result")].join("\n"), 0, 0);
|
|
56
123
|
}
|
|
57
124
|
|
|
@@ -94,22 +161,50 @@ export function renderResult(
|
|
|
94
161
|
}
|
|
95
162
|
}
|
|
96
163
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
} else if (
|
|
102
|
-
|
|
164
|
+
const request = args ?? result.details?.request;
|
|
165
|
+
const requestLines: string[] = [];
|
|
166
|
+
if (request?.file) {
|
|
167
|
+
requestLines.push(theme.fg("toolOutput", request.file));
|
|
168
|
+
} else if (request?.files?.length === 1) {
|
|
169
|
+
requestLines.push(theme.fg("toolOutput", request.files[0]));
|
|
170
|
+
} else if (request?.files?.length) {
|
|
171
|
+
requestLines.push(theme.fg("dim", `${request.files.length} file(s)`));
|
|
172
|
+
}
|
|
173
|
+
if (request?.line !== undefined) {
|
|
174
|
+
const col = request.column !== undefined ? `:${request.column}` : "";
|
|
175
|
+
requestLines.push(theme.fg("dim", `line ${request.line}${col}`));
|
|
103
176
|
}
|
|
104
|
-
|
|
177
|
+
if (request?.end_line !== undefined) {
|
|
178
|
+
const endCol = request.end_character !== undefined ? `:${request.end_character}` : "";
|
|
179
|
+
requestLines.push(theme.fg("dim", `end ${request.end_line}${endCol}`));
|
|
180
|
+
}
|
|
181
|
+
if (request?.query) requestLines.push(theme.fg("dim", `query: ${request.query}`));
|
|
182
|
+
if (request?.new_name) requestLines.push(theme.fg("dim", `new name: ${request.new_name}`));
|
|
183
|
+
if (request?.replacement) requestLines.push(theme.fg("dim", `replacement: ${request.replacement}`));
|
|
184
|
+
if (request?.kind) requestLines.push(theme.fg("dim", `kind: ${request.kind}`));
|
|
185
|
+
if (request?.apply !== undefined) requestLines.push(theme.fg("dim", `apply: ${request.apply ? "true" : "false"}`));
|
|
186
|
+
if (request?.action_index !== undefined) requestLines.push(theme.fg("dim", `action: ${request.action_index}`));
|
|
187
|
+
if (request?.include_declaration !== undefined) {
|
|
188
|
+
requestLines.push(theme.fg("dim", `include declaration: ${request.include_declaration ? "true" : "false"}`));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const actionLabel = (request?.action ?? result.details?.action ?? label.toLowerCase()).replace(/_/g, " ");
|
|
192
|
+
const status = options.isPartial ? "running" : result.isError ? "error" : "success";
|
|
193
|
+
const icon = formatStatusIcon(status, theme, options.spinnerFrame);
|
|
194
|
+
const header = `${icon} LSP ${actionLabel}`;
|
|
195
|
+
|
|
105
196
|
return {
|
|
106
197
|
render: (width: number) =>
|
|
107
198
|
renderOutputBlock(
|
|
108
199
|
{
|
|
109
200
|
header,
|
|
110
201
|
state,
|
|
111
|
-
sections: [
|
|
202
|
+
sections: [
|
|
203
|
+
...(requestLines.length > 0 ? [{ lines: requestLines }] : []),
|
|
204
|
+
{ label: theme.fg("toolTitle", "Response"), lines: bodyLines },
|
|
205
|
+
],
|
|
112
206
|
width,
|
|
207
|
+
applyBg: false,
|
|
113
208
|
},
|
|
114
209
|
theme,
|
|
115
210
|
),
|
|
@@ -612,4 +707,5 @@ export const lspToolRenderer = {
|
|
|
612
707
|
renderCall,
|
|
613
708
|
renderResult,
|
|
614
709
|
mergeCallAndResult: true,
|
|
710
|
+
inline: true,
|
|
615
711
|
};
|
package/src/lsp/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
|
+
type Component,
|
|
4
5
|
Container,
|
|
5
6
|
getCapabilities,
|
|
6
7
|
getImageDimensions,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
type TUI,
|
|
12
13
|
} from "@oh-my-pi/pi-tui";
|
|
13
14
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import type { Theme } from "../../modes/theme/theme";
|
|
14
16
|
import { theme } from "../../modes/theme/theme";
|
|
15
17
|
import { computeEditDiff, computePatchDiff, type EditDiffError, type EditDiffResult } from "../../patch";
|
|
16
18
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
|
|
@@ -376,7 +378,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
376
378
|
const tool = this.tool;
|
|
377
379
|
const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
|
|
378
380
|
// Custom tools use Box for flexible component rendering
|
|
379
|
-
|
|
381
|
+
const inline = Boolean((tool as { inline?: boolean }).inline);
|
|
382
|
+
this.contentBox.setBgFn(inline ? undefined : bgFn);
|
|
380
383
|
this.contentBox.clear();
|
|
381
384
|
|
|
382
385
|
// Render call component
|
|
@@ -404,10 +407,17 @@ export class ToolExecutionComponent extends Container {
|
|
|
404
407
|
// Render result component if we have a result
|
|
405
408
|
if (this.result && tool.renderResult) {
|
|
406
409
|
try {
|
|
407
|
-
const
|
|
408
|
-
{ content:
|
|
410
|
+
const renderResult = tool.renderResult as (
|
|
411
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
|
412
|
+
options: { expanded: boolean; isPartial: boolean; spinnerFrame?: number },
|
|
413
|
+
theme: Theme,
|
|
414
|
+
args?: unknown,
|
|
415
|
+
) => Component;
|
|
416
|
+
const resultComponent = renderResult(
|
|
417
|
+
{ content: this.result.content as any, details: this.result.details, isError: this.result.isError },
|
|
409
418
|
{ expanded: this.expanded, isPartial: this.isPartial, spinnerFrame: this.spinnerFrame },
|
|
410
419
|
theme,
|
|
420
|
+
this.args,
|
|
411
421
|
);
|
|
412
422
|
if (resultComponent) {
|
|
413
423
|
// Ensure component has invalidate() method for Component interface
|
|
@@ -542,10 +552,16 @@ export class ToolExecutionComponent extends Container {
|
|
|
542
552
|
}
|
|
543
553
|
|
|
544
554
|
/**
|
|
545
|
-
* Build render context for tools that need extra state (bash, edit)
|
|
555
|
+
* Build render context for tools that need extra state (bash, python, edit)
|
|
546
556
|
*/
|
|
547
557
|
private buildRenderContext(): Record<string, unknown> {
|
|
548
558
|
const context: Record<string, unknown> = {};
|
|
559
|
+
const normalizeTimeoutSeconds = (value: unknown, maxSeconds: number): number | undefined => {
|
|
560
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
561
|
+
let timeoutSec = value > 1000 ? value / 1000 : value;
|
|
562
|
+
timeoutSec = Math.max(1, Math.min(maxSeconds, timeoutSec));
|
|
563
|
+
return timeoutSec;
|
|
564
|
+
};
|
|
549
565
|
|
|
550
566
|
if (this.toolName === "bash" && this.result) {
|
|
551
567
|
// Pass raw output and expanded state - renderer handles width-aware truncation
|
|
@@ -553,13 +569,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
553
569
|
context.output = output;
|
|
554
570
|
context.expanded = this.expanded;
|
|
555
571
|
context.previewLines = BASH_DEFAULT_PREVIEW_LINES;
|
|
556
|
-
context.timeout =
|
|
572
|
+
context.timeout = normalizeTimeoutSeconds(this.args?.timeout, 3600);
|
|
557
573
|
} else if (this.toolName === "python" && this.result) {
|
|
558
574
|
const output = this.getTextOutput().trimEnd();
|
|
559
575
|
context.output = output;
|
|
560
576
|
context.expanded = this.expanded;
|
|
561
577
|
context.previewLines = PYTHON_DEFAULT_PREVIEW_LINES;
|
|
562
|
-
context.timeout =
|
|
578
|
+
context.timeout = normalizeTimeoutSeconds(this.args?.timeout, 600);
|
|
563
579
|
} else if (this.toolName === "edit") {
|
|
564
580
|
// Edit needs diff preview and renderDiff function
|
|
565
581
|
context.editDiffPreview = this.editDiffPreview;
|
package/src/patch/applicator.ts
CHANGED
|
@@ -957,11 +957,15 @@ function computeReplacements(
|
|
|
957
957
|
if (secondMatch.index !== undefined) {
|
|
958
958
|
// Extract 3-line previews for each match
|
|
959
959
|
const formatPreview = (startIdx: number) => {
|
|
960
|
-
const
|
|
960
|
+
const contextLines = 2;
|
|
961
|
+
const maxLineLength = 80;
|
|
962
|
+
const start = Math.max(0, startIdx - contextLines);
|
|
963
|
+
const end = Math.min(originalLines.length, startIdx + contextLines + 1);
|
|
964
|
+
const lines = originalLines.slice(start, end);
|
|
961
965
|
return lines
|
|
962
966
|
.map((line, i) => {
|
|
963
|
-
const num =
|
|
964
|
-
const truncated = line.length >
|
|
967
|
+
const num = start + i + 1;
|
|
968
|
+
const truncated = line.length > maxLineLength ? `${line.slice(0, maxLineLength - 3)}...` : line;
|
|
965
969
|
return ` ${num} | ${truncated}`;
|
|
966
970
|
})
|
|
967
971
|
.join("\n");
|
package/src/patch/fuzzy.ts
CHANGED
|
@@ -29,6 +29,12 @@ const PARTIAL_MATCH_MIN_LENGTH = 6;
|
|
|
29
29
|
/** Minimum ratio of pattern to line length for substring match */
|
|
30
30
|
const PARTIAL_MATCH_MIN_RATIO = 0.3;
|
|
31
31
|
|
|
32
|
+
/** Context lines to show before/after an ambiguous match preview */
|
|
33
|
+
const OCCURRENCE_PREVIEW_CONTEXT = 5;
|
|
34
|
+
|
|
35
|
+
/** Maximum line length for ambiguous match previews */
|
|
36
|
+
const OCCURRENCE_PREVIEW_MAX_LEN = 80;
|
|
37
|
+
|
|
32
38
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
39
|
// Core Algorithms
|
|
34
40
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -224,10 +230,14 @@ export function findMatch(
|
|
|
224
230
|
if (idx === -1) break;
|
|
225
231
|
const lineNumber = content.slice(0, idx).split("\n").length;
|
|
226
232
|
occurrenceLines.push(lineNumber);
|
|
227
|
-
|
|
228
|
-
const
|
|
233
|
+
const start = Math.max(0, lineNumber - 1 - OCCURRENCE_PREVIEW_CONTEXT);
|
|
234
|
+
const end = Math.min(contentLines.length, lineNumber + OCCURRENCE_PREVIEW_CONTEXT + 1);
|
|
235
|
+
const previewLines = contentLines.slice(start, end);
|
|
229
236
|
const preview = previewLines
|
|
230
|
-
.map((line,
|
|
237
|
+
.map((line, idx) => {
|
|
238
|
+
const num = start + idx + 1;
|
|
239
|
+
return ` ${num} | ${line.length > OCCURRENCE_PREVIEW_MAX_LEN ? `${line.slice(0, OCCURRENCE_PREVIEW_MAX_LEN - 3)}...` : line}`;
|
|
240
|
+
})
|
|
231
241
|
.join("\n");
|
|
232
242
|
occurrencePreviews.push(preview);
|
|
233
243
|
searchStart = idx + 1;
|
package/src/tools/bash.ts
CHANGED
package/src/tools/fetch.ts
CHANGED
package/src/tools/write.ts
CHANGED
|
@@ -15,10 +15,17 @@ import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCal
|
|
|
15
15
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
16
16
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
17
17
|
import type { ToolSession } from "../sdk";
|
|
18
|
-
import {
|
|
18
|
+
import { renderStatusLine } from "../tui";
|
|
19
19
|
import { type OutputMeta, outputMeta } from "./output-meta";
|
|
20
20
|
import { resolveToCwd } from "./path-utils";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
formatDiagnostics,
|
|
23
|
+
formatExpandHint,
|
|
24
|
+
formatMoreItems,
|
|
25
|
+
formatStatusIcon,
|
|
26
|
+
shortenPath,
|
|
27
|
+
ToolUIKit,
|
|
28
|
+
} from "./render-utils";
|
|
22
29
|
import type { RenderCallOptions } from "./renderers";
|
|
23
30
|
|
|
24
31
|
const writeSchema = Type.Object({
|
|
@@ -123,6 +130,7 @@ interface WriteRenderArgs {
|
|
|
123
130
|
content?: string;
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
const WRITE_PREVIEW_LINES = 6;
|
|
126
134
|
const WRITE_STREAMING_PREVIEW_LINES = 12;
|
|
127
135
|
|
|
128
136
|
function countLines(text: string): number {
|
|
@@ -138,93 +146,98 @@ function formatMetadataLine(lineCount: number | null, language: string | undefin
|
|
|
138
146
|
return uiTheme.fg("dim", `${icon}`);
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
function formatStreamingContent(content: string, uiTheme: Theme, ui: ToolUIKit): string {
|
|
150
|
+
if (!content) return "";
|
|
151
|
+
const lines = content.split("\n");
|
|
152
|
+
const displayLines = lines.slice(-WRITE_STREAMING_PREVIEW_LINES);
|
|
153
|
+
const hidden = lines.length - displayLines.length;
|
|
154
|
+
|
|
155
|
+
let text = "\n\n";
|
|
156
|
+
if (hidden > 0) {
|
|
157
|
+
text += uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)\n`);
|
|
158
|
+
}
|
|
159
|
+
for (const line of displayLines) {
|
|
160
|
+
text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
|
|
161
|
+
}
|
|
162
|
+
text += uiTheme.fg("dim", `${uiTheme.format.ellipsis} (streaming)`);
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme, ui: ToolUIKit): string {
|
|
167
|
+
if (!content) return "";
|
|
168
|
+
const lines = content.split("\n");
|
|
169
|
+
const maxLines = expanded ? lines.length : Math.min(lines.length, WRITE_PREVIEW_LINES);
|
|
170
|
+
const displayLines = expanded ? lines : lines.slice(-maxLines);
|
|
171
|
+
const hidden = lines.length - displayLines.length;
|
|
172
|
+
|
|
173
|
+
let text = "\n\n";
|
|
174
|
+
for (const line of displayLines) {
|
|
175
|
+
text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
|
|
176
|
+
}
|
|
177
|
+
if (!expanded && hidden > 0) {
|
|
178
|
+
const hint = formatExpandHint(uiTheme, expanded, hidden > 0);
|
|
179
|
+
const moreLine = `${formatMoreItems(hidden, "line", uiTheme)}${hint ? ` ${hint}` : ""}`;
|
|
180
|
+
text += uiTheme.fg("dim", moreLine);
|
|
181
|
+
}
|
|
182
|
+
return text;
|
|
183
|
+
}
|
|
184
|
+
|
|
141
185
|
export const writeToolRenderer = {
|
|
142
186
|
renderCall(args: WriteRenderArgs, uiTheme: Theme, options?: RenderCallOptions): Component {
|
|
187
|
+
const ui = new ToolUIKit(uiTheme);
|
|
143
188
|
const rawPath = args.file_path || args.path || "";
|
|
144
189
|
const filePath = shortenPath(rawPath);
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
uiTheme,
|
|
150
|
-
|
|
190
|
+
const lang = getLanguageFromPath(rawPath) ?? "text";
|
|
191
|
+
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
192
|
+
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
|
|
193
|
+
const spinner =
|
|
194
|
+
options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
|
|
195
|
+
|
|
196
|
+
let text = `${ui.title("Write")} ${spinner ? `${spinner} ` : ""}${langIcon} ${pathDisplay}`;
|
|
197
|
+
|
|
151
198
|
if (!args.content) {
|
|
152
199
|
return new Text(text, 0, 0);
|
|
153
200
|
}
|
|
154
201
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (hidden > 0) {
|
|
160
|
-
outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)`));
|
|
161
|
-
}
|
|
162
|
-
outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (streaming)`));
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
render: (width: number) =>
|
|
166
|
-
renderCodeCell(
|
|
167
|
-
{
|
|
168
|
-
code: displayLines.join("\n"),
|
|
169
|
-
language: getLanguageFromPath(rawPath),
|
|
170
|
-
title: filePath ? `Write ${filePath}` : "Write",
|
|
171
|
-
status,
|
|
172
|
-
spinnerFrame: options?.spinnerFrame,
|
|
173
|
-
output: outputLines.join("\n"),
|
|
174
|
-
codeMaxLines: WRITE_STREAMING_PREVIEW_LINES,
|
|
175
|
-
expanded: true,
|
|
176
|
-
width,
|
|
177
|
-
},
|
|
178
|
-
uiTheme,
|
|
179
|
-
),
|
|
180
|
-
invalidate: () => {},
|
|
181
|
-
};
|
|
202
|
+
// Show streaming preview of content (tail)
|
|
203
|
+
text += formatStreamingContent(args.content, uiTheme, ui);
|
|
204
|
+
|
|
205
|
+
return new Text(text, 0, 0);
|
|
182
206
|
},
|
|
183
207
|
|
|
184
208
|
renderResult(
|
|
185
209
|
result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails },
|
|
186
|
-
{ expanded
|
|
210
|
+
{ expanded }: RenderResultOptions,
|
|
187
211
|
uiTheme: Theme,
|
|
188
212
|
args?: WriteRenderArgs,
|
|
189
213
|
): Component {
|
|
214
|
+
const ui = new ToolUIKit(uiTheme);
|
|
190
215
|
const rawPath = args?.file_path || args?.path || "";
|
|
191
216
|
const filePath = shortenPath(rawPath);
|
|
192
217
|
const fileContent = args?.content || "";
|
|
193
218
|
const lang = getLanguageFromPath(rawPath);
|
|
194
|
-
const
|
|
219
|
+
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
220
|
+
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
|
|
195
221
|
const lineCount = countLines(fileContent);
|
|
196
222
|
|
|
197
|
-
|
|
223
|
+
// Build header with status icon
|
|
224
|
+
const header = renderStatusLine(
|
|
225
|
+
{
|
|
226
|
+
icon: "success",
|
|
227
|
+
title: "Write",
|
|
228
|
+
description: `${langIcon} ${pathDisplay}`,
|
|
229
|
+
},
|
|
230
|
+
uiTheme,
|
|
231
|
+
);
|
|
232
|
+
let text = header;
|
|
198
233
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const displayLines = contentLines.slice(-WRITE_STREAMING_PREVIEW_LINES);
|
|
202
|
-
const hidden = contentLines.length - displayLines.length;
|
|
203
|
-
if (hidden > 0) {
|
|
204
|
-
outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)`));
|
|
205
|
-
}
|
|
206
|
-
outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (streaming)`));
|
|
234
|
+
// Add metadata line
|
|
235
|
+
text += `\n${formatMetadataLine(lineCount, lang ?? "text", uiTheme)}`;
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
renderCodeCell(
|
|
211
|
-
{
|
|
212
|
-
code: displayLines.join("\n"),
|
|
213
|
-
language: lang,
|
|
214
|
-
title: filePath ? `Write ${filePath}` : "Write",
|
|
215
|
-
status: spinnerFrame !== undefined ? "running" : "pending",
|
|
216
|
-
spinnerFrame,
|
|
217
|
-
output: outputLines.join("\n"),
|
|
218
|
-
codeMaxLines: WRITE_STREAMING_PREVIEW_LINES,
|
|
219
|
-
expanded: true,
|
|
220
|
-
width,
|
|
221
|
-
},
|
|
222
|
-
uiTheme,
|
|
223
|
-
),
|
|
224
|
-
invalidate: () => {},
|
|
225
|
-
};
|
|
226
|
-
}
|
|
237
|
+
// Show content preview (collapsed tail, expandable)
|
|
238
|
+
text += renderContentPreview(fileContent, expanded, uiTheme, ui);
|
|
227
239
|
|
|
240
|
+
// Show diagnostics if available
|
|
228
241
|
if (result.details?.diagnostics) {
|
|
229
242
|
const diagText = formatDiagnostics(result.details.diagnostics, expanded, uiTheme, fp =>
|
|
230
243
|
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
@@ -232,27 +245,13 @@ export const writeToolRenderer = {
|
|
|
232
245
|
if (diagText.trim()) {
|
|
233
246
|
const diagLines = diagText.split("\n");
|
|
234
247
|
const firstNonEmpty = diagLines.findIndex(line => line.trim());
|
|
235
|
-
|
|
248
|
+
if (firstNonEmpty >= 0) {
|
|
249
|
+
text += `\n${diagLines.slice(firstNonEmpty).join("\n")}`;
|
|
250
|
+
}
|
|
236
251
|
}
|
|
237
252
|
}
|
|
238
253
|
|
|
239
|
-
return
|
|
240
|
-
render: (width: number) =>
|
|
241
|
-
renderCodeCell(
|
|
242
|
-
{
|
|
243
|
-
code: fileContent,
|
|
244
|
-
language: lang,
|
|
245
|
-
title: filePath ? `Write ${filePath}` : "Write",
|
|
246
|
-
status: "complete",
|
|
247
|
-
output: outputLines.join("\n"),
|
|
248
|
-
codeMaxLines: expanded ? Number.POSITIVE_INFINITY : 10,
|
|
249
|
-
expanded,
|
|
250
|
-
width,
|
|
251
|
-
},
|
|
252
|
-
uiTheme,
|
|
253
|
-
),
|
|
254
|
-
invalidate: () => {},
|
|
255
|
-
};
|
|
254
|
+
return new Text(text, 0, 0);
|
|
256
255
|
},
|
|
257
256
|
mergeCallAndResult: true,
|
|
258
257
|
};
|
package/src/tui/output-block.ts
CHANGED
|
@@ -12,10 +12,11 @@ export interface OutputBlockOptions {
|
|
|
12
12
|
state?: State;
|
|
13
13
|
sections?: Array<{ label?: string; lines: string[] }>;
|
|
14
14
|
width: number;
|
|
15
|
+
applyBg?: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[] {
|
|
18
|
-
const { header, headerMeta, state, sections = [], width } = options;
|
|
19
|
+
const { header, headerMeta, state, sections = [], width, applyBg = true } = options;
|
|
19
20
|
const h = theme.boxSharp.horizontal;
|
|
20
21
|
const v = theme.boxSharp.vertical;
|
|
21
22
|
const cap = h.repeat(3);
|
|
@@ -30,24 +31,31 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
30
31
|
? "accent"
|
|
31
32
|
: "dim";
|
|
32
33
|
const border = (text: string) => theme.fg(borderColor, text);
|
|
33
|
-
const bgFn = state ? (text: string) => theme.bg(getStateBgColor(state), text) : undefined;
|
|
34
|
+
const bgFn = state && applyBg ? (text: string) => theme.bg(getStateBgColor(state), text) : undefined;
|
|
34
35
|
|
|
35
|
-
const buildBarLine = (leftChar: string, label?: string, meta?: string): string => {
|
|
36
|
+
const buildBarLine = (leftChar: string, rightChar: string, label?: string, meta?: string): string => {
|
|
36
37
|
const left = border(`${leftChar}${cap}`);
|
|
37
|
-
|
|
38
|
+
const right = border(rightChar);
|
|
39
|
+
if (lineWidth <= 0) return left + right;
|
|
38
40
|
const labelText = [label, meta].filter(Boolean).join(theme.sep.dot);
|
|
39
41
|
const rawLabel = labelText ? ` ${labelText} ` : " ";
|
|
40
|
-
const
|
|
42
|
+
const leftWidth = visibleWidth(left);
|
|
43
|
+
const rightWidth = visibleWidth(right);
|
|
44
|
+
const maxLabelWidth = Math.max(0, lineWidth - leftWidth - rightWidth);
|
|
41
45
|
const trimmedLabel = truncateToWidth(rawLabel, maxLabelWidth, theme.format.ellipsis);
|
|
42
|
-
const
|
|
43
|
-
|
|
46
|
+
const labelWidth = visibleWidth(trimmedLabel);
|
|
47
|
+
const fillCount = Math.max(0, lineWidth - leftWidth - labelWidth - rightWidth);
|
|
48
|
+
return `${left}${trimmedLabel}${border(h.repeat(fillCount))}${right}`;
|
|
44
49
|
};
|
|
45
50
|
|
|
46
51
|
const contentPrefix = border(`${v} `);
|
|
47
|
-
const
|
|
52
|
+
const contentSuffix = border(v);
|
|
53
|
+
const contentWidth = Math.max(0, lineWidth - visibleWidth(contentPrefix) - visibleWidth(contentSuffix));
|
|
48
54
|
const lines: string[] = [];
|
|
49
55
|
|
|
50
|
-
lines.push(
|
|
56
|
+
lines.push(
|
|
57
|
+
padToWidth(buildBarLine(theme.boxSharp.topLeft, theme.boxSharp.topRight, header, headerMeta), lineWidth, bgFn),
|
|
58
|
+
);
|
|
51
59
|
|
|
52
60
|
const hasSections = sections.length > 0;
|
|
53
61
|
const normalizedSections = hasSections ? sections : [{ lines: [] }];
|
|
@@ -55,17 +63,22 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
55
63
|
for (let i = 0; i < normalizedSections.length; i++) {
|
|
56
64
|
const section = normalizedSections[i];
|
|
57
65
|
if (section.label) {
|
|
58
|
-
lines.push(
|
|
66
|
+
lines.push(
|
|
67
|
+
padToWidth(buildBarLine(theme.boxSharp.teeRight, theme.boxSharp.teeLeft, section.label), lineWidth, bgFn),
|
|
68
|
+
);
|
|
59
69
|
}
|
|
60
70
|
for (const line of section.lines) {
|
|
61
71
|
const text = truncateToWidth(line, contentWidth, theme.format.ellipsis);
|
|
62
|
-
|
|
72
|
+
const innerPadding = " ".repeat(Math.max(0, contentWidth - visibleWidth(text)));
|
|
73
|
+
const fullLine = `${contentPrefix}${text}${innerPadding}${contentSuffix}`;
|
|
74
|
+
lines.push(padToWidth(fullLine, lineWidth, bgFn));
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
const bottomLeft = border(`${theme.boxSharp.bottomLeft}${cap}`);
|
|
67
|
-
const
|
|
68
|
-
const
|
|
79
|
+
const bottomRight = border(theme.boxSharp.bottomRight);
|
|
80
|
+
const bottomFillCount = Math.max(0, lineWidth - visibleWidth(bottomLeft) - visibleWidth(bottomRight));
|
|
81
|
+
const bottomLine = `${bottomLeft}${border(h.repeat(bottomFillCount))}${bottomRight}`;
|
|
69
82
|
lines.push(padToWidth(bottomLine, lineWidth, bgFn));
|
|
70
83
|
|
|
71
84
|
return lines;
|