@heyhuynhgiabuu/pi-pretty 0.5.2 → 0.6.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 +21 -0
- package/bun.lock +598 -0
- package/package.json +6 -8
- package/pi-pretty.example.json +6 -0
- package/release-notes/v0.5.3.md +29 -0
- package/src/config.ts +250 -0
- package/src/fff.ts +147 -0
- package/src/helpers.ts +124 -0
- package/src/image.ts +129 -0
- package/src/index.ts +157 -1980
- package/src/render.ts +402 -0
- package/src/tools/bash.ts +115 -0
- package/src/tools/find.ts +87 -0
- package/src/tools/grep.ts +99 -0
- package/src/tools/ls.ts +66 -0
- package/src/tools/metrics.ts +40 -0
- package/src/tools/multi-grep.ts +171 -0
- package/src/tools/read.ts +112 -0
- package/src/types.ts +227 -0
- package/test/bash-rendering.test.ts +104 -1
package/src/types.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-pretty shared types.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TextContent, ImageContent } from "@earendil-works/pi-ai";
|
|
6
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Re-export FFF types needed by tools
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export type { FileFinder, FileItem, GrepResult, SearchResult, GrepMatch, GrepCursor } from "@ff-labs/fff-node";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Content / Result types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export type ToolContent = TextContent | ImageContent;
|
|
19
|
+
export type { TextContent, ImageContent };
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Theme / rendering context types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export interface ThemeLike {
|
|
25
|
+
fg: (key: string, text: string) => string;
|
|
26
|
+
bold: (text: string) => string;
|
|
27
|
+
getBgAnsi?: (key: string) => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RenderCtxLike {
|
|
31
|
+
lastComponent?: ComponentLike;
|
|
32
|
+
isError?: boolean;
|
|
33
|
+
state: Record<string, string | undefined>;
|
|
34
|
+
expanded?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TextLike {
|
|
38
|
+
setText(v: string): void;
|
|
39
|
+
getText?(): string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Minimal Component interface matching pi-tui's Component. */
|
|
43
|
+
export interface ComponentLike {
|
|
44
|
+
setText(v: string): void;
|
|
45
|
+
render(width: number): string[];
|
|
46
|
+
invalidate?(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Render detail types
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export type ReadDetails =
|
|
54
|
+
| { _type: "readImage"; filePath: string; data: string; mimeType: string }
|
|
55
|
+
| { _type: "readFile"; filePath: string; content: string; offset: number; lineCount: number };
|
|
56
|
+
|
|
57
|
+
export interface BashDetails extends Record<string, unknown> {
|
|
58
|
+
_type: "bashResult";
|
|
59
|
+
text: string;
|
|
60
|
+
exitCode: number | null;
|
|
61
|
+
command: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LsDetails extends Record<string, unknown> {
|
|
65
|
+
_type: "lsResult";
|
|
66
|
+
text: string;
|
|
67
|
+
path: string;
|
|
68
|
+
entryCount: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FindDetails extends Record<string, unknown> {
|
|
72
|
+
_type: "findResult";
|
|
73
|
+
text: string;
|
|
74
|
+
pattern: string;
|
|
75
|
+
matchCount: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface GrepDetails extends Record<string, unknown> {
|
|
79
|
+
_type: "grepResult";
|
|
80
|
+
text: string;
|
|
81
|
+
pattern: string;
|
|
82
|
+
matchCount: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type AnyDetails = ReadDetails | BashDetails | LsDetails | FindDetails | GrepDetails;
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Tool input types
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
export interface ReadInput {
|
|
92
|
+
path: string;
|
|
93
|
+
offset?: number;
|
|
94
|
+
limit?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface BashInput {
|
|
98
|
+
command: string;
|
|
99
|
+
timeout?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface LsInput {
|
|
103
|
+
path?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface FindInput {
|
|
107
|
+
pattern: string;
|
|
108
|
+
path?: string;
|
|
109
|
+
limit?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface GrepInput {
|
|
113
|
+
pattern: string;
|
|
114
|
+
path?: string;
|
|
115
|
+
glob?: string;
|
|
116
|
+
context?: number;
|
|
117
|
+
limit?: number;
|
|
118
|
+
literal?: boolean;
|
|
119
|
+
ignoreCase?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface MultiGrepInput {
|
|
123
|
+
patterns: string[];
|
|
124
|
+
path?: string;
|
|
125
|
+
constraints?: string;
|
|
126
|
+
context?: number;
|
|
127
|
+
limit?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// SDK tool definition shape (DI-friendly — accepts both mock and real SDK)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Minimal structural type for an SDK-produced tool definition.
|
|
136
|
+
* Accepts both the real SDK's ToolDefinition<> return type and test mocks.
|
|
137
|
+
*/
|
|
138
|
+
export interface SdkToolDef {
|
|
139
|
+
name?: string;
|
|
140
|
+
description?: string;
|
|
141
|
+
label?: string;
|
|
142
|
+
parameters?: unknown;
|
|
143
|
+
execute: (...args: any[]) => Promise<AgentToolResult<any>>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SdkTools {
|
|
147
|
+
createReadTool?: (cwd: string) => SdkToolDef;
|
|
148
|
+
createBashTool?: (cwd: string) => SdkToolDef;
|
|
149
|
+
createLsTool?: (cwd: string) => SdkToolDef;
|
|
150
|
+
createFindTool?: (cwd: string) => SdkToolDef;
|
|
151
|
+
createGrepTool?: (cwd: string) => SdkToolDef;
|
|
152
|
+
getAgentDir?: () => string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Multi-grep fallback types
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
export interface MultiGrepFallbackParams {
|
|
160
|
+
cwd: string;
|
|
161
|
+
patterns: string[];
|
|
162
|
+
path?: string;
|
|
163
|
+
constraints?: string;
|
|
164
|
+
context?: number;
|
|
165
|
+
limit: number;
|
|
166
|
+
ignoreCase: boolean;
|
|
167
|
+
signal?: AbortSignal;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface MultiGrepFallbackResult {
|
|
171
|
+
text: string;
|
|
172
|
+
matchCount: number;
|
|
173
|
+
limitReached: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type MultiGrepFallback = (params: MultiGrepFallbackParams) => Promise<MultiGrepFallbackResult>;
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// FFF service interfaces
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/** Minimal FFF service shape for tools that only need fileSearch. */
|
|
183
|
+
export interface FffServiceLike {
|
|
184
|
+
readonly isAvailable: boolean;
|
|
185
|
+
readonly partialIndex: boolean;
|
|
186
|
+
getFinder(): import("@ff-labs/fff-node").FileFinder | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Full FFF service for grep tools that also need cursor store. */
|
|
190
|
+
export interface FffServiceWithCursor extends FffServiceLike {
|
|
191
|
+
getCursorStore(): CursorStore;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** FFF lifecycle interface (used by session lifecycle code). */
|
|
195
|
+
export interface FffService extends FffServiceWithCursor {
|
|
196
|
+
ensureFinder(cwd: string): Promise<void>;
|
|
197
|
+
destroy(): void;
|
|
198
|
+
isModuleLoaded(): boolean;
|
|
199
|
+
tryLoadModule(): Promise<boolean>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface CursorStore {
|
|
203
|
+
store(cursor: unknown): string;
|
|
204
|
+
get(id: string): unknown | undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Constraint parsing
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
export interface ConstraintParseResult {
|
|
212
|
+
ok: boolean;
|
|
213
|
+
globs: string[];
|
|
214
|
+
tokens: string[];
|
|
215
|
+
error?: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// DI
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
export interface PiPrettyDeps {
|
|
223
|
+
sdk?: SdkTools;
|
|
224
|
+
TextComponent?: new (text?: string, x?: number, y?: number) => ComponentLike;
|
|
225
|
+
fffModule?: typeof import("@ff-labs/fff-node");
|
|
226
|
+
multiGrepRipgrepFallback?: MultiGrepFallback;
|
|
227
|
+
}
|
|
@@ -12,6 +12,9 @@ class MockText {
|
|
|
12
12
|
getText() {
|
|
13
13
|
return this.text;
|
|
14
14
|
}
|
|
15
|
+
render(_width: number) {
|
|
16
|
+
return this.text.split("\n");
|
|
17
|
+
}
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
const mockTheme = {
|
|
@@ -33,6 +36,10 @@ function mockToolFactory(exec: any) {
|
|
|
33
36
|
});
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
function stripAnsi(text: string): string {
|
|
40
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
function withStdoutColumns<T>(columns: number, fn: () => T): T {
|
|
37
44
|
const descriptor = Object.getOwnPropertyDescriptor(process.stdout, "columns");
|
|
38
45
|
Object.defineProperty(process.stdout, "columns", { configurable: true, value: columns });
|
|
@@ -127,7 +134,7 @@ describe("bash renderCall expansion", () => {
|
|
|
127
134
|
expect(expanded.getText()).toContain("5s timeout");
|
|
128
135
|
});
|
|
129
136
|
|
|
130
|
-
it("truncates
|
|
137
|
+
it("truncates ANSI tool headers that exceed the terminal width", () => {
|
|
131
138
|
withStdoutColumns(84, () => {
|
|
132
139
|
const bashTool = loadBashTool();
|
|
133
140
|
const command = `printf '${"界".repeat(120)}'`;
|
|
@@ -164,4 +171,100 @@ describe("bash renderCall expansion", () => {
|
|
|
164
171
|
}
|
|
165
172
|
});
|
|
166
173
|
});
|
|
174
|
+
|
|
175
|
+
it("does not add extra internal padding to the bash title in error state", () => {
|
|
176
|
+
withStdoutColumns(48, () => {
|
|
177
|
+
const bashTool = loadBashTool();
|
|
178
|
+
const rendered = bashTool.renderCall({ command: "false" }, mockTheme, {
|
|
179
|
+
lastComponent: new MockText(),
|
|
180
|
+
isError: true,
|
|
181
|
+
state: {},
|
|
182
|
+
expanded: false,
|
|
183
|
+
invalidate: () => {},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const lines = stripAnsi(rendered.getText()).split("\n");
|
|
187
|
+
expect(lines[1]).toMatch(/^ bash false/);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("pads every line of multi-line tool errors", () => {
|
|
192
|
+
withStdoutColumns(48, () => {
|
|
193
|
+
const bashTool = loadBashTool();
|
|
194
|
+
const rendered = bashTool.renderResult(
|
|
195
|
+
{ content: [{ type: "text", text: "\nfirst error\n\n\nsecond error\n" }] },
|
|
196
|
+
{},
|
|
197
|
+
ansiMockTheme,
|
|
198
|
+
{
|
|
199
|
+
lastComponent: new MockText(),
|
|
200
|
+
isError: true,
|
|
201
|
+
state: {},
|
|
202
|
+
expanded: false,
|
|
203
|
+
invalidate: () => {},
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const lines = stripAnsi(rendered.getText()).split("\n");
|
|
208
|
+
expect(lines[0]).toContain("✗ exit 1");
|
|
209
|
+
expect(lines[1]).toMatch(/^─+$/);
|
|
210
|
+
expect(lines[2]).toMatch(/^ first error/);
|
|
211
|
+
expect(lines[3]).toMatch(/^ /);
|
|
212
|
+
expect(lines[3].trim()).toBe("");
|
|
213
|
+
expect(lines[4]).toMatch(/^ second error/);
|
|
214
|
+
expect(lines[5]).toMatch(/^─+$/);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("applies tool background correctly to bash results without unnecessary resets", () => {
|
|
219
|
+
withStdoutColumns(64, () => {
|
|
220
|
+
const bashTool = loadBashTool();
|
|
221
|
+
const rendered = bashTool.renderResult(
|
|
222
|
+
{
|
|
223
|
+
content: [{ type: "text", text: "output" }],
|
|
224
|
+
details: { _type: "bashResult", text: "output", exitCode: 1, command: "test" },
|
|
225
|
+
},
|
|
226
|
+
{},
|
|
227
|
+
ansiMockTheme,
|
|
228
|
+
{
|
|
229
|
+
lastComponent: new MockText(),
|
|
230
|
+
isError: true,
|
|
231
|
+
state: { _tw: "64" },
|
|
232
|
+
expanded: false,
|
|
233
|
+
invalidate: () => {},
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(rendered.getText()).toMatch(/\x1b\[48;/); // tool background is applied
|
|
238
|
+
expect(rendered.getText()).not.toContain("\x1b[0m");
|
|
239
|
+
expect(rendered.getText()).not.toContain("\x1b[49m");
|
|
240
|
+
for (const line of rendered.getText().split("\n")) {
|
|
241
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(64);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("renders bash results using the component render width instead of stdout columns", () => {
|
|
247
|
+
withStdoutColumns(120, () => {
|
|
248
|
+
const bashTool = loadBashTool();
|
|
249
|
+
const rendered = bashTool.renderResult(
|
|
250
|
+
{ content: [{ type: "text", text: "hello world" }], details: { _type: "bashResult", text: "hello world", exitCode: 0, command: "echo hi" } },
|
|
251
|
+
{},
|
|
252
|
+
mockTheme,
|
|
253
|
+
{
|
|
254
|
+
lastComponent: new MockText(),
|
|
255
|
+
isError: false,
|
|
256
|
+
state: {},
|
|
257
|
+
expanded: false,
|
|
258
|
+
invalidate: () => {},
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
rendered.render(80);
|
|
263
|
+
const lines = stripAnsi(rendered.getText()).split("\n");
|
|
264
|
+
expect(lines.some((line) => /^─{80}$/.test(line))).toBe(true);
|
|
265
|
+
for (const line of rendered.getText().split("\n")) {
|
|
266
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(80);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
});
|
|
167
270
|
});
|