@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 CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.21.0",
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