@xynogen/pix-pretty 1.0.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/src/types.ts ADDED
@@ -0,0 +1,275 @@
1
+ import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
2
+ import type {
3
+ AgentToolResult,
4
+ AgentToolUpdateCallback,
5
+ BashToolInput,
6
+ EditToolInput,
7
+ ExtensionCommandContext,
8
+ ExtensionContext,
9
+ FindToolInput,
10
+ GrepToolInput,
11
+ LsToolInput,
12
+ ReadToolInput,
13
+ WriteToolInput,
14
+ } from "@earendil-works/pi-coding-agent";
15
+ import type { FileFinder } from "@ff-labs/fff-node";
16
+
17
+ // We keep the original shiki-shaped types as plain string aliases so the
18
+ // language map and function signatures stay 1:1 with upstream pi-pretty.
19
+ export type BundledLanguage = string;
20
+
21
+ export type BundledTheme = string;
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Multi-grep ripgrep fallback contracts
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export type ConstraintParseResult =
28
+ | { ok: true; globs: string[]; tokens: string[] }
29
+ | { ok: false; error: string };
30
+
31
+ export type MultiGrepRipgrepFallbackParams = {
32
+ cwd: string;
33
+ patterns: string[];
34
+ path?: string;
35
+ constraints?: string;
36
+ context?: number;
37
+ limit: number;
38
+ ignoreCase: boolean;
39
+ signal?: AbortSignal;
40
+ };
41
+
42
+ export type MultiGrepRipgrepFallbackResult = {
43
+ text: string;
44
+ matchCount: number;
45
+ limitReached: boolean;
46
+ };
47
+
48
+ export type MultiGrepRipgrepFallback = (
49
+ params: MultiGrepRipgrepFallbackParams,
50
+ ) => Promise<MultiGrepRipgrepFallbackResult>;
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Config
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export type BgTheme = { getBgAnsi?: (key: string) => string };
57
+
58
+ export type FgTheme = {
59
+ fg: (key: string, text: string) => string;
60
+ // Optional raw-ANSI accessor pi's theme exposes; used by the diff renderer
61
+ // to pull toolDiffAdded/Removed/Context colors. Absent on minimal themes.
62
+ getFgAnsi?: (key: string) => string;
63
+ };
64
+
65
+ export type ImageProtocol = "iterm2" | "kitty" | "none";
66
+
67
+ export type ToolTextContent = TextContent;
68
+
69
+ export type ToolImageContent = ImageContent;
70
+
71
+ export type ToolContent = TextContent | ImageContent;
72
+
73
+ export type ToolResultLike<TDetails = unknown> = AgentToolResult<
74
+ TDetails | undefined
75
+ >;
76
+
77
+ type TextComponentLike = {
78
+ setText(value: string): void;
79
+ getText?: () => string;
80
+ };
81
+
82
+ export type TextComponentCtor = new (
83
+ text?: string,
84
+ x?: number,
85
+ y?: number,
86
+ ) => TextComponentLike;
87
+
88
+ export type ThemeLike = BgTheme & FgTheme & { bold: (text: string) => string };
89
+
90
+ export type RenderContextLike<
91
+ TState extends Record<string, string | undefined> = Record<
92
+ string,
93
+ string | undefined
94
+ >,
95
+ > = {
96
+ lastComponent?: TextComponentLike;
97
+ state: TState;
98
+ expanded: boolean;
99
+ isError: boolean;
100
+ invalidate: () => void;
101
+ };
102
+
103
+ type SessionContextLike = ExtensionContext;
104
+
105
+ export type CommandContextLike = ExtensionCommandContext;
106
+
107
+ type ToolExecutor<TParams, TDetails = unknown> = (
108
+ toolCallId: string,
109
+ params: TParams,
110
+ signal?: AbortSignal,
111
+ onUpdate?: AgentToolUpdateCallback<TDetails | undefined>,
112
+ ctx?: ExtensionContext,
113
+ ) => Promise<ToolResultLike<TDetails>>;
114
+
115
+ export type ToolFactory<TParams, TDetails = unknown> = (cwd: string) => {
116
+ name?: string;
117
+ description?: string;
118
+ label?: string;
119
+ parameters?: unknown;
120
+ execute: ToolExecutor<TParams, TDetails>;
121
+ };
122
+
123
+ export type PiPrettySdk = {
124
+ createReadToolDefinition?: ToolFactory<ReadToolInput>;
125
+ createReadTool?: ToolFactory<ReadToolInput>;
126
+ createBashToolDefinition?: ToolFactory<BashToolInput>;
127
+ createBashTool?: ToolFactory<BashToolInput>;
128
+ createLsToolDefinition?: ToolFactory<LsToolInput>;
129
+ createLsTool?: ToolFactory<LsToolInput>;
130
+ createFindToolDefinition?: ToolFactory<FindToolInput>;
131
+ createFindTool?: ToolFactory<FindToolInput>;
132
+ createGrepToolDefinition?: ToolFactory<GrepToolInput>;
133
+ createGrepTool?: ToolFactory<GrepToolInput>;
134
+ createEditToolDefinition?: ToolFactory<EditToolInput>;
135
+ createEditTool?: ToolFactory<EditToolInput>;
136
+ createWriteToolDefinition?: ToolFactory<WriteToolInput>;
137
+ createWriteTool?: ToolFactory<WriteToolInput>;
138
+ getAgentDir?: () => string;
139
+ };
140
+
141
+ export type PiPrettyApi = {
142
+ registerTool: (tool: unknown) => void;
143
+ registerCommand: (
144
+ name: string,
145
+ command: {
146
+ description?: string;
147
+ handler: (args: string, ctx: CommandContextLike) => Promise<void> | void;
148
+ },
149
+ ) => void;
150
+ on: (
151
+ event: string,
152
+ handler: (event: unknown, ctx: SessionContextLike) => Promise<void> | void,
153
+ ) => void;
154
+ };
155
+
156
+ export type OptionalFffModule = { FileFinder: typeof FileFinder };
157
+
158
+ export type FffBackedFinder = FileFinder;
159
+
160
+ export type ReadParams = ReadToolInput;
161
+
162
+ export type BashParams = BashToolInput;
163
+
164
+ export type EditParams = EditToolInput;
165
+
166
+ export type WriteParams = WriteToolInput;
167
+
168
+ // A single old→new replacement extracted from an edit tool call (supports both
169
+ // the single oldText/newText shape and the batched `edits[]` shape).
170
+ export type EditOperation = { oldText: string; newText: string };
171
+
172
+ export type LsParams = LsToolInput;
173
+
174
+ export type FindParams = FindToolInput;
175
+
176
+ export type GrepParams = GrepToolInput;
177
+
178
+ export type MultiGrepParams = {
179
+ patterns: string[];
180
+ path?: string;
181
+ constraints?: string;
182
+ context?: number;
183
+ limit?: number;
184
+ };
185
+
186
+ export type GrepRenderState = { _gk?: string; _gt?: string };
187
+
188
+ export type EditRenderState = { _pk?: string; _pt?: string };
189
+
190
+ export type WriteRenderState = {
191
+ _previewKey?: string;
192
+ _previewText?: string;
193
+ _wdk?: string;
194
+ _wdt?: string;
195
+ _nfk?: string;
196
+ _nft?: string;
197
+ };
198
+
199
+ export type MultiGrepRenderState = { _mgk?: string; _mgt?: string };
200
+
201
+ export type FindResultDetails = {
202
+ _type: "findResult";
203
+ text: string;
204
+ pattern: string;
205
+ matchCount: number;
206
+ };
207
+
208
+ export type GrepResultDetails = {
209
+ _type: "grepResult";
210
+ text: string;
211
+ pattern: string;
212
+ matchCount: number;
213
+ };
214
+
215
+ export type RenderDetails =
216
+ | { _type: "readImage"; filePath: string; data: string; mimeType: string }
217
+ | {
218
+ _type: "readFile";
219
+ filePath: string;
220
+ content: string;
221
+ offset: number;
222
+ lineCount: number;
223
+ }
224
+ | {
225
+ _type: "bashResult";
226
+ text: string;
227
+ exitCode: number | null;
228
+ command: string;
229
+ }
230
+ | { _type: "lsResult"; text: string; path: string; entryCount: number }
231
+ | FindResultDetails
232
+ | GrepResultDetails
233
+ | EditInfoDetails
234
+ | MultiEditInfoDetails
235
+ | WriteDiffDetails
236
+ | WriteNewDetails
237
+ | WriteNoChangeDetails;
238
+
239
+ // --- edit/write render detail payloads (stored on result.details) ---
240
+ export type EditInfoDetails = {
241
+ _type: "editInfo";
242
+ summary: string;
243
+ editLine: number;
244
+ };
245
+
246
+ export type MultiEditInfoDetails = {
247
+ _type: "multiEditInfo";
248
+ summary: string;
249
+ editCount: number;
250
+ diffLineCount: number;
251
+ };
252
+
253
+ export type WriteDiffDetails = {
254
+ _type: "diff";
255
+ summary: string;
256
+ oldContent: string;
257
+ newContent: string;
258
+ language: string | undefined;
259
+ };
260
+
261
+ export type WriteNewDetails = {
262
+ _type: "new";
263
+ lines: number;
264
+ content: string;
265
+ filePath: string;
266
+ };
267
+
268
+ export type WriteNoChangeDetails = { _type: "noChange" };
269
+
270
+ export interface PiPrettyDeps {
271
+ sdk: PiPrettySdk;
272
+ TextComponent: TextComponentCtor;
273
+ fffModule?: OptionalFffModule;
274
+ multiGrepRipgrepFallback?: MultiGrepRipgrepFallback;
275
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { relative } from "node:path";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+
4
+ import {
5
+ ANSI_CAPTURE_RE,
6
+ BG_BASE,
7
+ BG_ERROR,
8
+ FG_LNUM,
9
+ FG_RULE,
10
+ RST,
11
+ } from "./ansi.js";
12
+ import type {
13
+ FgTheme,
14
+ ToolContent,
15
+ ToolImageContent,
16
+ ToolResultLike,
17
+ ToolTextContent,
18
+ } from "./types.js";
19
+
20
+ export function renderToolError(error: string, theme: FgTheme): string {
21
+ return fillToolBackground(`\n${theme.fg("error", error)}`, BG_ERROR);
22
+ }
23
+
24
+ export function normalizeLineEndings(text: string): string {
25
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
26
+ }
27
+
28
+ function preserveToolBackground(ansi: string, bg: string): string {
29
+ return ansi.replace(ANSI_CAPTURE_RE, (seq, params: string) => {
30
+ const codes = params.split(";");
31
+ return params === "0" || codes.includes("49") ? `${seq}${bg}` : seq;
32
+ });
33
+ }
34
+
35
+ export function fillToolBackground(text: string, bg = BG_BASE): string {
36
+ const width = termW();
37
+ return text
38
+ .split("\n")
39
+ .map((line) => {
40
+ const normalized = preserveToolBackground(line, bg);
41
+ const fitted = preserveToolBackground(
42
+ truncateToWidth(normalized, width, ""),
43
+ bg,
44
+ );
45
+ const padding = Math.max(0, width - visibleWidth(fitted));
46
+ return `${bg}${fitted}${" ".repeat(padding)}${RST}`;
47
+ })
48
+ .join("\n");
49
+ }
50
+
51
+ export function termW(): number {
52
+ const stderrWithColumns = process.stderr as NodeJS.WriteStream & {
53
+ columns?: number;
54
+ };
55
+ const raw =
56
+ process.stdout.columns ||
57
+ stderrWithColumns.columns ||
58
+ Number.parseInt(process.env.COLUMNS ?? "", 10) ||
59
+ 200;
60
+ return Math.max(1, Math.min(raw - 4, 210));
61
+ }
62
+
63
+ export function shortPath(cwd: string, home: string, p: string): string {
64
+ if (!p) return "";
65
+ const r = relative(cwd, p);
66
+ if (!r.startsWith("..") && !r.startsWith("/")) return r;
67
+ return p.replace(home, "~");
68
+ }
69
+
70
+ export function rule(w: number): string {
71
+ return `${FG_RULE}${"─".repeat(w)}${RST}`;
72
+ }
73
+
74
+ export function lnum(n: number, w: number): string {
75
+ const v = String(n);
76
+ return `${FG_LNUM}${" ".repeat(Math.max(0, w - v.length))}${v}${RST}`;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Human-readable file size
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export function humanSize(bytes: number): string {
84
+ if (bytes < 1024) return `${bytes}B`;
85
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
86
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // File-type icons — Nerd Font glyphs (Seti-UI + Devicons, stable in NF v3+)
91
+ //
92
+ // Requires a Nerd Font installed (e.g., JetBrainsMono Nerd Font, FiraCode NF).
93
+ // Fallback: set PRETTY_ICONS=none to disable icons.
94
+ // ---------------------------------------------------------------------------
95
+
96
+ export function isTextContent(
97
+ content: ToolContent,
98
+ ): content is ToolTextContent {
99
+ return content.type === "text";
100
+ }
101
+
102
+ export function isImageContent(
103
+ content: ToolContent,
104
+ ): content is ToolImageContent {
105
+ return content.type === "image";
106
+ }
107
+
108
+ export function getTextContent(result: ToolResultLike): string {
109
+ return (
110
+ result.content
111
+ ?.filter(isTextContent)
112
+ .map((content) => content.text || "")
113
+ .join("\n") ?? ""
114
+ );
115
+ }
116
+
117
+ export function setResultDetails<T>(result: ToolResultLike, details: T): void {
118
+ result.details = details;
119
+ }
120
+
121
+ export function makeTextResult<TDetails>(
122
+ text: string,
123
+ details: TDetails,
124
+ ): ToolResultLike<TDetails> {
125
+ return {
126
+ content: [{ type: "text", text }],
127
+ details,
128
+ };
129
+ }
130
+
131
+ export function appendNotices(text: string, notices: string[]): string {
132
+ return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text;
133
+ }
134
+
135
+ export function countRipgrepMatches(text: string): number {
136
+ return text
137
+ .trim()
138
+ .split("\n")
139
+ .filter((line) => /^.+?[:-]\d+[:-]/.test(line)).length;
140
+ }
141
+
142
+ export function getErrorMessage(error: unknown): string {
143
+ return error instanceof Error ? error.message : String(error);
144
+ }
145
+
146
+ export function trimToUndefined(value: string | undefined): string | undefined {
147
+ const trimmed = value?.trim();
148
+ return trimmed ? trimmed : undefined;
149
+ }
150
+
151
+ function escapeRegexLiteral(text: string): string {
152
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
153
+ }
154
+
155
+ export function buildLiteralAlternationPattern(patterns: string[]): string {
156
+ return patterns
157
+ .map(escapeRegexLiteral)
158
+ .sort((a, b) => b.length - a.length)
159
+ .join("|");
160
+ }
161
+
162
+ export function shouldIgnoreCaseForPatterns(patterns: string[]): boolean {
163
+ return patterns.every((pattern) => pattern.toLowerCase() === pattern);
164
+ }
165
+
166
+ export function getConstraintBackedPath(
167
+ constraints: string | undefined,
168
+ ): string | undefined {
169
+ const trimmed = trimToUndefined(constraints);
170
+ if (
171
+ !trimmed ||
172
+ /\s/.test(trimmed) ||
173
+ trimmed.includes("!") ||
174
+ trimmed.endsWith("/") ||
175
+ /[*?[{]/.test(trimmed)
176
+ ) {
177
+ return undefined;
178
+ }
179
+ return trimmed;
180
+ }