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.
Files changed (60) hide show
  1. package/README.md +28 -46
  2. package/dist/index.js +7333 -0
  3. package/package.json +8 -7
  4. package/index.ts +0 -147
  5. package/src/binary-detect.ts +0 -22
  6. package/src/check.ts +0 -92
  7. package/src/coerce-obvious-int.ts +0 -41
  8. package/src/confusable-hyphens.ts +0 -6
  9. package/src/def.ts +0 -143
  10. package/src/diff-data.ts +0 -302
  11. package/src/edit-classify.ts +0 -58
  12. package/src/edit-diff.ts +0 -359
  13. package/src/edit-output.ts +0 -110
  14. package/src/edit-render-helpers.ts +0 -141
  15. package/src/edit-syntax-validate.ts +0 -65
  16. package/src/edit.ts +0 -686
  17. package/src/file-map.ts +0 -17
  18. package/src/fs-error.ts +0 -33
  19. package/src/grep-budget.ts +0 -35
  20. package/src/grep-output.ts +0 -181
  21. package/src/grep-render-helpers.ts +0 -77
  22. package/src/grep-symbol-scope.ts +0 -184
  23. package/src/grep.ts +0 -608
  24. package/src/hashline.ts +0 -736
  25. package/src/hover.ts +0 -140
  26. package/src/path-utils.ts +0 -24
  27. package/src/pending-diff-preview.ts +0 -269
  28. package/src/read-local-bundle.ts +0 -75
  29. package/src/read-output.ts +0 -186
  30. package/src/read-render-helpers.ts +0 -104
  31. package/src/read.ts +0 -661
  32. package/src/readseek/constants.ts +0 -17
  33. package/src/readseek/enums.ts +0 -38
  34. package/src/readseek/formatter.ts +0 -350
  35. package/src/readseek/symbol-error-format.ts +0 -18
  36. package/src/readseek/symbol-lookup.ts +0 -240
  37. package/src/readseek/symbol-tree.ts +0 -21
  38. package/src/readseek/types.ts +0 -49
  39. package/src/readseek-client.ts +0 -1250
  40. package/src/readseek-params.ts +0 -39
  41. package/src/readseek-settings.ts +0 -263
  42. package/src/readseek-value.ts +0 -184
  43. package/src/refs-output.ts +0 -47
  44. package/src/refs.ts +0 -161
  45. package/src/register-tool.ts +0 -29
  46. package/src/rename.ts +0 -182
  47. package/src/replace-symbol.ts +0 -77
  48. package/src/runtime.ts +0 -3
  49. package/src/search-output.ts +0 -50
  50. package/src/search.ts +0 -214
  51. package/src/session-anchors.ts +0 -34
  52. package/src/stat-search-path.ts +0 -29
  53. package/src/syntax-validate-mode.ts +0 -15
  54. package/src/tool-prompt-metadata.ts +0 -107
  55. package/src/tool-types.ts +0 -12
  56. package/src/tui-diff-component.ts +0 -108
  57. package/src/tui-diff-renderer.ts +0 -92
  58. package/src/tui-render-utils.ts +0 -286
  59. package/src/view.ts +0 -158
  60. 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
- }