@xynogen/pix-pretty 1.21.0 → 1.22.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/package.json +2 -1
- package/src/batch.ts +168 -0
- package/src/types.ts +5 -1
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-pretty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, diff rendering, and FFF search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
9
|
"./ansi": "./src/ansi.ts",
|
|
10
|
+
"./batch": "./src/batch.ts",
|
|
10
11
|
"./confirm": "./src/confirm.ts",
|
|
11
12
|
"./progress": "./src/progress.ts",
|
|
12
13
|
"./config": "./src/config.ts",
|
package/src/batch.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Shared batching for read/grep/find/ls — N known targets, one tool result. */
|
|
2
|
+
|
|
3
|
+
export const BATCH_MAX_TARGETS = 20;
|
|
4
|
+
export const BATCH_MAX_BYTES = 50 * 1024;
|
|
5
|
+
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
|
|
9
|
+
/** Merge a singular target with an optional array, trimming and de-duping. */
|
|
10
|
+
export function resolveBatchStrings(single: string | undefined, many: unknown): string[] {
|
|
11
|
+
const out: string[] = [];
|
|
12
|
+
const seen = new Set<string>();
|
|
13
|
+
const add = (value: unknown) => {
|
|
14
|
+
if (typeof value !== "string") return;
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
if (!trimmed || seen.has(trimmed)) return;
|
|
17
|
+
seen.add(trimmed);
|
|
18
|
+
out.push(trimmed);
|
|
19
|
+
};
|
|
20
|
+
add(single);
|
|
21
|
+
if (Array.isArray(many)) {
|
|
22
|
+
for (const value of many) add(value);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Cap the target list at BATCH_MAX_TARGETS, reporting how many were dropped. */
|
|
28
|
+
export function sliceBatchTargets(targets: string[]): { targets: string[]; omitted: number } {
|
|
29
|
+
if (targets.length <= BATCH_MAX_TARGETS) return { targets, omitted: 0 };
|
|
30
|
+
return {
|
|
31
|
+
targets: targets.slice(0, BATCH_MAX_TARGETS),
|
|
32
|
+
omitted: targets.length - BATCH_MAX_TARGETS,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Minimal JSON-schema object shape we touch when adding a batch field. */
|
|
37
|
+
export interface SchemaObject {
|
|
38
|
+
properties: Record<string, unknown>;
|
|
39
|
+
required?: string[];
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Add an optional string[] field and drop listed keys from `required`. Does not mutate `schema`. */
|
|
44
|
+
export function withOptionalStringArray(
|
|
45
|
+
schema: unknown,
|
|
46
|
+
field: string,
|
|
47
|
+
description: string,
|
|
48
|
+
unrequire: string[] = [],
|
|
49
|
+
): SchemaObject {
|
|
50
|
+
const src = schema && typeof schema === "object" ? (schema as Record<string, unknown>) : {};
|
|
51
|
+
const properties = {
|
|
52
|
+
...((src.properties as Record<string, unknown> | undefined) ?? {}),
|
|
53
|
+
[field]: {
|
|
54
|
+
type: "array",
|
|
55
|
+
items: { type: "string" },
|
|
56
|
+
description,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const required = Array.isArray(src.required)
|
|
60
|
+
? (src.required as string[]).filter((key) => !unrequire.includes(key))
|
|
61
|
+
: [];
|
|
62
|
+
const next: SchemaObject = { ...src, properties };
|
|
63
|
+
if (required.length > 0) next.required = required;
|
|
64
|
+
else delete next.required;
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type BatchSection = {
|
|
69
|
+
id: string;
|
|
70
|
+
body: string;
|
|
71
|
+
units: number;
|
|
72
|
+
nouns: readonly [string, string];
|
|
73
|
+
error?: string;
|
|
74
|
+
truncated?: boolean;
|
|
75
|
+
hint?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function unitLabel(units: number, nouns: readonly [string, string]): string {
|
|
79
|
+
return `${units} ${units === 1 ? nouns[0] : nouns[1]}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One-line summary of every section (always covers all, even truncated/errored). */
|
|
83
|
+
export function formatBatchIndex(sections: BatchSection[], omitted = 0): string {
|
|
84
|
+
const parts = sections.map((section) => {
|
|
85
|
+
if (section.error) return `${section.id} error`;
|
|
86
|
+
if (section.truncated) {
|
|
87
|
+
return section.hint ? `${section.id} truncated, ${section.hint}` : `${section.id} truncated`;
|
|
88
|
+
}
|
|
89
|
+
return `${section.id} ${unitLabel(section.units, section.nouns)}`;
|
|
90
|
+
});
|
|
91
|
+
if (omitted > 0) parts.push(`+${omitted} omitted`);
|
|
92
|
+
return parts.join(" · ");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function byteLength(text: string): number {
|
|
96
|
+
return encoder.encode(text).length;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cutToBytes(text: string, maxBytes: number): string {
|
|
100
|
+
const bytes = encoder.encode(text);
|
|
101
|
+
if (bytes.length <= maxBytes) return text;
|
|
102
|
+
let cut = decoder.decode(bytes.slice(0, maxBytes));
|
|
103
|
+
if (cut.endsWith("\uFFFD")) cut = cut.slice(0, -1);
|
|
104
|
+
const lastNl = cut.lastIndexOf("\n");
|
|
105
|
+
return lastNl > 0 ? cut.slice(0, lastNl) : cut;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Cap section bodies across the whole batch. Index always covers every section. */
|
|
109
|
+
export function capSections(
|
|
110
|
+
sections: BatchSection[],
|
|
111
|
+
maxBytes: number,
|
|
112
|
+
maxUnits?: number,
|
|
113
|
+
omitted = 0,
|
|
114
|
+
): { index: string; text: string; sections: BatchSection[] } {
|
|
115
|
+
let remainingUnits = maxUnits;
|
|
116
|
+
let remainingBytes = maxBytes;
|
|
117
|
+
const capped: BatchSection[] = [];
|
|
118
|
+
|
|
119
|
+
for (const section of sections) {
|
|
120
|
+
if (section.error) {
|
|
121
|
+
capped.push(section);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if ((remainingUnits != null && remainingUnits <= 0) || remainingBytes <= 0) {
|
|
125
|
+
capped.push({ ...section, body: "", units: 0, truncated: true });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const lines = section.body.length > 0 ? section.body.split("\n") : [];
|
|
130
|
+
let keep = lines;
|
|
131
|
+
let truncated = Boolean(section.truncated);
|
|
132
|
+
if (remainingUnits != null && keep.length > remainingUnits) {
|
|
133
|
+
keep = keep.slice(0, remainingUnits);
|
|
134
|
+
truncated = true;
|
|
135
|
+
}
|
|
136
|
+
let body = keep.join("\n");
|
|
137
|
+
if (byteLength(body) > remainingBytes) {
|
|
138
|
+
body = cutToBytes(body, remainingBytes);
|
|
139
|
+
keep = body.length > 0 ? body.split("\n") : [];
|
|
140
|
+
truncated = true;
|
|
141
|
+
}
|
|
142
|
+
const units = body ? keep.length : 0;
|
|
143
|
+
if (remainingUnits != null) remainingUnits -= units;
|
|
144
|
+
remainingBytes -= byteLength(body);
|
|
145
|
+
capped.push({ ...section, body, units, truncated });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const index = formatBatchIndex(capped, omitted);
|
|
149
|
+
const blocks = capped
|
|
150
|
+
.filter((section) => section.error || section.body)
|
|
151
|
+
.map((section) => `===== ${section.id} =====\n${section.error ?? section.body}`);
|
|
152
|
+
const text = [index, ...blocks].filter(Boolean).join("\n\n");
|
|
153
|
+
return { index, text, sections: capped };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Join full section bodies with `===== id =====` headers (no capping). */
|
|
157
|
+
export function joinSectionBodies(sections: BatchSection[]): string {
|
|
158
|
+
return sections
|
|
159
|
+
.map((section) => `===== ${section.id} =====\n${section.error ?? section.body}`)
|
|
160
|
+
.join("\n\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Compact target label for a call row: list up to `max`, else `N noun`. */
|
|
164
|
+
export function formatCallTargets(ids: string[], max = 3, noun = "files"): string {
|
|
165
|
+
if (ids.length === 0) return "";
|
|
166
|
+
if (ids.length <= max) return ids.join(", ");
|
|
167
|
+
return `${ids.length} ${noun}`;
|
|
168
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -186,6 +186,8 @@ export type FindResultDetails = {
|
|
|
186
186
|
_type: "findResult";
|
|
187
187
|
text: string;
|
|
188
188
|
pattern: string;
|
|
189
|
+
/** Present when the call batched several glob patterns in one search. */
|
|
190
|
+
patterns?: string[];
|
|
189
191
|
path?: string;
|
|
190
192
|
matchCount: number;
|
|
191
193
|
};
|
|
@@ -194,6 +196,8 @@ export type GrepResultDetails = {
|
|
|
194
196
|
_type: "grepResult";
|
|
195
197
|
text: string;
|
|
196
198
|
pattern: string;
|
|
199
|
+
/** Present when the call batched several patterns in one search. */
|
|
200
|
+
patterns?: string[];
|
|
197
201
|
path?: string;
|
|
198
202
|
matchCount: number;
|
|
199
203
|
/** Search flags, so the renderer can rebuild the matcher to highlight hits:
|
|
@@ -217,7 +221,7 @@ export type RenderDetails =
|
|
|
217
221
|
exitCode: number | null;
|
|
218
222
|
command: string;
|
|
219
223
|
}
|
|
220
|
-
| { _type: "lsResult"; text: string; path: string; entryCount: number }
|
|
224
|
+
| { _type: "lsResult"; text: string; path: string; entryCount: number; paths?: string[] }
|
|
221
225
|
| FindResultDetails
|
|
222
226
|
| GrepResultDetails
|
|
223
227
|
| EditInfoDetails
|