@xynogen/pix-pretty 1.2.0 → 1.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/README.md +9 -4
- package/package.json +2 -3
- package/src/ansi.ts +0 -10
- package/src/commands/fff.ts +60 -0
- package/src/diff-render.ts +92 -16
- package/src/image.ts +0 -3
- package/src/index.ts +88 -1452
- package/src/tools/bash.ts +154 -0
- package/src/tools/context.ts +19 -0
- package/src/tools/edit.ts +291 -0
- package/src/tools/find.ts +158 -0
- package/src/tools/grep.ts +202 -0
- package/src/tools/ls.ts +111 -0
- package/src/tools/multi-grep.ts +328 -0
- package/src/tools/read.ts +177 -0
- package/src/tools/write.ts +231 -0
- package/src/tsconfig.json +1 -1
- package/src/types.ts +30 -1
- package/src/utils.ts +45 -2
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolUpdateCallback,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ReadToolInput,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { FG_DIM, RST } from "../ansi.js";
|
|
8
|
+
import { MAX_PREVIEW_LINES } from "../config.js";
|
|
9
|
+
import { fileIcon } from "../icons.js";
|
|
10
|
+
import { renderFileContent } from "../renderers.js";
|
|
11
|
+
import type {
|
|
12
|
+
PiPrettyApi,
|
|
13
|
+
ReadParams,
|
|
14
|
+
RenderContextLike,
|
|
15
|
+
ThemeLike,
|
|
16
|
+
ToolFactory,
|
|
17
|
+
ToolResultLike,
|
|
18
|
+
} from "../types.js";
|
|
19
|
+
import {
|
|
20
|
+
fillToolBackground,
|
|
21
|
+
getTextContent,
|
|
22
|
+
humanSize,
|
|
23
|
+
isImageContent,
|
|
24
|
+
isTextContent,
|
|
25
|
+
normalizeLineEndings,
|
|
26
|
+
renderToolError,
|
|
27
|
+
setResultDetails,
|
|
28
|
+
} from "../utils.js";
|
|
29
|
+
import type { ToolContext } from "./context.js";
|
|
30
|
+
|
|
31
|
+
export function registerReadTool(
|
|
32
|
+
pi: PiPrettyApi,
|
|
33
|
+
createReadTool: ToolFactory<ReadToolInput>,
|
|
34
|
+
ctx: ToolContext,
|
|
35
|
+
): void {
|
|
36
|
+
const { cwd, sp, TextComponent } = ctx;
|
|
37
|
+
const origRead = createReadTool(cwd);
|
|
38
|
+
|
|
39
|
+
pi.registerTool({
|
|
40
|
+
...origRead,
|
|
41
|
+
name: "read",
|
|
42
|
+
|
|
43
|
+
async execute(
|
|
44
|
+
tid: string,
|
|
45
|
+
params: ReadParams,
|
|
46
|
+
sig: AbortSignal | undefined,
|
|
47
|
+
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
48
|
+
toolCtx: ExtensionContext,
|
|
49
|
+
) {
|
|
50
|
+
const result = (await origRead.execute(
|
|
51
|
+
tid,
|
|
52
|
+
params,
|
|
53
|
+
sig,
|
|
54
|
+
upd,
|
|
55
|
+
toolCtx,
|
|
56
|
+
)) as ToolResultLike;
|
|
57
|
+
|
|
58
|
+
const fp = params.path ?? "";
|
|
59
|
+
const offset = params.offset ?? 1;
|
|
60
|
+
|
|
61
|
+
const imageBlock = result.content?.find(isImageContent);
|
|
62
|
+
if (imageBlock) {
|
|
63
|
+
setResultDetails(result, {
|
|
64
|
+
_type: "readImage",
|
|
65
|
+
filePath: fp,
|
|
66
|
+
data: imageBlock.data,
|
|
67
|
+
mimeType: imageBlock.mimeType ?? "image/png",
|
|
68
|
+
});
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const textContent = getTextContent(result);
|
|
73
|
+
if (textContent && fp) {
|
|
74
|
+
const normalizedContent = normalizeLineEndings(textContent);
|
|
75
|
+
const lineCount = normalizedContent.split("\n").length;
|
|
76
|
+
setResultDetails(result, {
|
|
77
|
+
_type: "readFile",
|
|
78
|
+
filePath: fp,
|
|
79
|
+
content: normalizedContent,
|
|
80
|
+
offset,
|
|
81
|
+
lineCount,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
renderCall(
|
|
89
|
+
args: ReadParams,
|
|
90
|
+
theme: ThemeLike,
|
|
91
|
+
renderCtx: RenderContextLike,
|
|
92
|
+
) {
|
|
93
|
+
const fp = args.path ?? "";
|
|
94
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
95
|
+
const offset = args.offset
|
|
96
|
+
? ` ${theme.fg("muted", `from line ${args.offset}`)}`
|
|
97
|
+
: "";
|
|
98
|
+
const limit = args.limit
|
|
99
|
+
? ` ${theme.fg("muted", `(${args.limit} lines)`)}`
|
|
100
|
+
: "";
|
|
101
|
+
text.setText(
|
|
102
|
+
fillToolBackground(
|
|
103
|
+
`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
return text;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
renderResult(
|
|
110
|
+
result: ToolResultLike,
|
|
111
|
+
_opt: unknown,
|
|
112
|
+
theme: ThemeLike,
|
|
113
|
+
renderCtx: RenderContextLike,
|
|
114
|
+
) {
|
|
115
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
116
|
+
|
|
117
|
+
if (renderCtx.isError) {
|
|
118
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const d = result.details as Record<string, unknown> | undefined;
|
|
123
|
+
|
|
124
|
+
if (d?._type === "readImage") {
|
|
125
|
+
const byteSize = Math.ceil(((d.data as string).length * 3) / 4);
|
|
126
|
+
text.setText(
|
|
127
|
+
fillToolBackground(
|
|
128
|
+
` ${fileIcon(d.filePath as string)}${FG_DIM}${d.mimeType ?? "image"} · ${humanSize(byteSize)}${RST}`,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (d?._type === "readFile" && d.content) {
|
|
135
|
+
const key = `read:${d.filePath}:${d.offset}:${d.lineCount}:${process.stdout.columns ?? 80}`;
|
|
136
|
+
if (renderCtx.state._rk !== key) {
|
|
137
|
+
renderCtx.state._rk = key;
|
|
138
|
+
const info = `${FG_DIM}${d.lineCount} lines${RST}`;
|
|
139
|
+
renderCtx.state._rt = fillToolBackground(` ${info}`);
|
|
140
|
+
|
|
141
|
+
const maxShow = renderCtx.expanded
|
|
142
|
+
? (d.lineCount as number)
|
|
143
|
+
: MAX_PREVIEW_LINES;
|
|
144
|
+
renderFileContent(
|
|
145
|
+
d.content as string,
|
|
146
|
+
d.filePath as string,
|
|
147
|
+
d.offset as number,
|
|
148
|
+
maxShow,
|
|
149
|
+
)
|
|
150
|
+
.then((rendered: string) => {
|
|
151
|
+
if (renderCtx.state._rk !== key) return;
|
|
152
|
+
renderCtx.state._rt = fillToolBackground(
|
|
153
|
+
` ${info}\n${rendered}`,
|
|
154
|
+
);
|
|
155
|
+
renderCtx.invalidate();
|
|
156
|
+
})
|
|
157
|
+
.catch(() => {});
|
|
158
|
+
}
|
|
159
|
+
text.setText(
|
|
160
|
+
renderCtx.state._rt ??
|
|
161
|
+
fillToolBackground(` ${FG_DIM}${d.lineCount} lines${RST}`),
|
|
162
|
+
);
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fallback = result.content?.[0];
|
|
167
|
+
const fallbackText =
|
|
168
|
+
fallback && isTextContent(fallback) ? fallback.text : "read";
|
|
169
|
+
text.setText(
|
|
170
|
+
fillToolBackground(
|
|
171
|
+
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
return text;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import type {
|
|
3
|
+
AgentToolUpdateCallback,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
ToolRenderResultOptions,
|
|
6
|
+
WriteToolInput,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
import { MAX_RENDER_LINES } from "../config.js";
|
|
10
|
+
import { parseDiff } from "../diff.js";
|
|
11
|
+
import {
|
|
12
|
+
diffThemeCacheKey,
|
|
13
|
+
renderSplit,
|
|
14
|
+
resolveDiffColors,
|
|
15
|
+
summarize,
|
|
16
|
+
} from "../diff-render.js";
|
|
17
|
+
import { hlBlock } from "../highlight.js";
|
|
18
|
+
import { lang } from "../lang.js";
|
|
19
|
+
import type {
|
|
20
|
+
PiPrettyApi,
|
|
21
|
+
RenderContextLike,
|
|
22
|
+
ThemeLike,
|
|
23
|
+
ToolFactory,
|
|
24
|
+
ToolResultLike,
|
|
25
|
+
WriteParams,
|
|
26
|
+
WriteRenderState,
|
|
27
|
+
} from "../types.js";
|
|
28
|
+
import {
|
|
29
|
+
fillToolBackground,
|
|
30
|
+
getTextContent,
|
|
31
|
+
isTextContent,
|
|
32
|
+
renderToolError,
|
|
33
|
+
setResultDetails,
|
|
34
|
+
termW,
|
|
35
|
+
} from "../utils.js";
|
|
36
|
+
import type { ToolContext } from "./context.js";
|
|
37
|
+
|
|
38
|
+
export function registerWriteTool(
|
|
39
|
+
pi: PiPrettyApi,
|
|
40
|
+
createWriteTool: ToolFactory<WriteToolInput>,
|
|
41
|
+
ctx: ToolContext,
|
|
42
|
+
trackInvalidator: (id: string, inv: () => void) => void,
|
|
43
|
+
): void {
|
|
44
|
+
const { cwd, sp, TextComponent } = ctx;
|
|
45
|
+
const origWrite = createWriteTool(cwd);
|
|
46
|
+
|
|
47
|
+
pi.registerTool({
|
|
48
|
+
...origWrite,
|
|
49
|
+
name: "write",
|
|
50
|
+
|
|
51
|
+
async execute(
|
|
52
|
+
tid: string,
|
|
53
|
+
params: WriteParams,
|
|
54
|
+
sig: AbortSignal | undefined,
|
|
55
|
+
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
56
|
+
toolCtx: ExtensionContext,
|
|
57
|
+
) {
|
|
58
|
+
const fp = params.path ?? params.file_path ?? "";
|
|
59
|
+
let old: string | null = null;
|
|
60
|
+
try {
|
|
61
|
+
if (fp && existsSync(fp)) old = readFileSync(fp, "utf-8");
|
|
62
|
+
} catch {
|
|
63
|
+
old = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = (await origWrite.execute(
|
|
67
|
+
tid,
|
|
68
|
+
params as unknown as Parameters<typeof origWrite.execute>[1],
|
|
69
|
+
sig,
|
|
70
|
+
upd,
|
|
71
|
+
toolCtx,
|
|
72
|
+
)) as ToolResultLike;
|
|
73
|
+
const content = params.content ?? "";
|
|
74
|
+
|
|
75
|
+
if (old !== null && old !== content) {
|
|
76
|
+
const diff = parseDiff(old, content);
|
|
77
|
+
setResultDetails(result, {
|
|
78
|
+
_type: "diff",
|
|
79
|
+
summary: summarize(diff.added, diff.removed),
|
|
80
|
+
oldContent: old,
|
|
81
|
+
newContent: content,
|
|
82
|
+
language: lang(fp),
|
|
83
|
+
});
|
|
84
|
+
} else if (old === null) {
|
|
85
|
+
setResultDetails(result, {
|
|
86
|
+
_type: "new",
|
|
87
|
+
lines: content ? content.split("\n").length : 0,
|
|
88
|
+
content,
|
|
89
|
+
filePath: fp,
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
setResultDetails(result, { _type: "noChange" });
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
renderCall(
|
|
98
|
+
args: WriteParams,
|
|
99
|
+
theme: ThemeLike,
|
|
100
|
+
renderCtx: RenderContextLike<WriteRenderState>,
|
|
101
|
+
) {
|
|
102
|
+
const fp = args?.path ?? args?.file_path ?? "";
|
|
103
|
+
const isNew = !fp || !existsSync(fp);
|
|
104
|
+
const label = isNew ? "create" : "write";
|
|
105
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
106
|
+
const hdr = `${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("accent", sp(fp))}`;
|
|
107
|
+
|
|
108
|
+
if (args?.content && isNew) {
|
|
109
|
+
const previewKey = `create:${diffThemeCacheKey(theme)}:${fp}:${String(args.content).length}`;
|
|
110
|
+
if (renderCtx.state._previewKey !== previewKey) {
|
|
111
|
+
renderCtx.state._previewKey = previewKey;
|
|
112
|
+
renderCtx.state._previewText = hdr;
|
|
113
|
+
const lg = lang(fp);
|
|
114
|
+
hlBlock(String(args.content), lg)
|
|
115
|
+
.then((lines) => {
|
|
116
|
+
if (renderCtx.state._previewKey !== previewKey) return;
|
|
117
|
+
const maxShow = renderCtx.expanded ? lines.length : 16;
|
|
118
|
+
const preview = lines.slice(0, maxShow).join("\n");
|
|
119
|
+
const rem = lines.length - maxShow;
|
|
120
|
+
let out = `${hdr}\n\n${preview}`;
|
|
121
|
+
if (rem > 0)
|
|
122
|
+
out += `\n${theme.fg("muted", `… (${rem} more lines, ${lines.length} total)`)}`;
|
|
123
|
+
renderCtx.state._previewText = out;
|
|
124
|
+
renderCtx.invalidate();
|
|
125
|
+
})
|
|
126
|
+
.catch(() => {});
|
|
127
|
+
}
|
|
128
|
+
text.setText(renderCtx.state._previewText ?? hdr);
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
text.setText(fillToolBackground(hdr));
|
|
133
|
+
return text;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
renderResult(
|
|
137
|
+
result: ToolResultLike,
|
|
138
|
+
_opt: ToolRenderResultOptions,
|
|
139
|
+
theme: ThemeLike,
|
|
140
|
+
renderCtx: RenderContextLike<WriteRenderState>,
|
|
141
|
+
) {
|
|
142
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
143
|
+
if (renderCtx.isError) {
|
|
144
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
const d = result.details as Record<string, unknown> | undefined;
|
|
148
|
+
|
|
149
|
+
if (d?._type === "diff") {
|
|
150
|
+
const key = `wd:${diffThemeCacheKey(theme)}:${termW()}:${d.summary}:${(d.newContent as string).length}:${d.language ?? ""}`;
|
|
151
|
+
if (renderCtx.toolCallId)
|
|
152
|
+
trackInvalidator(renderCtx.toolCallId, renderCtx.invalidate);
|
|
153
|
+
if (renderCtx.state._wdk !== key) {
|
|
154
|
+
renderCtx.state._wdk = key;
|
|
155
|
+
renderCtx.state._wdt = ` ${d.summary}\n${theme.fg("muted", " rendering diff…")}`;
|
|
156
|
+
const dc = resolveDiffColors(theme);
|
|
157
|
+
const diff = parseDiff(
|
|
158
|
+
d.oldContent as string,
|
|
159
|
+
d.newContent as string,
|
|
160
|
+
);
|
|
161
|
+
renderSplit(
|
|
162
|
+
diff,
|
|
163
|
+
d.language as string | undefined,
|
|
164
|
+
MAX_RENDER_LINES,
|
|
165
|
+
dc,
|
|
166
|
+
)
|
|
167
|
+
.then((rendered) => {
|
|
168
|
+
if (renderCtx.state._wdk !== key) return;
|
|
169
|
+
renderCtx.state._wdt = ` ${d.summary}\n${rendered}`;
|
|
170
|
+
renderCtx.invalidate();
|
|
171
|
+
})
|
|
172
|
+
.catch(() => {
|
|
173
|
+
if (renderCtx.state._wdk !== key) return;
|
|
174
|
+
renderCtx.state._wdt = ` ${d.summary}`;
|
|
175
|
+
renderCtx.invalidate();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
text.setText(renderCtx.state._wdt ?? ` ${d.summary}`);
|
|
179
|
+
return text;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (d?._type === "noChange") {
|
|
183
|
+
text.setText(
|
|
184
|
+
fillToolBackground(` ${theme.fg("muted", "✓ no changes")}`),
|
|
185
|
+
);
|
|
186
|
+
return text;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (d?._type === "new") {
|
|
190
|
+
const {
|
|
191
|
+
lines: lineCount,
|
|
192
|
+
content: rawContent,
|
|
193
|
+
filePath: fp,
|
|
194
|
+
} = d as { lines: number; content: string; filePath: string };
|
|
195
|
+
const base = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`;
|
|
196
|
+
const pk = `nf:${diffThemeCacheKey(theme)}:${fp}:${lineCount}`;
|
|
197
|
+
if (renderCtx.state._nfk !== pk) {
|
|
198
|
+
renderCtx.state._nfk = pk;
|
|
199
|
+
renderCtx.state._nft = base;
|
|
200
|
+
if (rawContent) {
|
|
201
|
+
hlBlock(rawContent, lang(fp))
|
|
202
|
+
.then((hlLines) => {
|
|
203
|
+
if (renderCtx.state._nfk !== pk) return;
|
|
204
|
+
const maxShow = renderCtx.expanded ? hlLines.length : 12;
|
|
205
|
+
const preview = hlLines.slice(0, maxShow).join("\n");
|
|
206
|
+
const rem = hlLines.length - maxShow;
|
|
207
|
+
let out = `${base}\n${preview}`;
|
|
208
|
+
if (rem > 0)
|
|
209
|
+
out += `\n${theme.fg("muted", ` … ${rem} more lines`)}`;
|
|
210
|
+
renderCtx.state._nft = out;
|
|
211
|
+
renderCtx.invalidate();
|
|
212
|
+
})
|
|
213
|
+
.catch(() => {});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
text.setText(renderCtx.state._nft ?? base);
|
|
217
|
+
return text;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const fallback = result.content?.[0];
|
|
221
|
+
const fallbackText =
|
|
222
|
+
fallback && isTextContent(fallback) ? fallback.text : "written";
|
|
223
|
+
text.setText(
|
|
224
|
+
fillToolBackground(
|
|
225
|
+
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
return text;
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
package/src/tsconfig.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -98,6 +98,8 @@ export type RenderContextLike<
|
|
|
98
98
|
expanded: boolean;
|
|
99
99
|
isError: boolean;
|
|
100
100
|
invalidate: () => void;
|
|
101
|
+
/** Stable id for this tool execution — used to key resize invalidators. */
|
|
102
|
+
toolCallId?: string;
|
|
101
103
|
};
|
|
102
104
|
|
|
103
105
|
type SessionContextLike = ExtensionContext;
|
|
@@ -211,7 +213,12 @@ export type MultiGrepParams = {
|
|
|
211
213
|
|
|
212
214
|
export type GrepRenderState = { _gk?: string; _gt?: string };
|
|
213
215
|
|
|
214
|
-
export type EditRenderState = {
|
|
216
|
+
export type EditRenderState = {
|
|
217
|
+
_pk?: string;
|
|
218
|
+
_pt?: string;
|
|
219
|
+
_edk?: string;
|
|
220
|
+
_edt?: string;
|
|
221
|
+
};
|
|
215
222
|
|
|
216
223
|
export type WriteRenderState = {
|
|
217
224
|
_previewKey?: string;
|
|
@@ -258,6 +265,7 @@ export type RenderDetails =
|
|
|
258
265
|
| GrepResultDetails
|
|
259
266
|
| EditInfoDetails
|
|
260
267
|
| MultiEditInfoDetails
|
|
268
|
+
| EditDiffDetails
|
|
261
269
|
| WriteDiffDetails
|
|
262
270
|
| WriteNewDetails
|
|
263
271
|
| WriteNoChangeDetails;
|
|
@@ -267,6 +275,11 @@ export type EditInfoDetails = {
|
|
|
267
275
|
_type: "editInfo";
|
|
268
276
|
summary: string;
|
|
269
277
|
editLine: number;
|
|
278
|
+
/** Full diff payload for split-view rendering */
|
|
279
|
+
oldContent: string;
|
|
280
|
+
newContent: string;
|
|
281
|
+
language: string | undefined;
|
|
282
|
+
filePath: string;
|
|
270
283
|
};
|
|
271
284
|
|
|
272
285
|
export type MultiEditInfoDetails = {
|
|
@@ -274,6 +287,13 @@ export type MultiEditInfoDetails = {
|
|
|
274
287
|
summary: string;
|
|
275
288
|
editCount: number;
|
|
276
289
|
diffLineCount: number;
|
|
290
|
+
/** Per-operation diffs for split-view rendering */
|
|
291
|
+
ops: Array<{
|
|
292
|
+
oldContent: string;
|
|
293
|
+
newContent: string;
|
|
294
|
+
language: string | undefined;
|
|
295
|
+
filePath: string;
|
|
296
|
+
}>;
|
|
277
297
|
};
|
|
278
298
|
|
|
279
299
|
export type WriteDiffDetails = {
|
|
@@ -293,6 +313,15 @@ export type WriteNewDetails = {
|
|
|
293
313
|
|
|
294
314
|
export type WriteNoChangeDetails = { _type: "noChange" };
|
|
295
315
|
|
|
316
|
+
export type EditDiffDetails = {
|
|
317
|
+
_type: "editDiff";
|
|
318
|
+
summary: string;
|
|
319
|
+
oldContent: string;
|
|
320
|
+
newContent: string;
|
|
321
|
+
language: string | undefined;
|
|
322
|
+
filePath: string;
|
|
323
|
+
};
|
|
324
|
+
|
|
296
325
|
export interface PiPrettyDeps {
|
|
297
326
|
sdk: PiPrettySdk;
|
|
298
327
|
TextComponent: TextComponentCtor;
|
package/src/utils.ts
CHANGED
|
@@ -48,7 +48,14 @@ export function fillToolBackground(text: string, bg = BG_BASE): string {
|
|
|
48
48
|
.join("\n");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
let _cachedTermW: number | undefined;
|
|
52
|
+
|
|
53
|
+
/** Read terminal width — checks all available sources in priority order.
|
|
54
|
+
* Falls back to querying the controlling tty via fd 1/2/stdin ioctl.
|
|
55
|
+
* Result is cached and invalidated on SIGWINCH / stdout resize. */
|
|
51
56
|
export function termW(): number {
|
|
57
|
+
if (_cachedTermW !== undefined) return _cachedTermW;
|
|
58
|
+
|
|
52
59
|
const stderrWithColumns = process.stderr as NodeJS.WriteStream & {
|
|
53
60
|
columns?: number;
|
|
54
61
|
};
|
|
@@ -56,8 +63,44 @@ export function termW(): number {
|
|
|
56
63
|
process.stdout.columns ||
|
|
57
64
|
stderrWithColumns.columns ||
|
|
58
65
|
Number.parseInt(process.env.COLUMNS ?? "", 10) ||
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
_readTtyColumns() ||
|
|
67
|
+
120;
|
|
68
|
+
_cachedTermW = Math.max(1, Math.min(raw, 210));
|
|
69
|
+
|
|
70
|
+
// Invalidate on resize so next call re-reads
|
|
71
|
+
process.stdout.once("resize", () => {
|
|
72
|
+
_cachedTermW = undefined;
|
|
73
|
+
});
|
|
74
|
+
process.stdin.once("resize", () => {
|
|
75
|
+
_cachedTermW = undefined;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return _cachedTermW;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Synchronously query the tty size via Node's built-in ioctl binding.
|
|
82
|
+
* Works even when stdout/stderr are piped, as long as stdin is a tty. */
|
|
83
|
+
function _readTtyColumns(): number | undefined {
|
|
84
|
+
try {
|
|
85
|
+
// Node exposes getWindowSize() on tty.ReadStream / tty.WriteStream
|
|
86
|
+
const { getWindowSize } = require("node:tty") as {
|
|
87
|
+
getWindowSize?: (fd: number) => [number, number];
|
|
88
|
+
};
|
|
89
|
+
if (getWindowSize) {
|
|
90
|
+
// Try fd 1 (stdout), 2 (stderr), 0 (stdin) in order
|
|
91
|
+
for (const fd of [1, 2, 0]) {
|
|
92
|
+
try {
|
|
93
|
+
const [cols] = getWindowSize(fd);
|
|
94
|
+
if (cols && cols > 0) return cols;
|
|
95
|
+
} catch {
|
|
96
|
+
/* fd not a tty */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
/* tty module unavailable */
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
61
104
|
}
|
|
62
105
|
|
|
63
106
|
export function shortPath(cwd: string, home: string, p: string): string {
|