@quandev104/pi-style 0.2.0 → 0.2.1
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/CHANGELOG.md +6 -0
- package/dist/extensions/pi-style.js +691 -296
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/app/index.ts +3 -2
- package/extension-src/pi-style/app/runtime.ts +72 -84
- package/extension-src/pi-style/app/snapshot.ts +41 -2
- package/extension-src/pi-style/features/editor/index.ts +80 -61
- package/extension-src/pi-style/features/messages/index.ts +194 -56
- package/extension-src/pi-style/features/tools/bash-execution.ts +12 -1
- package/extension-src/pi-style/features/tools/boxed/bash.ts +99 -31
- package/extension-src/pi-style/features/tools/boxed/batch.ts +44 -10
- package/extension-src/pi-style/features/tools/boxed/edit.ts +30 -15
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +31 -15
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -14
- package/extension-src/pi-style/features/tools/boxed/shared.ts +25 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +1 -1
- package/extension-src/pi-style/pi/index.ts +26 -13
- package/extension-src/pi-style/pi/session-usage.ts +204 -21
- package/extension-src/pi-style/shared/box.ts +13 -2
- package/package.json +1 -1
|
@@ -1,9 +1,46 @@
|
|
|
1
|
-
import { visibleWidth } from "
|
|
1
|
+
import { visibleWidth } from "../../shared/ansi.js";
|
|
2
2
|
|
|
3
3
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
4
4
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
5
5
|
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
|
|
6
6
|
type OscParts = { start: string; body: string; end: string };
|
|
7
|
+
type LineAnalysis = {
|
|
8
|
+
visibleWidth: number;
|
|
9
|
+
hasContent: boolean;
|
|
10
|
+
oscEnvelope: OscParts | undefined;
|
|
11
|
+
hasOscStart: boolean;
|
|
12
|
+
leadingMarkers: { head: string; rest: string };
|
|
13
|
+
isBackgroundWrapped: boolean;
|
|
14
|
+
backgroundAnsi: string;
|
|
15
|
+
backgroundBody: string | undefined;
|
|
16
|
+
backgroundBodyWidth: number | undefined;
|
|
17
|
+
};
|
|
18
|
+
type DecoratedRenderCacheEntry = {
|
|
19
|
+
nativeRef: readonly string[];
|
|
20
|
+
nativeLines: readonly string[];
|
|
21
|
+
result: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
type MessageDecorationTestState = {
|
|
24
|
+
decoratePasses: number;
|
|
25
|
+
cacheHits: number;
|
|
26
|
+
cacheMisses: number;
|
|
27
|
+
lineCacheHits: number;
|
|
28
|
+
lineCacheMisses: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const BG_RESET = "\x1b[49m";
|
|
32
|
+
const MAX_RENDER_CACHE_KEYS_PER_INSTANCE = 8;
|
|
33
|
+
const MAX_LINE_ANALYSIS_ENTRIES = 4096;
|
|
34
|
+
|
|
35
|
+
let renderCacheByInstance = new WeakMap<object, Map<string, DecoratedRenderCacheEntry>>();
|
|
36
|
+
let lineAnalysisCache = new Map<string, LineAnalysis>();
|
|
37
|
+
const messageDecorationTestState: MessageDecorationTestState = {
|
|
38
|
+
decoratePasses: 0,
|
|
39
|
+
cacheHits: 0,
|
|
40
|
+
cacheMisses: 0,
|
|
41
|
+
lineCacheHits: 0,
|
|
42
|
+
lineCacheMisses: 0,
|
|
43
|
+
};
|
|
7
44
|
|
|
8
45
|
function extractOscEnvelope(line: string): OscParts | undefined {
|
|
9
46
|
if (!line.startsWith(OSC133_ZONE_START)) return undefined;
|
|
@@ -12,8 +49,6 @@ function extractOscEnvelope(line: string): OscParts | undefined {
|
|
|
12
49
|
return { start: OSC133_ZONE_START, body: line.slice(OSC133_ZONE_START.length, bodyEnd), end: line.slice(bodyEnd) };
|
|
13
50
|
}
|
|
14
51
|
|
|
15
|
-
const BG_RESET = "\x1b[49m";
|
|
16
|
-
|
|
17
52
|
/** Leading zero-width OSC sequences (e.g. OSC133 markers) of a line. */
|
|
18
53
|
function splitLeadingMarkers(line: string): { head: string; rest: string } {
|
|
19
54
|
let index = 0;
|
|
@@ -51,27 +86,81 @@ function isBackgroundSgr(sequence: string): boolean {
|
|
|
51
86
|
return false;
|
|
52
87
|
}
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
function contentText(line: string): string {
|
|
90
|
+
let output = "";
|
|
91
|
+
for (let index = 0; index < line.length; index++) {
|
|
92
|
+
if (line.charCodeAt(index) !== 27) {
|
|
93
|
+
output += line[index];
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const next = line[index + 1];
|
|
97
|
+
if (next === "]") {
|
|
98
|
+
index += 2;
|
|
99
|
+
while (index < line.length && line.charCodeAt(index) !== 7) index++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (next === "[") {
|
|
103
|
+
index += 2;
|
|
104
|
+
while (index < line.length && (line.charCodeAt(index) < 64 || line.charCodeAt(index) > 126)) index++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return output.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasContent(line: string): boolean {
|
|
111
|
+
return [...contentText(line)].some((character) => !/\s/u.test(character));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getLineAnalysis(line: string): LineAnalysis {
|
|
115
|
+
const cached = lineAnalysisCache.get(line);
|
|
116
|
+
if (cached) {
|
|
117
|
+
messageDecorationTestState.lineCacheHits++;
|
|
118
|
+
return cached;
|
|
119
|
+
}
|
|
120
|
+
messageDecorationTestState.lineCacheMisses++;
|
|
121
|
+
const leadingMarkers = splitLeadingMarkers(line);
|
|
122
|
+
const backgroundAnsi = leadingSgr(leadingMarkers.rest);
|
|
123
|
+
const isBackgroundWrapped =
|
|
124
|
+
backgroundAnsi !== "" && isBackgroundSgr(backgroundAnsi) && leadingMarkers.rest.endsWith(BG_RESET);
|
|
125
|
+
const backgroundBody = isBackgroundWrapped
|
|
126
|
+
? leadingMarkers.rest.slice(backgroundAnsi.length, leadingMarkers.rest.length - BG_RESET.length)
|
|
127
|
+
: undefined;
|
|
128
|
+
const analysis: LineAnalysis = {
|
|
129
|
+
visibleWidth: visibleWidth(line),
|
|
130
|
+
hasContent: hasContent(line),
|
|
131
|
+
oscEnvelope: extractOscEnvelope(line),
|
|
132
|
+
hasOscStart: line.startsWith(OSC133_ZONE_START),
|
|
133
|
+
leadingMarkers,
|
|
134
|
+
isBackgroundWrapped,
|
|
135
|
+
backgroundAnsi,
|
|
136
|
+
backgroundBody,
|
|
137
|
+
backgroundBodyWidth: backgroundBody === undefined ? undefined : visibleWidth(backgroundBody),
|
|
138
|
+
};
|
|
139
|
+
lineAnalysisCache.set(line, analysis);
|
|
140
|
+
if (lineAnalysisCache.size > MAX_LINE_ANALYSIS_ENTRIES) {
|
|
141
|
+
const oldestKey = lineAnalysisCache.keys().next().value;
|
|
142
|
+
if (oldestKey !== undefined) lineAnalysisCache.delete(oldestKey);
|
|
143
|
+
}
|
|
144
|
+
return analysis;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function rebuildAtWidth(
|
|
148
|
+
line: string,
|
|
149
|
+
width: number,
|
|
150
|
+
lead: string,
|
|
151
|
+
leadWidth: number,
|
|
152
|
+
analysis = getLineAnalysis(line),
|
|
153
|
+
): string {
|
|
154
|
+
if (
|
|
155
|
+
analysis.isBackgroundWrapped &&
|
|
156
|
+
analysis.backgroundBody !== undefined &&
|
|
157
|
+
analysis.backgroundBodyWidth !== undefined
|
|
158
|
+
) {
|
|
159
|
+
const pad = " ".repeat(Math.max(0, width - leadWidth - analysis.backgroundBodyWidth));
|
|
160
|
+
return `${analysis.leadingMarkers.head}${analysis.backgroundAnsi}${lead}${analysis.backgroundBody}${pad}${BG_RESET}`;
|
|
72
161
|
}
|
|
73
|
-
const
|
|
74
|
-
return `${
|
|
162
|
+
const pad = " ".repeat(Math.max(0, width - leadWidth - analysis.visibleWidth));
|
|
163
|
+
return `${lead}${line}${pad}`;
|
|
75
164
|
}
|
|
76
165
|
|
|
77
166
|
function decorateMessageLine(
|
|
@@ -85,20 +174,23 @@ function decorateMessageLine(
|
|
|
85
174
|
firstHasStart: boolean;
|
|
86
175
|
multilineEnvelope: boolean;
|
|
87
176
|
prefix: string;
|
|
177
|
+
prefixWidth: number;
|
|
88
178
|
},
|
|
179
|
+
analysis = getLineAnalysis(line),
|
|
89
180
|
): string {
|
|
90
|
-
const { firstEnvelope, firstHasStart, multilineEnvelope, prefix } = options;
|
|
91
|
-
const prefixWidth = visibleWidth(prefix);
|
|
181
|
+
const { firstEnvelope, firstHasStart, multilineEnvelope, prefix, prefixWidth } = options;
|
|
92
182
|
const lead = index === contentIndex ? prefix : index > contentIndex ? " ".repeat(prefixWidth) : "";
|
|
183
|
+
const leadWidth = index < contentIndex ? 0 : prefixWidth;
|
|
93
184
|
if (index === contentIndex && firstEnvelope)
|
|
94
|
-
return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix)}${firstEnvelope.end}`;
|
|
185
|
+
return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix, prefixWidth)}${firstEnvelope.end}`;
|
|
95
186
|
if (index === contentIndex && firstHasStart)
|
|
96
|
-
return `${OSC133_ZONE_START}${rebuildAtWidth(line.slice(OSC133_ZONE_START.length), width, prefix)}`;
|
|
187
|
+
return `${OSC133_ZONE_START}${rebuildAtWidth(line.slice(OSC133_ZONE_START.length), width, prefix, prefixWidth)}`;
|
|
97
188
|
if (index === lastIndex && multilineEnvelope && index !== contentIndex)
|
|
98
189
|
return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
|
|
99
190
|
line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
|
|
100
191
|
width,
|
|
101
192
|
lead,
|
|
193
|
+
leadWidth,
|
|
102
194
|
)}`;
|
|
103
195
|
if (
|
|
104
196
|
index === contentIndex &&
|
|
@@ -110,43 +202,61 @@ function decorateMessageLine(
|
|
|
110
202
|
line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
|
|
111
203
|
width,
|
|
112
204
|
prefix,
|
|
205
|
+
prefixWidth,
|
|
113
206
|
)}`;
|
|
114
|
-
return rebuildAtWidth(line, width, lead);
|
|
207
|
+
return rebuildAtWidth(line, width, lead, leadWidth, analysis);
|
|
115
208
|
}
|
|
116
209
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const next = line[index + 1];
|
|
125
|
-
if (next === "]") {
|
|
126
|
-
index += 2;
|
|
127
|
-
while (index < line.length && line.charCodeAt(index) !== 7) index++;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (next === "[") {
|
|
131
|
-
index += 2;
|
|
132
|
-
while (index < line.length && (line.charCodeAt(index) < 64 || line.charCodeAt(index) > 126)) index++;
|
|
133
|
-
}
|
|
210
|
+
function sameLines(left: readonly string[], right: readonly string[]): boolean {
|
|
211
|
+
if (left === right) return true;
|
|
212
|
+
if (left.length !== right.length) return false;
|
|
213
|
+
for (let index = 0; index < left.length; index++) {
|
|
214
|
+
if (left[index] !== right[index]) return false;
|
|
134
215
|
}
|
|
135
|
-
return
|
|
216
|
+
return true;
|
|
136
217
|
}
|
|
137
218
|
|
|
138
|
-
function
|
|
139
|
-
return
|
|
219
|
+
function cacheKey(width: number, prefix: string): string {
|
|
220
|
+
return `${width}\u0000${prefix}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getRenderCache(instance: object): Map<string, DecoratedRenderCacheEntry> {
|
|
224
|
+
let cache = renderCacheByInstance.get(instance);
|
|
225
|
+
if (!cache) {
|
|
226
|
+
cache = new Map();
|
|
227
|
+
renderCacheByInstance.set(instance, cache);
|
|
228
|
+
}
|
|
229
|
+
return cache;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function storeRenderCache(
|
|
233
|
+
instance: object,
|
|
234
|
+
width: number,
|
|
235
|
+
prefix: string,
|
|
236
|
+
native: readonly string[],
|
|
237
|
+
result: readonly string[],
|
|
238
|
+
): void {
|
|
239
|
+
const cache = getRenderCache(instance);
|
|
240
|
+
const key = cacheKey(width, prefix);
|
|
241
|
+
if (cache.has(key)) cache.delete(key);
|
|
242
|
+
cache.set(key, { nativeRef: native, nativeLines: [...native], result: [...result] });
|
|
243
|
+
while (cache.size > MAX_RENDER_CACHE_KEYS_PER_INSTANCE) {
|
|
244
|
+
const oldestKey = cache.keys().next().value;
|
|
245
|
+
if (oldestKey === undefined) break;
|
|
246
|
+
cache.delete(oldestKey);
|
|
247
|
+
}
|
|
140
248
|
}
|
|
141
249
|
|
|
142
250
|
function prefixNative(lines: unknown, width: number, prefix: string): string[] | undefined {
|
|
143
251
|
if (!Array.isArray(lines) || lines.length === 0 || !lines.every((line) => typeof line === "string")) return undefined;
|
|
252
|
+
messageDecorationTestState.decoratePasses++;
|
|
144
253
|
const nativeLines = lines as string[];
|
|
145
254
|
const prefixWidth = visibleWidth(prefix);
|
|
146
255
|
if (width <= prefixWidth) return undefined;
|
|
147
256
|
const bodyWidth = width - prefixWidth;
|
|
148
257
|
const first = nativeLines[0] ?? "";
|
|
149
258
|
const last = nativeLines.at(-1) ?? "";
|
|
259
|
+
const lastAnalysis = getLineAnalysis(last);
|
|
150
260
|
const multilineEnvelope = nativeLines.length > 1 && last.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL);
|
|
151
261
|
// The last line is a content-start candidate only when the envelope is
|
|
152
262
|
// single-line, or when no earlier line carries content. Assistant messages
|
|
@@ -154,22 +264,26 @@ function prefixNative(lines: unknown, width: number, prefix: string): string[] |
|
|
|
154
264
|
// sits on the final line ([OSC133_A, OSC133_END+FINAL+body]); excluding it
|
|
155
265
|
// would drop the prefix for every short assistant reply.
|
|
156
266
|
const firstContentIndex = nativeLines.findIndex((line, index) => {
|
|
157
|
-
if (index !== nativeLines.length - 1 || !multilineEnvelope) return
|
|
158
|
-
return
|
|
267
|
+
if (index !== nativeLines.length - 1 || !multilineEnvelope) return getLineAnalysis(line).hasContent;
|
|
268
|
+
return (
|
|
269
|
+
!nativeLines.slice(0, index).some((earlier) => getLineAnalysis(earlier).hasContent) && lastAnalysis.hasContent
|
|
270
|
+
);
|
|
159
271
|
});
|
|
160
272
|
if (firstContentIndex < 0) return nativeLines;
|
|
161
|
-
const
|
|
162
|
-
const
|
|
273
|
+
const firstAnalysis = getLineAnalysis(first);
|
|
274
|
+
const firstEnvelope = firstContentIndex === 0 ? firstAnalysis.oscEnvelope : undefined;
|
|
275
|
+
const firstHasStart = firstContentIndex === 0 && firstAnalysis.hasOscStart;
|
|
163
276
|
const decorated = nativeLines.map((line, index) =>
|
|
164
277
|
decorateMessageLine(line, index, nativeLines.length - 1, firstContentIndex, width, {
|
|
165
278
|
firstEnvelope,
|
|
166
279
|
firstHasStart,
|
|
167
280
|
multilineEnvelope,
|
|
168
281
|
prefix,
|
|
282
|
+
prefixWidth,
|
|
169
283
|
}),
|
|
170
284
|
);
|
|
171
285
|
if (!decorated.every((line) => visibleWidth(line) <= width)) return undefined;
|
|
172
|
-
if (!nativeLines.every((line) =>
|
|
286
|
+
if (!nativeLines.every((line) => getLineAnalysis(line).visibleWidth <= bodyWidth)) return undefined;
|
|
173
287
|
return decorated;
|
|
174
288
|
}
|
|
175
289
|
|
|
@@ -182,6 +296,20 @@ export type MessageDecorationSnapshot = Readonly<{
|
|
|
182
296
|
hideInterimText: boolean;
|
|
183
297
|
}>;
|
|
184
298
|
|
|
299
|
+
export function __getMessageDecorationTestState(): Readonly<MessageDecorationTestState> {
|
|
300
|
+
return { ...messageDecorationTestState };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function __resetMessageDecorationTestState(): void {
|
|
304
|
+
messageDecorationTestState.decoratePasses = 0;
|
|
305
|
+
messageDecorationTestState.cacheHits = 0;
|
|
306
|
+
messageDecorationTestState.cacheMisses = 0;
|
|
307
|
+
messageDecorationTestState.lineCacheHits = 0;
|
|
308
|
+
messageDecorationTestState.lineCacheMisses = 0;
|
|
309
|
+
renderCacheByInstance = new WeakMap<object, Map<string, DecoratedRenderCacheEntry>>();
|
|
310
|
+
lineAnalysisCache = new Map<string, LineAnalysis>();
|
|
311
|
+
}
|
|
312
|
+
|
|
185
313
|
export function decorateMessageRender(
|
|
186
314
|
original: unknown,
|
|
187
315
|
instance: object,
|
|
@@ -197,12 +325,22 @@ export function decorateMessageRender(
|
|
|
197
325
|
const width = typeof args[0] === "number" ? args[0] : 0;
|
|
198
326
|
const prefix = snapshot.assistantPrefix;
|
|
199
327
|
if (!snapshot.assistantEnabled) return Reflect.apply(original, instance, args);
|
|
200
|
-
|
|
328
|
+
const prefixWidth = visibleWidth(prefix);
|
|
329
|
+
if (width <= prefixWidth) return Reflect.apply(original, instance, args);
|
|
201
330
|
// Exactly one native invocation. If the reduced render cannot be certified, the
|
|
202
331
|
// already-obtained result is the only safe fallback; retrying can mutate state.
|
|
203
|
-
const reducedWidth = width -
|
|
332
|
+
const reducedWidth = width - prefixWidth;
|
|
204
333
|
const native = Reflect.apply(original, instance, [reducedWidth, ...args.slice(1)]);
|
|
205
|
-
|
|
334
|
+
if (!Array.isArray(native) || !native.every((line) => typeof line === "string")) return native;
|
|
335
|
+
const cached = getRenderCache(instance).get(cacheKey(width, prefix));
|
|
336
|
+
if (cached && (cached.nativeRef === native || sameLines(cached.nativeLines, native))) {
|
|
337
|
+
messageDecorationTestState.cacheHits++;
|
|
338
|
+
return [...cached.result];
|
|
339
|
+
}
|
|
340
|
+
messageDecorationTestState.cacheMisses++;
|
|
341
|
+
const decorated = prefixNative(native, width, prefix) ?? native;
|
|
342
|
+
storeRenderCache(instance, width, prefix, native, decorated);
|
|
343
|
+
return decorated;
|
|
206
344
|
}
|
|
207
345
|
|
|
208
346
|
/** Spacer-like: renders empty lines and exposes only setLines among these surfaces. */
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
boxLine,
|
|
20
20
|
boxWidth,
|
|
21
21
|
formatBoxedRunningStatus,
|
|
22
|
+
themeCacheKey,
|
|
22
23
|
} from "../../shared/box.js";
|
|
23
24
|
import { getThemeExtra } from "../../shared/theme-extras.js";
|
|
24
25
|
|
|
@@ -30,6 +31,11 @@ export function setBashExecutionTheme(theme: BoxTheme | undefined): void {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/** Structural view of the native BashExecutionComponent as used by the patch. */
|
|
34
|
+
interface BashExecutionRenderCache {
|
|
35
|
+
key: string;
|
|
36
|
+
lines: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
interface BashExecutionInstance {
|
|
34
40
|
command: string;
|
|
35
41
|
status: "running" | "cancelled" | "error" | "complete";
|
|
@@ -37,6 +43,7 @@ interface BashExecutionInstance {
|
|
|
37
43
|
contentContainer: { render(width: number): string[] };
|
|
38
44
|
/** Wall-clock start captured on the first boxed render for the live `◌ Running · Ns` footer. */
|
|
39
45
|
piStyleStart?: number;
|
|
46
|
+
piStyleRenderCache?: BashExecutionRenderCache;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
const TOP_LEFT = "╭";
|
|
@@ -92,13 +99,15 @@ export function renderBashExecutionBox(instance: unknown, args: unknown[]): stri
|
|
|
92
99
|
try {
|
|
93
100
|
if (host.piStyleStart === undefined) host.piStyleStart = Date.now();
|
|
94
101
|
const renderedWidth = boxWidth(width);
|
|
102
|
+
const cacheKey = `${themeCacheKey(theme)}|${width}|${host.status}|${host.exitCode ?? ""}|${host.command}`;
|
|
103
|
+
if (host.status !== "running" && host.piStyleRenderCache?.key === cacheKey) return host.piStyleRenderCache.lines;
|
|
95
104
|
const inner = boxInnerWidth(renderedWidth);
|
|
96
105
|
// The native Text children render one leading padding space per line;
|
|
97
106
|
// drop it so boxLine's own side padding produces symmetric borders.
|
|
98
107
|
const wrapped = content
|
|
99
108
|
.render(inner)
|
|
100
109
|
.map((line) => boxLine(theme, line.startsWith(" ") ? line.slice(1) : line, renderedWidth));
|
|
101
|
-
|
|
110
|
+
const lines = [
|
|
102
111
|
"",
|
|
103
112
|
boxLabeledBorder(theme, TOP_LEFT, TOP_RIGHT, bashBoxTitle(theme, host), undefined, renderedWidth),
|
|
104
113
|
boxBlankLine(theme, renderedWidth),
|
|
@@ -106,6 +115,8 @@ export function renderBashExecutionBox(instance: unknown, args: unknown[]): stri
|
|
|
106
115
|
boxBlankLine(theme, renderedWidth),
|
|
107
116
|
boxLabeledBorder(theme, BOTTOM_LEFT, BOTTOM_RIGHT, bashBoxFooter(theme, host), undefined, renderedWidth),
|
|
108
117
|
];
|
|
118
|
+
if (host.status !== "running") host.piStyleRenderCache = { key: cacheKey, lines };
|
|
119
|
+
return lines;
|
|
109
120
|
} catch {
|
|
110
121
|
return undefined;
|
|
111
122
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
renderBoxedToolResult,
|
|
16
16
|
replaceTabs,
|
|
17
17
|
shortenPath,
|
|
18
|
+
themeCacheKey,
|
|
18
19
|
} from "../../../shared/box.js";
|
|
19
20
|
import { safeTruncateToWidth, truncateAtCodePointBoundary } from "../../../shared/render-budget.js";
|
|
20
21
|
import { parseSimpleBashCommand } from "./command-shape.js";
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
} from "./output-tree.js";
|
|
53
54
|
import {
|
|
54
55
|
getStateElapsedMs,
|
|
56
|
+
getToolsRenderCacheSignature,
|
|
55
57
|
getToolsRenderConfig,
|
|
56
58
|
isResultSeen,
|
|
57
59
|
markResultSeen,
|
|
@@ -59,7 +61,14 @@ import {
|
|
|
59
61
|
startElapsedTicker,
|
|
60
62
|
stopElapsedTicker,
|
|
61
63
|
} from "./session-config.js";
|
|
62
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
type BoxedToolContext,
|
|
66
|
+
type BoxedToolDefinition,
|
|
67
|
+
getRenderCacheKey,
|
|
68
|
+
memoizedStateComponent,
|
|
69
|
+
noteBoxedCallState,
|
|
70
|
+
noteExecutionStart,
|
|
71
|
+
} from "./shared.js";
|
|
63
72
|
|
|
64
73
|
const MAX_LINE_CHARS = 2000;
|
|
65
74
|
const ESC = "\x1b";
|
|
@@ -754,13 +763,21 @@ function parseBashTreeOutput(cls: BashTreeClass, output: string): ParsedBashTree
|
|
|
754
763
|
return { matches };
|
|
755
764
|
}
|
|
756
765
|
|
|
766
|
+
type FinalSemanticRenderCache = {
|
|
767
|
+
key: string;
|
|
768
|
+
lines: string[];
|
|
769
|
+
};
|
|
770
|
+
|
|
757
771
|
interface BashTreeState {
|
|
758
|
-
|
|
772
|
+
cls: BashSemanticClass;
|
|
759
773
|
/** Raw command, so the call panel can render the boxed bash call on fallback. */
|
|
760
|
-
|
|
774
|
+
command: string;
|
|
761
775
|
/** `parsed` once the result arrives; `fallback` when the boxed shell takes over. */
|
|
762
776
|
parsed?: ParsedSemantic;
|
|
763
777
|
fallback?: boolean;
|
|
778
|
+
finished: boolean;
|
|
779
|
+
revision: number;
|
|
780
|
+
renderCache?: FinalSemanticRenderCache;
|
|
764
781
|
}
|
|
765
782
|
|
|
766
783
|
/** Classified semantic command: a bash tree (ls/find/grep), a git card, or a
|
|
@@ -879,29 +896,37 @@ function renderSemanticPanel(theme: BoxTheme, toolCallId: string, context: Boxed
|
|
|
879
896
|
invalidate() {},
|
|
880
897
|
render(width: number): string[] {
|
|
881
898
|
if (!state) return [];
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
899
|
+
const renderFresh = () => {
|
|
900
|
+
if (state.fallback) {
|
|
901
|
+
return renderBoxedBashCall(
|
|
902
|
+
theme,
|
|
903
|
+
state.command.split("\n"),
|
|
904
|
+
context,
|
|
905
|
+
bashWidthKey(state.command, context?.args?.timeout),
|
|
906
|
+
).render(width);
|
|
907
|
+
}
|
|
908
|
+
if (isBashTreeClass(state.cls)) {
|
|
909
|
+
const treeState: { cls: BashTreeClass; parsed?: ParsedBashTree } = { cls: state.cls };
|
|
910
|
+
if (state.parsed !== undefined) treeState.parsed = state.parsed as ParsedBashTree;
|
|
911
|
+
return renderBashTreeLines(theme, treeState, width);
|
|
912
|
+
}
|
|
913
|
+
// Git classes only ever carry git parsed values (parseSemanticOutput
|
|
914
|
+
// dispatches on the class), so the narrowed cast is exact.
|
|
915
|
+
if (isGhClass(state.cls)) {
|
|
916
|
+
const ghState: { cls: GhSemanticClass; parsed?: GhParsedSemantic } = { cls: state.cls };
|
|
917
|
+
if (state.parsed !== undefined) ghState.parsed = state.parsed as GhParsedSemantic;
|
|
918
|
+
return renderGhCardLines(theme, ghState, width);
|
|
919
|
+
}
|
|
920
|
+
const gitState: { cls: GitSemanticClass; parsed?: GitParsedSemantic } = { cls: state.cls };
|
|
921
|
+
if (state.parsed !== undefined) gitState.parsed = state.parsed as GitParsedSemantic;
|
|
922
|
+
return renderGitCardLines(theme, gitState, width);
|
|
923
|
+
};
|
|
924
|
+
if (!state.finished) return renderFresh();
|
|
925
|
+
const cacheKey = [themeCacheKey(theme), getToolsRenderCacheSignature(), width, state.revision].join("|");
|
|
926
|
+
if (state.renderCache?.key === cacheKey) return state.renderCache.lines;
|
|
927
|
+
const lines = renderFresh();
|
|
928
|
+
state.renderCache = { key: cacheKey, lines };
|
|
929
|
+
return lines;
|
|
905
930
|
},
|
|
906
931
|
};
|
|
907
932
|
}
|
|
@@ -911,7 +936,26 @@ export const bashTool: BoxedToolDefinition = {
|
|
|
911
936
|
noteExecutionStart(context);
|
|
912
937
|
const cls = classifyBashSemantic(String(args?.command ?? ""));
|
|
913
938
|
if (cls) {
|
|
914
|
-
|
|
939
|
+
const command = String(args?.command ?? "");
|
|
940
|
+
const existing = semanticStates.get(context.toolCallId);
|
|
941
|
+
if (existing) {
|
|
942
|
+
if (existing.command !== command || existing.cls.kind !== cls.kind) {
|
|
943
|
+
delete existing.parsed;
|
|
944
|
+
delete existing.fallback;
|
|
945
|
+
existing.finished = false;
|
|
946
|
+
existing.revision++;
|
|
947
|
+
delete existing.renderCache;
|
|
948
|
+
}
|
|
949
|
+
existing.command = command;
|
|
950
|
+
existing.cls = cls;
|
|
951
|
+
} else {
|
|
952
|
+
semanticStates.set(context.toolCallId, {
|
|
953
|
+
cls,
|
|
954
|
+
command,
|
|
955
|
+
finished: false,
|
|
956
|
+
revision: 0,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
915
959
|
return renderSemanticPanel(theme, context.toolCallId, context);
|
|
916
960
|
}
|
|
917
961
|
noteBoxedCallState(context);
|
|
@@ -939,12 +983,18 @@ export const bashTool: BoxedToolDefinition = {
|
|
|
939
983
|
const parsed = parseSemanticOutput(cls, output);
|
|
940
984
|
const state = semanticStates.get(context.toolCallId);
|
|
941
985
|
if (parsed) {
|
|
942
|
-
if (state)
|
|
943
|
-
|
|
986
|
+
if (state) {
|
|
987
|
+
state.parsed = parsed;
|
|
988
|
+
state.finished = !options.isPartial;
|
|
989
|
+
state.revision++;
|
|
990
|
+
delete state.renderCache;
|
|
991
|
+
} else
|
|
944
992
|
semanticStates.set(context.toolCallId, {
|
|
945
993
|
cls,
|
|
946
994
|
command: String(context?.args?.command ?? ""),
|
|
947
995
|
parsed,
|
|
996
|
+
finished: !options.isPartial,
|
|
997
|
+
revision: 0,
|
|
948
998
|
});
|
|
949
999
|
// `git diff` / `git show` render a boxed adaptive-diff result (one frame
|
|
950
1000
|
// per file); `gh run view --job=<id>` renders a boxed log result. Every
|
|
@@ -961,7 +1011,12 @@ export const bashTool: BoxedToolDefinition = {
|
|
|
961
1011
|
// Unparseable output (ls -l, raw rg summary, non-git output): the boxed
|
|
962
1012
|
// shell owns the result; flag the call panel to render nothing so the
|
|
963
1013
|
// two don't duplicate.
|
|
964
|
-
if (state)
|
|
1014
|
+
if (state) {
|
|
1015
|
+
state.fallback = true;
|
|
1016
|
+
state.finished = !options.isPartial;
|
|
1017
|
+
state.revision++;
|
|
1018
|
+
delete state.renderCache;
|
|
1019
|
+
}
|
|
965
1020
|
}
|
|
966
1021
|
} else if (options.isPartial) {
|
|
967
1022
|
startElapsedTicker(context.state, context.invalidate);
|
|
@@ -976,6 +1031,19 @@ export const bashTool: BoxedToolDefinition = {
|
|
|
976
1031
|
if (firstResultPass) return EMPTY_BASH_RESULT;
|
|
977
1032
|
return renderBashStreamingResult(theme, raw, options, context);
|
|
978
1033
|
}
|
|
979
|
-
return
|
|
1034
|
+
return memoizedStateComponent(
|
|
1035
|
+
context.state,
|
|
1036
|
+
"__piStyleBashFinalResult",
|
|
1037
|
+
getRenderCacheKey(
|
|
1038
|
+
"bash-final-result",
|
|
1039
|
+
theme,
|
|
1040
|
+
Boolean(options.expanded),
|
|
1041
|
+
Boolean(context.isError),
|
|
1042
|
+
String(context?.args?.command ?? ""),
|
|
1043
|
+
raw,
|
|
1044
|
+
getStateElapsedMs(context.state) ?? "",
|
|
1045
|
+
),
|
|
1046
|
+
() => renderBashFinalResult(theme, raw, options, context),
|
|
1047
|
+
);
|
|
980
1048
|
},
|
|
981
1049
|
};
|