pi-readseek 0.7.7 → 0.7.8
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/README.md +28 -46
- package/dist/index.js +7333 -0
- package/package.json +8 -7
- package/index.ts +0 -147
- package/src/binary-detect.ts +0 -22
- package/src/check.ts +0 -92
- package/src/coerce-obvious-int.ts +0 -41
- package/src/confusable-hyphens.ts +0 -6
- package/src/def.ts +0 -143
- package/src/diff-data.ts +0 -302
- package/src/edit-classify.ts +0 -58
- package/src/edit-diff.ts +0 -359
- package/src/edit-output.ts +0 -110
- package/src/edit-render-helpers.ts +0 -141
- package/src/edit-syntax-validate.ts +0 -65
- package/src/edit.ts +0 -686
- package/src/file-map.ts +0 -17
- package/src/fs-error.ts +0 -33
- package/src/grep-budget.ts +0 -35
- package/src/grep-output.ts +0 -181
- package/src/grep-render-helpers.ts +0 -77
- package/src/grep-symbol-scope.ts +0 -184
- package/src/grep.ts +0 -608
- package/src/hashline.ts +0 -736
- package/src/hover.ts +0 -140
- package/src/path-utils.ts +0 -24
- package/src/pending-diff-preview.ts +0 -269
- package/src/read-local-bundle.ts +0 -75
- package/src/read-output.ts +0 -186
- package/src/read-render-helpers.ts +0 -104
- package/src/read.ts +0 -661
- package/src/readseek/constants.ts +0 -17
- package/src/readseek/enums.ts +0 -38
- package/src/readseek/formatter.ts +0 -350
- package/src/readseek/symbol-error-format.ts +0 -18
- package/src/readseek/symbol-lookup.ts +0 -240
- package/src/readseek/symbol-tree.ts +0 -21
- package/src/readseek/types.ts +0 -49
- package/src/readseek-client.ts +0 -1250
- package/src/readseek-params.ts +0 -39
- package/src/readseek-settings.ts +0 -263
- package/src/readseek-value.ts +0 -184
- package/src/refs-output.ts +0 -47
- package/src/refs.ts +0 -161
- package/src/register-tool.ts +0 -29
- package/src/rename.ts +0 -182
- package/src/replace-symbol.ts +0 -77
- package/src/runtime.ts +0 -3
- package/src/search-output.ts +0 -50
- package/src/search.ts +0 -214
- package/src/session-anchors.ts +0 -34
- package/src/stat-search-path.ts +0 -29
- package/src/syntax-validate-mode.ts +0 -15
- package/src/tool-prompt-metadata.ts +0 -107
- package/src/tool-types.ts +0 -12
- package/src/tui-diff-component.ts +0 -108
- package/src/tui-diff-renderer.ts +0 -92
- package/src/tui-render-utils.ts +0 -286
- package/src/view.ts +0 -158
- package/src/write.ts +0 -357
package/src/view.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_MAX_BYTES,
|
|
4
|
-
DEFAULT_MAX_LINES,
|
|
5
|
-
truncateHead,
|
|
6
|
-
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
8
|
-
import { Type } from "@sinclair/typebox";
|
|
9
|
-
|
|
10
|
-
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
11
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
12
|
-
import { classifyReadSeekFailure, readSeekView } from "./readseek-client.js";
|
|
13
|
-
import { buildToolErrorResult } from "./readseek-value.js";
|
|
14
|
-
import { filePathParam, optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
15
|
-
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
16
|
-
import {
|
|
17
|
-
clampLineToWidth,
|
|
18
|
-
clampLinesToWidth,
|
|
19
|
-
linkToolPath,
|
|
20
|
-
renderErrorResult,
|
|
21
|
-
renderPendingResult,
|
|
22
|
-
resolveRenderResultContext,
|
|
23
|
-
summaryLine,
|
|
24
|
-
} from "./tui-render-utils.js";
|
|
25
|
-
|
|
26
|
-
const NODE_KINDS = [
|
|
27
|
-
"artifact",
|
|
28
|
-
"footer",
|
|
29
|
-
"header",
|
|
30
|
-
"heading",
|
|
31
|
-
"marginal_label",
|
|
32
|
-
"page",
|
|
33
|
-
"page_number",
|
|
34
|
-
"paragraph",
|
|
35
|
-
"section",
|
|
36
|
-
"structural_section",
|
|
37
|
-
] as const;
|
|
38
|
-
|
|
39
|
-
const VIEW_PROMPT_METADATA = defineToolPromptMetadata({
|
|
40
|
-
promptUrl: new URL("../prompts/view.md", import.meta.url),
|
|
41
|
-
promptSnippet: "View the structure or selected content of an indexed PDF",
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
interface ViewParams {
|
|
45
|
-
path: string;
|
|
46
|
-
node?: string;
|
|
47
|
-
page?: number | string;
|
|
48
|
-
kind?: (typeof NODE_KINDS)[number];
|
|
49
|
-
depth?: number | string;
|
|
50
|
-
outline?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function executeView(opts: {
|
|
54
|
-
params: unknown;
|
|
55
|
-
signal: AbortSignal | undefined;
|
|
56
|
-
cwd: string;
|
|
57
|
-
}): Promise<any> {
|
|
58
|
-
const { params, signal, cwd } = opts;
|
|
59
|
-
const input = params as ViewParams;
|
|
60
|
-
const page = coerceObviousBase10Int(input.page, "page");
|
|
61
|
-
if (!page.ok || (page.value !== undefined && page.value < 1)) {
|
|
62
|
-
const message = page.ok
|
|
63
|
-
? `Invalid page: expected a positive integer, received ${page.value}.`
|
|
64
|
-
: page.message;
|
|
65
|
-
return buildToolErrorResult("view", "invalid-page", message, { path: input.path });
|
|
66
|
-
}
|
|
67
|
-
const depth = coerceObviousBase10Int(input.depth, "depth");
|
|
68
|
-
if (!depth.ok || (depth.value !== undefined && depth.value < 0)) {
|
|
69
|
-
const message = depth.ok
|
|
70
|
-
? `Invalid depth: expected a non-negative integer, received ${depth.value}.`
|
|
71
|
-
: depth.message;
|
|
72
|
-
return buildToolErrorResult("view", "invalid-depth", message, { path: input.path });
|
|
73
|
-
}
|
|
74
|
-
const node = input.node?.trim();
|
|
75
|
-
if (input.node !== undefined && !node) {
|
|
76
|
-
return buildToolErrorResult("view", "invalid-node", "Invalid node: expected a non-empty ID.", {
|
|
77
|
-
path: input.path,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const filePath = resolveToCwd(input.path, cwd);
|
|
82
|
-
try {
|
|
83
|
-
const output = await readSeekView(filePath, {
|
|
84
|
-
node,
|
|
85
|
-
page: page.value,
|
|
86
|
-
kind: input.kind,
|
|
87
|
-
depth: depth.value,
|
|
88
|
-
outline: input.outline,
|
|
89
|
-
signal,
|
|
90
|
-
});
|
|
91
|
-
const truncation = truncateHead(output, {
|
|
92
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
93
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
94
|
-
});
|
|
95
|
-
const notice = truncation.truncated
|
|
96
|
-
? "\n[… document view truncated; narrow it with page, node, kind, or depth]"
|
|
97
|
-
: "";
|
|
98
|
-
return {
|
|
99
|
-
content: [{ type: "text", text: `${truncation.content}${notice}` }],
|
|
100
|
-
details: {
|
|
101
|
-
readSeekValue: {
|
|
102
|
-
tool: "view",
|
|
103
|
-
path: filePath,
|
|
104
|
-
truncated: truncation.truncated,
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
} catch (error) {
|
|
109
|
-
const failure = classifyReadSeekFailure(error);
|
|
110
|
-
return buildToolErrorResult("view", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function registerViewTool(pi: ExtensionAPI) {
|
|
115
|
-
return registerReadSeekTool(pi, {
|
|
116
|
-
name: "readSeek_view",
|
|
117
|
-
label: "View",
|
|
118
|
-
description: VIEW_PROMPT_METADATA.description,
|
|
119
|
-
promptSnippet: VIEW_PROMPT_METADATA.promptSnippet,
|
|
120
|
-
promptGuidelines: VIEW_PROMPT_METADATA.promptGuidelines,
|
|
121
|
-
parameters: Type.Object({
|
|
122
|
-
path: filePathParam(),
|
|
123
|
-
node: Type.Optional(Type.String({ description: "Node ID to use as the view root" })),
|
|
124
|
-
page: optionalIntOrString("One-based source page"),
|
|
125
|
-
kind: Type.Optional(Type.Union(NODE_KINDS.map((kind) => Type.Literal(kind)), {
|
|
126
|
-
description: "Node kind filter",
|
|
127
|
-
})),
|
|
128
|
-
depth: optionalIntOrString("Maximum depth below the selected roots"),
|
|
129
|
-
outline: Type.Optional(Type.Boolean({ description: "Return outline nodes only" })),
|
|
130
|
-
}),
|
|
131
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
132
|
-
return executeView({ params, signal, cwd: ctx.cwd });
|
|
133
|
-
},
|
|
134
|
-
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
135
|
-
const context = rest[0] ?? {};
|
|
136
|
-
const cwd = context.cwd ?? process.cwd();
|
|
137
|
-
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
138
|
-
const text = `${theme.fg("toolTitle", theme.bold("view"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
139
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
140
|
-
},
|
|
141
|
-
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
142
|
-
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
143
|
-
if (isPartial) return renderPendingResult("pending view", width, theme);
|
|
144
|
-
const textContent = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
145
|
-
if (isError || result.isError) {
|
|
146
|
-
return renderErrorResult(textContent, { expanded, width, fallback: "view failed", theme });
|
|
147
|
-
}
|
|
148
|
-
const lines = textContent.split("\n").filter(Boolean).length;
|
|
149
|
-
let text = summaryLine(`loaded ${lines} document ${lines === 1 ? "line" : "lines"}`, {
|
|
150
|
-
hidden: !!textContent && !expanded,
|
|
151
|
-
theme,
|
|
152
|
-
style: "success",
|
|
153
|
-
});
|
|
154
|
-
if (expanded && textContent) text += `\n${textContent}`;
|
|
155
|
-
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
}
|
package/src/write.ts
DELETED
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, relative } from "node:path";
|
|
3
|
-
|
|
4
|
-
import { withFileMutationQueue, truncateHead, type ExtensionAPI, type ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
6
|
-
import { Type } from "@sinclair/typebox";
|
|
7
|
-
|
|
8
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
-
import { ensureHashInit, formatHashlineDisplay } from "./hashline.js";
|
|
10
|
-
import { buildReadSeekLine, buildReadSeekWarning, buildToolErrorResult, type ReadSeekLine, type ReadSeekWarning, type ToolErrorResult } from "./readseek-value.js";
|
|
11
|
-
import { looksLikeBinary } from "./binary-detect.js";
|
|
12
|
-
import { formatFsError } from "./fs-error.js";
|
|
13
|
-
import { getOrGenerateMap } from "./file-map.js";
|
|
14
|
-
import { formatFileMapWithBudget } from "./readseek/formatter.js";
|
|
15
|
-
|
|
16
|
-
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
17
|
-
import { buildPendingWritePreviewData, buildWritePreviewKey, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
|
|
18
|
-
import { generateCompactOrFullDiff, normalizeToLF, hasBareCarriageReturn } from "./edit-diff.js";
|
|
19
|
-
import { buildDiffData, type DiffData } from "./diff-data.js";
|
|
20
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
21
|
-
import { upsertDiffComponent, upsertTextComponent } from "./tui-diff-component.js";
|
|
22
|
-
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
23
|
-
import { filePathParam, mapParam, registerReadSeekTool } from "./register-tool.js";
|
|
24
|
-
|
|
25
|
-
const WRITE_PENDING_PREVIEW_STATE_KEY = "hashline-write-pending-preview";
|
|
26
|
-
|
|
27
|
-
const CONTENT_PREVIEW_MAX_LINES = 200;
|
|
28
|
-
|
|
29
|
-
function formatContentPreviewLines(content: string, theme: any): string[] {
|
|
30
|
-
const lines = content.split("\n");
|
|
31
|
-
// Drop the single trailing blank produced by a terminal newline so the
|
|
32
|
-
// preview reads naturally.
|
|
33
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
34
|
-
const shown = lines.slice(0, CONTENT_PREVIEW_MAX_LINES);
|
|
35
|
-
// Right-align line numbers so the body has a stable column for the content.
|
|
36
|
-
// The dim gutter (" N │ ") visually distinguishes the body from the
|
|
37
|
-
// "↳ created / pending create" header above it without re-introducing
|
|
38
|
-
// diff chrome (no +/- marker, no red/green tint).
|
|
39
|
-
const width = String(shown.length).length;
|
|
40
|
-
// Bind theme.fg so we keep its `this` (the Theme uses internal state); fall back
|
|
41
|
-
// to an identity tint when no theme is provided (e.g. in tests).
|
|
42
|
-
const fg = typeof theme?.fg === "function" ? (style: string, text: string) => theme.fg(style, text) : (_style: string, text: string) => text;
|
|
43
|
-
const formatted = shown.map((line, index) => {
|
|
44
|
-
const gutter = fg("dim", `${String(index + 1).padStart(width, " ")} │ `);
|
|
45
|
-
return ` ${gutter}${line}`;
|
|
46
|
-
});
|
|
47
|
-
if (lines.length > CONTENT_PREVIEW_MAX_LINES) {
|
|
48
|
-
formatted.push(` ${fg("dim", `… ${lines.length - CONTENT_PREVIEW_MAX_LINES} more lines not shown`)}`);
|
|
49
|
-
}
|
|
50
|
-
return formatted;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function pendingWritePreviewParts(summary: string, preview: PendingDiffPreviewResult | undefined, expanded: boolean, theme: any): { lines: string[]; diffData?: DiffData } {
|
|
54
|
-
if (!preview || preview.type !== "ok") return { lines: summary.split("\n") };
|
|
55
|
-
// Pure creates (write to a new file) have no "old" side, so a diff-shaped
|
|
56
|
-
// preview is just noise. Show the new file's content with a dim gutter of
|
|
57
|
-
// line numbers when expanded; otherwise just a Ctrl+O hint.
|
|
58
|
-
const hasOldSide = preview.data.fileExistedBeforeWrite;
|
|
59
|
-
const headerLine = summaryLine(preview.data.headerLabel, { hidden: !expanded });
|
|
60
|
-
if (!hasOldSide) {
|
|
61
|
-
const lines = [summary, headerLine];
|
|
62
|
-
if (expanded) lines.push(...formatContentPreviewLines(preview.data.nextContent, theme));
|
|
63
|
-
return { lines };
|
|
64
|
-
}
|
|
65
|
-
const diffData = buildDiffData({ path: preview.data.filePath, oldContent: preview.data.previousContent, newContent: preview.data.nextContent, diff: preview.data.diff });
|
|
66
|
-
return { lines: [summary, headerLine], diffData: expanded ? diffData : undefined };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
type WriteDiffFields = {
|
|
71
|
-
diff?: string;
|
|
72
|
-
diffData?: DiffData;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export interface WriteResult extends WriteDiffFields {
|
|
76
|
-
text: string;
|
|
77
|
-
warnings: string[];
|
|
78
|
-
writeState?: "created" | "overwritten";
|
|
79
|
-
readSeekValue: {
|
|
80
|
-
tool: "write";
|
|
81
|
-
path: string;
|
|
82
|
-
lines: ReadSeekLine[];
|
|
83
|
-
warnings: ReadSeekWarning[];
|
|
84
|
-
diff?: string;
|
|
85
|
-
diffData?: DiffData;
|
|
86
|
-
map?: { appended: boolean };
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
type ExecuteWriteResult = WriteResult | ToolErrorResult;
|
|
92
|
-
|
|
93
|
-
function isToolErrorResult(result: ExecuteWriteResult): result is ToolErrorResult {
|
|
94
|
-
return "isError" in result;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function readPreviousTextForDiff(filePath: string): Promise<string> {
|
|
98
|
-
try {
|
|
99
|
-
const previous = await readFile(filePath);
|
|
100
|
-
if (looksLikeBinary(previous)) return "";
|
|
101
|
-
return previous.toString("utf-8");
|
|
102
|
-
} catch {
|
|
103
|
-
return "";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function generateWriteDiff(previousContent: string, nextContent: string): { diff: string; firstChangedLine: number | undefined } {
|
|
108
|
-
if (previousContent !== "") return generateCompactOrFullDiff(previousContent, nextContent);
|
|
109
|
-
const normalizedNext = normalizeToLF(nextContent);
|
|
110
|
-
if (normalizedNext === "") return { diff: "", firstChangedLine: undefined };
|
|
111
|
-
const lines = normalizedNext.split("\n");
|
|
112
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
113
|
-
const width = String(lines.length).length;
|
|
114
|
-
return {
|
|
115
|
-
diff: lines.map((line, index) => `+${String(index + 1).padStart(width, " ")} ${line}`).join("\n"),
|
|
116
|
-
firstChangedLine: 1,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface WriteToolOptions {
|
|
121
|
-
onFileAnchored?: FileAnchoredCallback;
|
|
122
|
-
name?: string;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function buildWriteFsErrorResult(err: any, absolutePath: string) {
|
|
126
|
-
const { code, message } = formatFsError(err, "write-error");
|
|
127
|
-
return buildToolErrorResult("write", code, message, {
|
|
128
|
-
path: absolutePath,
|
|
129
|
-
extra: { lines: [], warnings: [] },
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export async function executeWrite(opts: {
|
|
134
|
-
path: string;
|
|
135
|
-
content: string;
|
|
136
|
-
map?: boolean;
|
|
137
|
-
cwd?: string;
|
|
138
|
-
}): Promise<ExecuteWriteResult> {
|
|
139
|
-
await ensureHashInit();
|
|
140
|
-
|
|
141
|
-
const { path: filePath, content, map: requestMap, cwd } = opts;
|
|
142
|
-
const warnings: string[] = [];
|
|
143
|
-
const readSeekWarnings: ReadSeekWarning[] = [];
|
|
144
|
-
|
|
145
|
-
if (hasBareCarriageReturn(content)) {
|
|
146
|
-
const message = "File content contains bare CR (\\r) line endings; readSeek_write refuses to emit anchors that readSeek_read/readSeek_edit would normalize differently.";
|
|
147
|
-
warnings.push(message);
|
|
148
|
-
readSeekWarnings.push(buildReadSeekWarning("bare-cr", message));
|
|
149
|
-
return buildToolErrorResult("write", "bare-cr", `Cannot write ${filePath}\n⚠️ ${message}`, {
|
|
150
|
-
path: filePath,
|
|
151
|
-
extra: { lines: [], warnings: readSeekWarnings },
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
if (looksLikeBinary(Buffer.from(content, "utf-8"))) {
|
|
155
|
-
const message = "File content appears to be binary.";
|
|
156
|
-
warnings.push(message);
|
|
157
|
-
readSeekWarnings.push(buildReadSeekWarning("binary-content", message));
|
|
158
|
-
return buildToolErrorResult("write", "binary-content", `Cannot write ${filePath}\n⚠️ ${message} — refusing to write.`, {
|
|
159
|
-
path: filePath,
|
|
160
|
-
extra: { lines: [], warnings: readSeekWarnings },
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const previousContent = await readPreviousTextForDiff(filePath);
|
|
165
|
-
const existedBeforeWrite = await access(filePath).then(() => true, () => false);
|
|
166
|
-
|
|
167
|
-
// Create parent directories
|
|
168
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
169
|
-
// Write file
|
|
170
|
-
await writeFile(filePath, content, "utf-8");
|
|
171
|
-
// Compute hashlines
|
|
172
|
-
const rawLines = content.split("\n");
|
|
173
|
-
const readSeekLines: ReadSeekLine[] = [];
|
|
174
|
-
const displayLines: string[] = [];
|
|
175
|
-
|
|
176
|
-
for (let i = 0; i < rawLines.length; i++) {
|
|
177
|
-
const lineNum = i + 1;
|
|
178
|
-
const readSeekLine = buildReadSeekLine(lineNum, rawLines[i]);
|
|
179
|
-
readSeekLines.push(readSeekLine);
|
|
180
|
-
displayLines.push(formatHashlineDisplay(lineNum, rawLines[i]));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const fullText = displayLines.join("\n");
|
|
184
|
-
const truncated = truncateHead(fullText);
|
|
185
|
-
let text = truncated.content;
|
|
186
|
-
if (truncated.truncated) {
|
|
187
|
-
if (truncated.truncatedBy === "lines") {
|
|
188
|
-
text += `\n[… ${truncated.totalLines - truncated.outputLines} more lines not shown — full anchors in readSeekValue]`;
|
|
189
|
-
} else {
|
|
190
|
-
text += "\n[… output truncated at 50 KB — full anchors in readSeekValue]";
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Optional structural map
|
|
195
|
-
let mapAppended = false;
|
|
196
|
-
if (requestMap) {
|
|
197
|
-
try {
|
|
198
|
-
const fileMap = await getOrGenerateMap(filePath);
|
|
199
|
-
if (fileMap) {
|
|
200
|
-
const mapText = formatFileMapWithBudget(fileMap);
|
|
201
|
-
if (mapText) {
|
|
202
|
-
text += "\n\n" + mapText;
|
|
203
|
-
mapAppended = true;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
// Map generation failure is non-fatal
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const displayPath = cwd ? relative(cwd, filePath) || filePath : filePath;
|
|
212
|
-
const normalizedPrevious = normalizeToLF(previousContent);
|
|
213
|
-
const normalizedNext = normalizeToLF(content);
|
|
214
|
-
const diffResult = generateWriteDiff(normalizedPrevious, normalizedNext);
|
|
215
|
-
const diffData = buildDiffData({
|
|
216
|
-
path: filePath,
|
|
217
|
-
oldContent: normalizedPrevious,
|
|
218
|
-
newContent: normalizedNext,
|
|
219
|
-
diff: diffResult.diff,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
text,
|
|
224
|
-
warnings,
|
|
225
|
-
writeState: existedBeforeWrite ? "overwritten" : "created",
|
|
226
|
-
diff: diffResult.diff,
|
|
227
|
-
diffData,
|
|
228
|
-
readSeekValue: {
|
|
229
|
-
tool: "write",
|
|
230
|
-
path: displayPath,
|
|
231
|
-
lines: readSeekLines,
|
|
232
|
-
warnings: readSeekWarnings,
|
|
233
|
-
diff: diffResult.diff,
|
|
234
|
-
diffData,
|
|
235
|
-
...(requestMap ? { map: { appended: mapAppended } } : {}),
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions = {}) {
|
|
241
|
-
const name = options.name ?? "readSeek_write";
|
|
242
|
-
const promptMetadata = defineToolPromptMetadata({
|
|
243
|
-
promptUrl: new URL("../prompts/write.md", import.meta.url),
|
|
244
|
-
promptSnippet: "Create or replace a complete file with edit anchors",
|
|
245
|
-
registeredName: name,
|
|
246
|
-
});
|
|
247
|
-
const tool = registerReadSeekTool(pi, {
|
|
248
|
-
name,
|
|
249
|
-
label: "write",
|
|
250
|
-
description: promptMetadata.description,
|
|
251
|
-
promptSnippet: promptMetadata.promptSnippet,
|
|
252
|
-
promptGuidelines: promptMetadata.promptGuidelines,
|
|
253
|
-
parameters: Type.Object({
|
|
254
|
-
path: filePathParam(),
|
|
255
|
-
content: Type.String({ description: "Complete file content" }),
|
|
256
|
-
map: mapParam(),
|
|
257
|
-
}),
|
|
258
|
-
async execute(_toolCallId: string, params: { path: string; content: string; map?: boolean }, _signal: AbortSignal | undefined, _onUpdate: any, ctx: any): Promise<any> {
|
|
259
|
-
const cwd = ctx?.cwd ?? process.cwd();
|
|
260
|
-
const absolutePath = resolveToCwd(params.path, cwd);
|
|
261
|
-
try {
|
|
262
|
-
return await withFileMutationQueue(absolutePath, async () => {
|
|
263
|
-
let result: ExecuteWriteResult;
|
|
264
|
-
try {
|
|
265
|
-
result = await executeWrite({
|
|
266
|
-
path: absolutePath,
|
|
267
|
-
content: params.content,
|
|
268
|
-
map: params.map,
|
|
269
|
-
cwd,
|
|
270
|
-
});
|
|
271
|
-
} catch (err: any) {
|
|
272
|
-
return buildWriteFsErrorResult(err, absolutePath);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (isToolErrorResult(result)) return result;
|
|
276
|
-
|
|
277
|
-
if (result.readSeekValue.lines.length > 0) {
|
|
278
|
-
options.onFileAnchored?.(absolutePath);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return {
|
|
282
|
-
content: [{ type: "text" as const, text: result.text }],
|
|
283
|
-
details: {
|
|
284
|
-
...(result.diff !== undefined ? { diff: result.diff } : {}),
|
|
285
|
-
...(result.diffData !== undefined ? { diffData: result.diffData } : {}),
|
|
286
|
-
...(result.writeState ? { writeState: result.writeState } : {}),
|
|
287
|
-
readSeekValue: result.readSeekValue,
|
|
288
|
-
warnings: result.warnings,
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
});
|
|
292
|
-
} catch (err: any) {
|
|
293
|
-
return buildWriteFsErrorResult(err, absolutePath);
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
renderCall(args: any, theme: any, context: any = {}) {
|
|
297
|
-
const { path, content } = args as { path: string; content?: string };
|
|
298
|
-
const cwd = context.cwd ?? process.cwd();
|
|
299
|
-
const label = renderToolLabel(theme, "write");
|
|
300
|
-
const lineCount = typeof content === "string" ? content.split("\n").length : 0;
|
|
301
|
-
const bytes = typeof content === "string" ? Buffer.byteLength(content, "utf8") : 0;
|
|
302
|
-
const renderedPath = typeof path === "string"
|
|
303
|
-
? linkToolPath(theme.fg("muted", path), path, cwd)
|
|
304
|
-
: theme.fg("toolOutput", "...");
|
|
305
|
-
let text = clampLineToWidth(`${label} ${renderedPath}${typeof content === "string" ? ` (${lineCount} ${lineCount === 1 ? "line" : "lines"} • ${bytes} B)` : ""}`, context.width);
|
|
306
|
-
// Once execution has started, the pending preview's only job is done:
|
|
307
|
-
// renderResult will carry the story ("↳ created" / "↳ overwritten" with
|
|
308
|
-
// expandable content or diff). Showing the "↳ pending…" sub-line and
|
|
309
|
-
// its preview alongside the final result is just duplicate noise — the
|
|
310
|
-
// pre-execution state can no longer change in any meaningful way.
|
|
311
|
-
if (context.executionStarted) {
|
|
312
|
-
return upsertTextComponent(context.lastComponent, text);
|
|
313
|
-
}
|
|
314
|
-
const previewKey = buildWritePreviewKey(args ?? {});
|
|
315
|
-
const preview = resolvePendingDiffPreview(context, WRITE_PENDING_PREVIEW_STATE_KEY, previewKey, () => buildPendingWritePreviewData(args ?? {}, context.cwd ?? process.cwd()));
|
|
316
|
-
const expanded = !!context.expanded;
|
|
317
|
-
const parts = pendingWritePreviewParts(text, preview, expanded, theme);
|
|
318
|
-
if (parts.diffData) {
|
|
319
|
-
return upsertDiffComponent(context.lastComponent, { prefixLines: parts.lines, diffData: parts.diffData, theme, expanded: true });
|
|
320
|
-
}
|
|
321
|
-
return upsertTextComponent(context.lastComponent, clampLinesToWidth(parts.lines, context.width).join("\n"));
|
|
322
|
-
},
|
|
323
|
-
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
324
|
-
const { isPartial, expanded, width, context } = resolveRenderResultContext(options, rest);
|
|
325
|
-
if (isPartial) return renderPendingResult("pending write", width, theme);
|
|
326
|
-
const details = result.details ?? {};
|
|
327
|
-
const output = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
328
|
-
if (result.isError || details.readSeekValue?.ok === false) {
|
|
329
|
-
return renderErrorResult(output, { expanded, width, fallback: "write failed", theme });
|
|
330
|
-
}
|
|
331
|
-
const diffData = details.diffData;
|
|
332
|
-
const state = details.writeState === "overwritten" ? "overwritten" : "created";
|
|
333
|
-
// Pure creates: render the new file's contents on expand (no diff chrome)
|
|
334
|
-
// instead of a diff body — every line is an add, so the gutter, line
|
|
335
|
-
// numbers, and red/green tinting are noise.
|
|
336
|
-
if (state === "created") {
|
|
337
|
-
const readSeekLines = (details.readSeekValue?.lines ?? []) as Array<{ raw: string }>;
|
|
338
|
-
const hasContent = readSeekLines.length > 0;
|
|
339
|
-
const header = summaryLine(state, { hidden: hasContent && !expanded, theme, style: "success" });
|
|
340
|
-
const lines = header.split("\n");
|
|
341
|
-
if (expanded && hasContent) {
|
|
342
|
-
const content = readSeekLines.map((l) => l.raw).join("\n");
|
|
343
|
-
lines.push(...formatContentPreviewLines(content, theme));
|
|
344
|
-
}
|
|
345
|
-
return new Text(clampLinesToWidth(lines, width).join("\n"), 0, 0);
|
|
346
|
-
}
|
|
347
|
-
// Overwrite: the old vs new comparison still carries signal — keep the diff UI.
|
|
348
|
-
const hasExpandableDiff = !!diffData;
|
|
349
|
-
let text = summaryLine(state, { hidden: hasExpandableDiff && !expanded, theme, style: "success" });
|
|
350
|
-
if (expanded && hasExpandableDiff) {
|
|
351
|
-
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
352
|
-
}
|
|
353
|
-
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
return tool;
|
|
357
|
-
}
|