pi-cc-extensions 0.7.4 → 0.7.5
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/extensions/claude-code-style.ts +1713 -1675
- package/extensions/context.ts +386 -362
- package/package.json +1 -1
|
@@ -1,1675 +1,1713 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
createBashToolDefinition,
|
|
4
|
-
createEditToolDefinition,
|
|
5
|
-
createFindToolDefinition,
|
|
6
|
-
createGrepToolDefinition,
|
|
7
|
-
createLsToolDefinition,
|
|
8
|
-
createReadToolDefinition,
|
|
9
|
-
createWriteToolDefinition,
|
|
10
|
-
generateDiffString,
|
|
11
|
-
keyHint,
|
|
12
|
-
getSettingsListTheme,
|
|
13
|
-
renderDiff,
|
|
14
|
-
ToolExecutionComponent,
|
|
15
|
-
} from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { Container, SettingsList, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
17
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { readFile, stat } from "node:fs/promises";
|
|
19
|
-
import { homedir } from "node:os";
|
|
20
|
-
import { join, resolve } from "node:path";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Claude Code Style for pi
|
|
24
|
-
*
|
|
25
|
-
* Edit/write tool calls show a bounded, colorized diff preview with +/- lines
|
|
26
|
-
* after execution. A collapsed tool result can be clicked on its Ctrl+O hint to
|
|
27
|
-
* expand only that tool.
|
|
28
|
-
*
|
|
29
|
-
* Dynamic commands:
|
|
30
|
-
* /ccstyle open interactive settings
|
|
31
|
-
* /ccstyle on enable
|
|
32
|
-
* /ccstyle off disable
|
|
33
|
-
* /ccstyle status show current state
|
|
34
|
-
*
|
|
35
|
-
* Shortcut:
|
|
36
|
-
* ctrl+shift+o toggle on/off
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
type Config = {
|
|
40
|
-
enabled: boolean;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const CONFIG_PATH = join(homedir(), ".pi", "agent", "claude-code-style.json");
|
|
44
|
-
|
|
45
|
-
let config: Config = loadConfig();
|
|
46
|
-
|
|
47
|
-
function loadConfig(): Config {
|
|
48
|
-
try {
|
|
49
|
-
if (existsSync(CONFIG_PATH)) {
|
|
50
|
-
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<Config>;
|
|
51
|
-
return {
|
|
52
|
-
enabled: parsed.enabled ?? true,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
// Ignore bad config and fall back to defaults.
|
|
57
|
-
}
|
|
58
|
-
return { enabled: true };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function saveConfig() {
|
|
62
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function shortenPath(path: string): string {
|
|
66
|
-
const home = homedir();
|
|
67
|
-
if (path.startsWith(home)) return `~${path.slice(home.length)}`;
|
|
68
|
-
return path;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function oneLine(value: unknown, max = 72): string {
|
|
72
|
-
const text = String(value ?? "")
|
|
73
|
-
.replace(/\s+/g, " ")
|
|
74
|
-
.trim();
|
|
75
|
-
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const UNSAFE_TERMINAL_ESCAPE = new RegExp(
|
|
79
|
-
"\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\x5C)"
|
|
80
|
-
+ "|\\u001B[PX^_][\\s\\S]*?\\u001B\\x5C"
|
|
81
|
-
+ "|(?:\\u001B\\[|\\u009B)[0-?]*[ -/]*[@-~]"
|
|
82
|
-
+ "|\\u001B[@-_]",
|
|
83
|
-
"g",
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
/** Prevent captured terminal control responses from being replayed by tool renderers. */
|
|
87
|
-
function sanitizeToolResultText(value: string): string {
|
|
88
|
-
return value
|
|
89
|
-
.replace(UNSAFE_TERMINAL_ESCAPE, "")
|
|
90
|
-
.replace(/\x1B/g, "")
|
|
91
|
-
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, "")
|
|
92
|
-
.replace(/\r\n/g, "\n")
|
|
93
|
-
.replace(/\r/g, "\n");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function textFromResult(result: any): string {
|
|
97
|
-
const item = result?.content?.find?.((c: any) => c.type === "text");
|
|
98
|
-
return item?.type === "text" ? sanitizeToolResultText(item.text ?? "") : "";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function countLines(text: string): number {
|
|
102
|
-
return text.trim().split("\n").filter((line) => line.trim().length > 0).length;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function hasExpandableResult(text: string): boolean {
|
|
106
|
-
return countLines(text) > 1;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function parsePathCount(text: string): number {
|
|
110
|
-
const files = new Set<string>();
|
|
111
|
-
for (const line of text.split("\n")) {
|
|
112
|
-
const match = line.match(/^([^:\n]+):(\d+:)?/);
|
|
113
|
-
if (match?.[1]) files.add(match[1]);
|
|
114
|
-
}
|
|
115
|
-
return files.size;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function summarizeBash(text: string): string {
|
|
119
|
-
const clean = text.trim();
|
|
120
|
-
if (!clean) return "Done";
|
|
121
|
-
const lower = clean.toLowerCase();
|
|
122
|
-
const lines = clean.split("\n").filter(Boolean);
|
|
123
|
-
const errorLine = lines.find((line) => /\b(error|failed|exception|fatal)\b/i.test(line));
|
|
124
|
-
if (errorLine) return `Failed: ${oneLine(errorLine, 88)}`;
|
|
125
|
-
if (/\b(warning|warn)\b/i.test(lower)) {
|
|
126
|
-
const warnings = lines.filter((line) => /\b(warning|warn)\b/i.test(line)).length;
|
|
127
|
-
return `Completed with ${warnings} warning${warnings === 1 ? "" : "s"}`;
|
|
128
|
-
}
|
|
129
|
-
if (/\b(success|passed|compiled|built|done)\b/i.test(lower)) return oneLine(lines.at(-1) ?? "Done", 88);
|
|
130
|
-
return lines.length > 1 ? `${lines.length} lines output: ${oneLine(lines.at(-1), 72)}` : oneLine(clean, 88);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function summarizeEdit(text: string): string {
|
|
134
|
-
const clean = text.trim();
|
|
135
|
-
if (!clean) return "Edited";
|
|
136
|
-
const added = (clean.match(/^\+/gm) ?? []).length;
|
|
137
|
-
const removed = (clean.match(/^-/gm) ?? []).length;
|
|
138
|
-
if (added || removed) return `Updated ${added} added, ${removed} removed`;
|
|
139
|
-
return oneLine(clean, 88);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function summarizeDiffStats(preview: FileDiffPreview | undefined): string | undefined {
|
|
143
|
-
if (!preview?.diff) return undefined;
|
|
144
|
-
const { added, removed } = preview.stats ?? diffStats(preview.lines ?? preview.diff.split("\n"));
|
|
145
|
-
if (!added && !removed) return undefined;
|
|
146
|
-
return `Added ${added} line${added === 1 ? "" : "s"}, removed ${removed} line${removed === 1 ? "" : "s"}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function toolIcon(_name: string): string {
|
|
150
|
-
return "●";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Match the braille loader shown to the left of pi's "Working..." row.
|
|
154
|
-
// Every frame is one cell wide, so tool titles remain horizontally stable.
|
|
155
|
-
const WORKING_LOADER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
156
|
-
const TOOL_PENDING_FRAMES: Record<string, string[]> = {
|
|
157
|
-
read: WORKING_LOADER_FRAMES,
|
|
158
|
-
bash: WORKING_LOADER_FRAMES,
|
|
159
|
-
edit: WORKING_LOADER_FRAMES,
|
|
160
|
-
write: WORKING_LOADER_FRAMES,
|
|
161
|
-
find: WORKING_LOADER_FRAMES,
|
|
162
|
-
grep: WORKING_LOADER_FRAMES,
|
|
163
|
-
ls: WORKING_LOADER_FRAMES,
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
function animationFrame(frames: string[], intervalMs = 120): string {
|
|
167
|
-
return frames[Math.floor(Date.now() / intervalMs) % frames.length] ?? frames[0] ?? "";
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const activeAnimationContexts = new Set<any>();
|
|
171
|
-
let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
172
|
-
|
|
173
|
-
function clearAnimation(context: any) {
|
|
174
|
-
if (!context?.state?.ccstyleAnimationScheduled) return;
|
|
175
|
-
context.state.ccstyleAnimationScheduled = false;
|
|
176
|
-
activeAnimationContexts.delete(context);
|
|
177
|
-
if (activeAnimationContexts.size === 0 && sharedAnimationTimer) {
|
|
178
|
-
clearTimeout(sharedAnimationTimer);
|
|
179
|
-
sharedAnimationTimer = null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function clearAllAnimations() {
|
|
184
|
-
for (const ctx of activeAnimationContexts) {
|
|
185
|
-
ctx.state.ccstyleAnimationScheduled = false;
|
|
186
|
-
}
|
|
187
|
-
activeAnimationContexts.clear();
|
|
188
|
-
if (sharedAnimationTimer) {
|
|
189
|
-
clearTimeout(sharedAnimationTimer);
|
|
190
|
-
sharedAnimationTimer = null;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function scheduleAnimation(context: any, intervalMs = 80) {
|
|
195
|
-
const state = (context.state ??= {});
|
|
196
|
-
if (state.ccstyleAnimationScheduled) return;
|
|
197
|
-
state.ccstyleAnimationScheduled = true;
|
|
198
|
-
activeAnimationContexts.add(context);
|
|
199
|
-
if (!sharedAnimationTimer) {
|
|
200
|
-
sharedAnimationTimer = setTimeout(() => {
|
|
201
|
-
sharedAnimationTimer = null;
|
|
202
|
-
const contexts = Array.from(activeAnimationContexts);
|
|
203
|
-
activeAnimationContexts.clear();
|
|
204
|
-
for (const ctx of contexts) {
|
|
205
|
-
ctx.state.ccstyleAnimationScheduled = false;
|
|
206
|
-
ctx.invalidate?.();
|
|
207
|
-
}
|
|
208
|
-
}, intervalMs);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function pendingIcon(name: string): string {
|
|
213
|
-
return animationFrame(TOOL_PENDING_FRAMES[name] ?? [toolIcon(name)], 80);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
type ToolVisualState = "pending" | "success" | "error";
|
|
217
|
-
|
|
218
|
-
function settledIcon(name: string, state: ToolVisualState | undefined): string {
|
|
219
|
-
if (state === "success") return "✓";
|
|
220
|
-
if (state === "error") return "✗";
|
|
221
|
-
return toolIcon(name);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function setToolVisualState(context: any, visualState: ToolVisualState) {
|
|
225
|
-
const state = (context.state ??= {});
|
|
226
|
-
if (visualState !== "pending") clearAnimation(context);
|
|
227
|
-
if (state.ccstyleToolVisualState === visualState) return;
|
|
228
|
-
state.ccstyleToolVisualState = visualState;
|
|
229
|
-
// Do not invalidate synchronously from renderResult. Pi is already rendering
|
|
230
|
-
// this tool row; recursively scheduling another render here can retain both
|
|
231
|
-
// the finalized result component and its previous secondary/partial component,
|
|
232
|
-
// which displays the result summary twice. The current render pass also
|
|
233
|
-
// refreshes renderCall, so the settled icon still updates immediately.
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function getToolVisualState(context: any): ToolVisualState | undefined {
|
|
237
|
-
return context?.state?.ccstyleToolVisualState as ToolVisualState | undefined;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function resolveToolVisualState(context: any): ToolVisualState | undefined {
|
|
241
|
-
const visualState = getToolVisualState(context);
|
|
242
|
-
if (visualState || context?.isPartial !== false) return visualState;
|
|
243
|
-
const settledState: ToolVisualState = context?.isError ? "error" : "success";
|
|
244
|
-
setToolVisualState(context, settledState);
|
|
245
|
-
return settledState;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function toolIconColor(context: any): "accent" | "error" | "success" | "muted" {
|
|
249
|
-
const visualState = getToolVisualState(context);
|
|
250
|
-
if (context?.isError || visualState === "error") return "error";
|
|
251
|
-
if (visualState === "success") return "success";
|
|
252
|
-
if (context?.isPartial || context?.executionStarted || visualState === "pending") return "accent";
|
|
253
|
-
return "muted";
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function isToolExpanded(options: any, context: any): boolean {
|
|
257
|
-
const local = context?.state?.ccstyleToolExpanded;
|
|
258
|
-
return typeof local === "boolean" ? local : Boolean(options?.expanded ?? context?.expanded);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Keep the guide aligned when long result lines wrap at the viewport edge. */
|
|
262
|
-
class ExpandedToolResultText {
|
|
263
|
-
private
|
|
264
|
-
private
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
function
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
collectToolComponents(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
||
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
)
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
if (
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
if (
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
?
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
return
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function
|
|
787
|
-
const
|
|
788
|
-
if (!
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
?
|
|
802
|
-
:
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
toolMouseInputUnsubscribe =
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
);
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
return
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
patch.
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
prototype
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
function
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
const
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
createBashToolDefinition,
|
|
4
|
+
createEditToolDefinition,
|
|
5
|
+
createFindToolDefinition,
|
|
6
|
+
createGrepToolDefinition,
|
|
7
|
+
createLsToolDefinition,
|
|
8
|
+
createReadToolDefinition,
|
|
9
|
+
createWriteToolDefinition,
|
|
10
|
+
generateDiffString,
|
|
11
|
+
keyHint,
|
|
12
|
+
getSettingsListTheme,
|
|
13
|
+
renderDiff,
|
|
14
|
+
ToolExecutionComponent,
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Container, SettingsList, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { readFile, stat } from "node:fs/promises";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join, resolve } from "node:path";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Claude Code Style for pi
|
|
24
|
+
*
|
|
25
|
+
* Edit/write tool calls show a bounded, colorized diff preview with +/- lines
|
|
26
|
+
* after execution. A collapsed tool result can be clicked on its Ctrl+O hint to
|
|
27
|
+
* expand only that tool.
|
|
28
|
+
*
|
|
29
|
+
* Dynamic commands:
|
|
30
|
+
* /ccstyle open interactive settings
|
|
31
|
+
* /ccstyle on enable
|
|
32
|
+
* /ccstyle off disable
|
|
33
|
+
* /ccstyle status show current state
|
|
34
|
+
*
|
|
35
|
+
* Shortcut:
|
|
36
|
+
* ctrl+shift+o toggle on/off
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
type Config = {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const CONFIG_PATH = join(homedir(), ".pi", "agent", "claude-code-style.json");
|
|
44
|
+
|
|
45
|
+
let config: Config = loadConfig();
|
|
46
|
+
|
|
47
|
+
function loadConfig(): Config {
|
|
48
|
+
try {
|
|
49
|
+
if (existsSync(CONFIG_PATH)) {
|
|
50
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<Config>;
|
|
51
|
+
return {
|
|
52
|
+
enabled: parsed.enabled ?? true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore bad config and fall back to defaults.
|
|
57
|
+
}
|
|
58
|
+
return { enabled: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveConfig() {
|
|
62
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function shortenPath(path: string): string {
|
|
66
|
+
const home = homedir();
|
|
67
|
+
if (path.startsWith(home)) return `~${path.slice(home.length)}`;
|
|
68
|
+
return path;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function oneLine(value: unknown, max = 72): string {
|
|
72
|
+
const text = String(value ?? "")
|
|
73
|
+
.replace(/\s+/g, " ")
|
|
74
|
+
.trim();
|
|
75
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const UNSAFE_TERMINAL_ESCAPE = new RegExp(
|
|
79
|
+
"\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\x5C)"
|
|
80
|
+
+ "|\\u001B[PX^_][\\s\\S]*?\\u001B\\x5C"
|
|
81
|
+
+ "|(?:\\u001B\\[|\\u009B)[0-?]*[ -/]*[@-~]"
|
|
82
|
+
+ "|\\u001B[@-_]",
|
|
83
|
+
"g",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
/** Prevent captured terminal control responses from being replayed by tool renderers. */
|
|
87
|
+
function sanitizeToolResultText(value: string): string {
|
|
88
|
+
return value
|
|
89
|
+
.replace(UNSAFE_TERMINAL_ESCAPE, "")
|
|
90
|
+
.replace(/\x1B/g, "")
|
|
91
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, "")
|
|
92
|
+
.replace(/\r\n/g, "\n")
|
|
93
|
+
.replace(/\r/g, "\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function textFromResult(result: any): string {
|
|
97
|
+
const item = result?.content?.find?.((c: any) => c.type === "text");
|
|
98
|
+
return item?.type === "text" ? sanitizeToolResultText(item.text ?? "") : "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function countLines(text: string): number {
|
|
102
|
+
return text.trim().split("\n").filter((line) => line.trim().length > 0).length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasExpandableResult(text: string): boolean {
|
|
106
|
+
return countLines(text) > 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parsePathCount(text: string): number {
|
|
110
|
+
const files = new Set<string>();
|
|
111
|
+
for (const line of text.split("\n")) {
|
|
112
|
+
const match = line.match(/^([^:\n]+):(\d+:)?/);
|
|
113
|
+
if (match?.[1]) files.add(match[1]);
|
|
114
|
+
}
|
|
115
|
+
return files.size;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function summarizeBash(text: string): string {
|
|
119
|
+
const clean = text.trim();
|
|
120
|
+
if (!clean) return "Done";
|
|
121
|
+
const lower = clean.toLowerCase();
|
|
122
|
+
const lines = clean.split("\n").filter(Boolean);
|
|
123
|
+
const errorLine = lines.find((line) => /\b(error|failed|exception|fatal)\b/i.test(line));
|
|
124
|
+
if (errorLine) return `Failed: ${oneLine(errorLine, 88)}`;
|
|
125
|
+
if (/\b(warning|warn)\b/i.test(lower)) {
|
|
126
|
+
const warnings = lines.filter((line) => /\b(warning|warn)\b/i.test(line)).length;
|
|
127
|
+
return `Completed with ${warnings} warning${warnings === 1 ? "" : "s"}`;
|
|
128
|
+
}
|
|
129
|
+
if (/\b(success|passed|compiled|built|done)\b/i.test(lower)) return oneLine(lines.at(-1) ?? "Done", 88);
|
|
130
|
+
return lines.length > 1 ? `${lines.length} lines output: ${oneLine(lines.at(-1), 72)}` : oneLine(clean, 88);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function summarizeEdit(text: string): string {
|
|
134
|
+
const clean = text.trim();
|
|
135
|
+
if (!clean) return "Edited";
|
|
136
|
+
const added = (clean.match(/^\+/gm) ?? []).length;
|
|
137
|
+
const removed = (clean.match(/^-/gm) ?? []).length;
|
|
138
|
+
if (added || removed) return `Updated ${added} added, ${removed} removed`;
|
|
139
|
+
return oneLine(clean, 88);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function summarizeDiffStats(preview: FileDiffPreview | undefined): string | undefined {
|
|
143
|
+
if (!preview?.diff) return undefined;
|
|
144
|
+
const { added, removed } = preview.stats ?? diffStats(preview.lines ?? preview.diff.split("\n"));
|
|
145
|
+
if (!added && !removed) return undefined;
|
|
146
|
+
return `Added ${added} line${added === 1 ? "" : "s"}, removed ${removed} line${removed === 1 ? "" : "s"}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toolIcon(_name: string): string {
|
|
150
|
+
return "●";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Match the braille loader shown to the left of pi's "Working..." row.
|
|
154
|
+
// Every frame is one cell wide, so tool titles remain horizontally stable.
|
|
155
|
+
const WORKING_LOADER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
156
|
+
const TOOL_PENDING_FRAMES: Record<string, string[]> = {
|
|
157
|
+
read: WORKING_LOADER_FRAMES,
|
|
158
|
+
bash: WORKING_LOADER_FRAMES,
|
|
159
|
+
edit: WORKING_LOADER_FRAMES,
|
|
160
|
+
write: WORKING_LOADER_FRAMES,
|
|
161
|
+
find: WORKING_LOADER_FRAMES,
|
|
162
|
+
grep: WORKING_LOADER_FRAMES,
|
|
163
|
+
ls: WORKING_LOADER_FRAMES,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
function animationFrame(frames: string[], intervalMs = 120): string {
|
|
167
|
+
return frames[Math.floor(Date.now() / intervalMs) % frames.length] ?? frames[0] ?? "";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const activeAnimationContexts = new Set<any>();
|
|
171
|
+
let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
172
|
+
|
|
173
|
+
function clearAnimation(context: any) {
|
|
174
|
+
if (!context?.state?.ccstyleAnimationScheduled) return;
|
|
175
|
+
context.state.ccstyleAnimationScheduled = false;
|
|
176
|
+
activeAnimationContexts.delete(context);
|
|
177
|
+
if (activeAnimationContexts.size === 0 && sharedAnimationTimer) {
|
|
178
|
+
clearTimeout(sharedAnimationTimer);
|
|
179
|
+
sharedAnimationTimer = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function clearAllAnimations() {
|
|
184
|
+
for (const ctx of activeAnimationContexts) {
|
|
185
|
+
ctx.state.ccstyleAnimationScheduled = false;
|
|
186
|
+
}
|
|
187
|
+
activeAnimationContexts.clear();
|
|
188
|
+
if (sharedAnimationTimer) {
|
|
189
|
+
clearTimeout(sharedAnimationTimer);
|
|
190
|
+
sharedAnimationTimer = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function scheduleAnimation(context: any, intervalMs = 80) {
|
|
195
|
+
const state = (context.state ??= {});
|
|
196
|
+
if (state.ccstyleAnimationScheduled) return;
|
|
197
|
+
state.ccstyleAnimationScheduled = true;
|
|
198
|
+
activeAnimationContexts.add(context);
|
|
199
|
+
if (!sharedAnimationTimer) {
|
|
200
|
+
sharedAnimationTimer = setTimeout(() => {
|
|
201
|
+
sharedAnimationTimer = null;
|
|
202
|
+
const contexts = Array.from(activeAnimationContexts);
|
|
203
|
+
activeAnimationContexts.clear();
|
|
204
|
+
for (const ctx of contexts) {
|
|
205
|
+
ctx.state.ccstyleAnimationScheduled = false;
|
|
206
|
+
ctx.invalidate?.();
|
|
207
|
+
}
|
|
208
|
+
}, intervalMs);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function pendingIcon(name: string): string {
|
|
213
|
+
return animationFrame(TOOL_PENDING_FRAMES[name] ?? [toolIcon(name)], 80);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
type ToolVisualState = "pending" | "success" | "error";
|
|
217
|
+
|
|
218
|
+
function settledIcon(name: string, state: ToolVisualState | undefined): string {
|
|
219
|
+
if (state === "success") return "✓";
|
|
220
|
+
if (state === "error") return "✗";
|
|
221
|
+
return toolIcon(name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function setToolVisualState(context: any, visualState: ToolVisualState) {
|
|
225
|
+
const state = (context.state ??= {});
|
|
226
|
+
if (visualState !== "pending") clearAnimation(context);
|
|
227
|
+
if (state.ccstyleToolVisualState === visualState) return;
|
|
228
|
+
state.ccstyleToolVisualState = visualState;
|
|
229
|
+
// Do not invalidate synchronously from renderResult. Pi is already rendering
|
|
230
|
+
// this tool row; recursively scheduling another render here can retain both
|
|
231
|
+
// the finalized result component and its previous secondary/partial component,
|
|
232
|
+
// which displays the result summary twice. The current render pass also
|
|
233
|
+
// refreshes renderCall, so the settled icon still updates immediately.
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getToolVisualState(context: any): ToolVisualState | undefined {
|
|
237
|
+
return context?.state?.ccstyleToolVisualState as ToolVisualState | undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function resolveToolVisualState(context: any): ToolVisualState | undefined {
|
|
241
|
+
const visualState = getToolVisualState(context);
|
|
242
|
+
if (visualState || context?.isPartial !== false) return visualState;
|
|
243
|
+
const settledState: ToolVisualState = context?.isError ? "error" : "success";
|
|
244
|
+
setToolVisualState(context, settledState);
|
|
245
|
+
return settledState;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function toolIconColor(context: any): "accent" | "error" | "success" | "muted" {
|
|
249
|
+
const visualState = getToolVisualState(context);
|
|
250
|
+
if (context?.isError || visualState === "error") return "error";
|
|
251
|
+
if (visualState === "success") return "success";
|
|
252
|
+
if (context?.isPartial || context?.executionStarted || visualState === "pending") return "accent";
|
|
253
|
+
return "muted";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isToolExpanded(options: any, context: any): boolean {
|
|
257
|
+
const local = context?.state?.ccstyleToolExpanded;
|
|
258
|
+
return typeof local === "boolean" ? local : Boolean(options?.expanded ?? context?.expanded);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Keep the guide aligned when long result lines wrap at the viewport edge. */
|
|
262
|
+
export class ExpandedToolResultText {
|
|
263
|
+
private text: string;
|
|
264
|
+
private prefix: string;
|
|
265
|
+
private normalizedText: string;
|
|
266
|
+
private cachedWidth: number | undefined;
|
|
267
|
+
private cachedLines: string[] | undefined;
|
|
268
|
+
|
|
269
|
+
constructor(text: string, prefix: string) {
|
|
270
|
+
this.text = text;
|
|
271
|
+
this.prefix = prefix;
|
|
272
|
+
this.normalizedText = text.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
setText(text: string): void {
|
|
276
|
+
if (this.text === text) return;
|
|
277
|
+
this.text = text;
|
|
278
|
+
this.normalizedText = text.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
279
|
+
this.invalidate();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
setPrefix(prefix: string): void {
|
|
283
|
+
if (this.prefix === prefix) return;
|
|
284
|
+
this.prefix = prefix;
|
|
285
|
+
this.invalidate();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
render(width: number): string[] {
|
|
289
|
+
if (this.cachedLines !== undefined && this.cachedWidth === width) return this.cachedLines;
|
|
290
|
+
|
|
291
|
+
const prefixWidth = visibleWidth(this.prefix);
|
|
292
|
+
const contentWidth = Math.max(1, width - prefixWidth);
|
|
293
|
+
const lines = wrapTextWithAnsi(this.normalizedText, contentWidth)
|
|
294
|
+
.map((line) => truncateToWidth(this.prefix + line, width, ""));
|
|
295
|
+
this.cachedWidth = width;
|
|
296
|
+
this.cachedLines = lines;
|
|
297
|
+
return lines;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
invalidate(): void {
|
|
301
|
+
this.cachedWidth = undefined;
|
|
302
|
+
this.cachedLines = undefined;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function renderCollapsedToolResult(body: string, collapsedHint = ""): string {
|
|
307
|
+
return ` ⎿ ${body}${collapsedHint}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function renderExpandedToolResult(
|
|
311
|
+
body: string,
|
|
312
|
+
theme: any,
|
|
313
|
+
isError: boolean,
|
|
314
|
+
lastComponent?: unknown,
|
|
315
|
+
): ExpandedToolResultText | Text {
|
|
316
|
+
const color = isError ? "error" : "muted";
|
|
317
|
+
if (!body.trim()) {
|
|
318
|
+
return new Text(theme.fg(color, renderCollapsedToolResult("Done")), 0, 0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const text = theme.fg(color, body);
|
|
322
|
+
const prefix = theme.fg(color, " │ ");
|
|
323
|
+
if (lastComponent instanceof ExpandedToolResultText) {
|
|
324
|
+
lastComponent.setText(text);
|
|
325
|
+
lastComponent.setPrefix(prefix);
|
|
326
|
+
return lastComponent;
|
|
327
|
+
}
|
|
328
|
+
return new ExpandedToolResultText(text, prefix);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function expandHint(theme: any): string {
|
|
332
|
+
// Keep interaction guidance neutral; it should not inherit success/error
|
|
333
|
+
// coloring from the tool result surrounding it.
|
|
334
|
+
return `${theme.fg("muted", " (")}${keyHint("app.tools.expand", "expand")}${theme.fg("muted", " / click)")}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
type SgrMousePacket = {
|
|
338
|
+
code: number;
|
|
339
|
+
col: number;
|
|
340
|
+
row: number;
|
|
341
|
+
final: "M" | "m";
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
type ToolRenderHit = {
|
|
345
|
+
component: any;
|
|
346
|
+
start: number;
|
|
347
|
+
end: number;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
|
|
351
|
+
const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h";
|
|
352
|
+
const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1000l";
|
|
353
|
+
const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
|
|
354
|
+
const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
|
|
355
|
+
const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
|
|
356
|
+
let toolMouseTui: any = null;
|
|
357
|
+
let toolMouseUi: any = null;
|
|
358
|
+
let toolMouseInputUnsubscribe: (() => void) | null = null;
|
|
359
|
+
let toolMouseInputPatchTui: any = null;
|
|
360
|
+
let toolMouseInputPatchOriginalHandle: ((...args: any[]) => any) | null = null;
|
|
361
|
+
let toolMouseInputPatchWrapper: ((...args: any[]) => any) | null = null;
|
|
362
|
+
let scrollButtonVisible = false;
|
|
363
|
+
let scrollButtonWidget: any = null;
|
|
364
|
+
let pendingScrollMessages = 0;
|
|
365
|
+
let assistantMessageActive = false;
|
|
366
|
+
let scrollButtonSyncScheduled = false;
|
|
367
|
+
let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
368
|
+
|
|
369
|
+
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
370
|
+
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
371
|
+
const packets: SgrMousePacket[] = [];
|
|
372
|
+
let offset = 0;
|
|
373
|
+
|
|
374
|
+
for (const match of data.matchAll(pattern)) {
|
|
375
|
+
if (match.index !== offset) return null;
|
|
376
|
+
offset = match.index + match[0].length;
|
|
377
|
+
packets.push({
|
|
378
|
+
code: Number(match[1]),
|
|
379
|
+
col: Number(match[2]),
|
|
380
|
+
row: Number(match[3]),
|
|
381
|
+
final: match[4] as "M" | "m",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return packets.length > 0 && offset === data.length ? packets : null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isSgrLeftPress(packet: SgrMousePacket): boolean {
|
|
389
|
+
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
390
|
+
return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function stripTerminalSequences(value: string): string {
|
|
394
|
+
return value
|
|
395
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
396
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
397
|
+
.replace(/\s+/g, " ")
|
|
398
|
+
.trim();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isToolExecutionComponent(value: any): boolean {
|
|
402
|
+
return Boolean(
|
|
403
|
+
value
|
|
404
|
+
&& typeof value === "object"
|
|
405
|
+
&& typeof value.toolCallId === "string"
|
|
406
|
+
&& typeof value.setExpanded === "function"
|
|
407
|
+
&& typeof value.render === "function",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function renderedLineCount(component: any, width: number, cache: WeakMap<object, number>): number {
|
|
412
|
+
if (!component || typeof component !== "object") return 0;
|
|
413
|
+
const cached = cache.get(component);
|
|
414
|
+
if (cached !== undefined) return cached;
|
|
415
|
+
|
|
416
|
+
let count = 0;
|
|
417
|
+
try {
|
|
418
|
+
const lines = component.render(width);
|
|
419
|
+
count = Array.isArray(lines) ? lines.length : 0;
|
|
420
|
+
} catch {
|
|
421
|
+
count = 0;
|
|
422
|
+
}
|
|
423
|
+
cache.set(component, count);
|
|
424
|
+
return count;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function collectToolRenderHits(
|
|
428
|
+
component: any,
|
|
429
|
+
start: number,
|
|
430
|
+
width: number,
|
|
431
|
+
hits: ToolRenderHit[],
|
|
432
|
+
cache: WeakMap<object, number>,
|
|
433
|
+
seen: Set<object>,
|
|
434
|
+
): number {
|
|
435
|
+
if (!component || typeof component !== "object" || seen.has(component)) return start;
|
|
436
|
+
seen.add(component);
|
|
437
|
+
|
|
438
|
+
const count = renderedLineCount(component, width, cache);
|
|
439
|
+
if (isToolExecutionComponent(component)) {
|
|
440
|
+
if (count > 0) hits.push({ component, start, end: start + count });
|
|
441
|
+
return start + count;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let childStart = start;
|
|
445
|
+
if (Array.isArray(component.children)) {
|
|
446
|
+
for (const child of component.children) {
|
|
447
|
+
collectToolRenderHits(child, childStart, width, hits, cache, seen);
|
|
448
|
+
childStart += renderedLineCount(child, width, cache);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return start + count;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function collectToolComponents(component: any, tools: any[], seen = new Set<any>()): void {
|
|
455
|
+
if (!component || typeof component !== "object" || seen.has(component)) return;
|
|
456
|
+
seen.add(component);
|
|
457
|
+
if (isToolExecutionComponent(component)) {
|
|
458
|
+
tools.push(component);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!Array.isArray(component.children)) return;
|
|
462
|
+
for (const child of component.children) collectToolComponents(child, tools, seen);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function fixedEditorLineMatch(rendered: string, visible: string): boolean {
|
|
466
|
+
return rendered === visible
|
|
467
|
+
|| (visible.length >= 8 && (rendered.includes(visible) || visible.includes(rendered)));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function fixedEditorContextScore(
|
|
471
|
+
renderedLines: string[],
|
|
472
|
+
renderedRow: number,
|
|
473
|
+
visibleLines: string[],
|
|
474
|
+
visibleRow: number,
|
|
475
|
+
): number {
|
|
476
|
+
let score = renderedLines[renderedRow] === visibleLines[visibleRow] ? 100 : 50;
|
|
477
|
+
for (const direction of [-1, 1]) {
|
|
478
|
+
for (let distance = 1; distance <= 4; distance++) {
|
|
479
|
+
const candidate = renderedLines[renderedRow + direction * distance];
|
|
480
|
+
const visible = visibleLines[visibleRow + direction * distance];
|
|
481
|
+
if (candidate === undefined || visible === undefined || candidate !== visible) break;
|
|
482
|
+
score += 5 - distance;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return score;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function findToolAtFixedEditorRow(
|
|
489
|
+
tui: any,
|
|
490
|
+
visibleRow: number,
|
|
491
|
+
previousLines: string[],
|
|
492
|
+
width: number,
|
|
493
|
+
): ToolRenderHit | null {
|
|
494
|
+
if (visibleRow < 0 || visibleRow >= previousLines.length) return null;
|
|
495
|
+
const visibleLines = previousLines.map((line) => stripTerminalSequences(String(line)));
|
|
496
|
+
const clickedLine = visibleLines[visibleRow] ?? "";
|
|
497
|
+
if (!clickedLine) return null;
|
|
498
|
+
|
|
499
|
+
const tools: any[] = [];
|
|
500
|
+
collectToolComponents(tui, tools);
|
|
501
|
+
let best: { hit: ToolRenderHit; score: number } | null = null;
|
|
502
|
+
for (const component of tools) {
|
|
503
|
+
const expanded = Boolean(component.expanded);
|
|
504
|
+
if (!expanded && !/(?:expand|\/ click)/i.test(clickedLine)) continue;
|
|
505
|
+
const renderedLines = renderComponentTree(component, width)
|
|
506
|
+
.map((line) => stripTerminalSequences(String(line)));
|
|
507
|
+
for (let renderedRow = 0; renderedRow < renderedLines.length; renderedRow++) {
|
|
508
|
+
if (!fixedEditorLineMatch(renderedLines[renderedRow] ?? "", clickedLine)) continue;
|
|
509
|
+
const score = fixedEditorContextScore(renderedLines, renderedRow, visibleLines, visibleRow);
|
|
510
|
+
if (!best || score > best.score) {
|
|
511
|
+
best = {
|
|
512
|
+
hit: { component, start: visibleRow, end: visibleRow + renderedLines.length },
|
|
513
|
+
score,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return best?.hit ?? null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function findToolAtScreenRow(tui: any, screenRow: number): ToolRenderHit | null {
|
|
522
|
+
const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
523
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
524
|
+
if (isFixedEditorTui(tui)) {
|
|
525
|
+
// Zentui replaces Pi's root render with the already-sliced visible
|
|
526
|
+
// transcript. previousViewportTop remains cursor bookkeeping and must not
|
|
527
|
+
// be added to a physical mouse row here.
|
|
528
|
+
return findToolAtFixedEditorRow(tui, screenRow - 1, previousLines, width);
|
|
529
|
+
}
|
|
530
|
+
const viewportTop = Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0;
|
|
531
|
+
const bufferRow = viewportTop + screenRow - 1;
|
|
532
|
+
if (bufferRow < 0 || bufferRow >= previousLines.length) return null;
|
|
533
|
+
|
|
534
|
+
const hits: ToolRenderHit[] = [];
|
|
535
|
+
const cache = new WeakMap<object, number>();
|
|
536
|
+
collectToolRenderHits(tui, 0, width, hits, cache, new Set<object>());
|
|
537
|
+
|
|
538
|
+
const clickedLine = stripTerminalSequences(String(previousLines[bufferRow] ?? ""));
|
|
539
|
+
for (const hit of hits) {
|
|
540
|
+
if (bufferRow < hit.start || bufferRow >= hit.end) continue;
|
|
541
|
+
const component = hit.component;
|
|
542
|
+
const expanded = Boolean(component.expanded);
|
|
543
|
+
// Match Pi's old click contract: collapsed results expose a click hint;
|
|
544
|
+
// expanded results can be collapsed by clicking any rendered line.
|
|
545
|
+
if (!expanded && !/(?:expand|\/ click)/i.test(clickedLine)) continue;
|
|
546
|
+
return hit;
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function isFixedEditorTui(tui: any): boolean {
|
|
552
|
+
const terminal = tui?.terminal;
|
|
553
|
+
if (!terminal) return false;
|
|
554
|
+
const ownRows = Object.getOwnPropertyDescriptor(terminal, "rows");
|
|
555
|
+
const prototype = Object.getPrototypeOf(terminal);
|
|
556
|
+
const inheritedRows = prototype
|
|
557
|
+
? Object.getOwnPropertyDescriptor(prototype, "rows")
|
|
558
|
+
: undefined;
|
|
559
|
+
return typeof ownRows?.get === "function" && ownRows.get !== inheritedRows?.get;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function formatShortcut(shortcut: string): string {
|
|
563
|
+
return shortcut
|
|
564
|
+
.split("+")
|
|
565
|
+
.map((part) => part.length <= 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
566
|
+
.join("+");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function isScrollBottomInput(data: string): boolean {
|
|
570
|
+
return matchesKey(data, SCROLL_BOTTOM_SHORTCUT);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function isScrollNavigationInput(data: string): boolean {
|
|
574
|
+
if (
|
|
575
|
+
matchesKey(data, "pageUp")
|
|
576
|
+
|| matchesKey(data, "pageDown")
|
|
577
|
+
|| ZENTUI_PAGE_UP_INPUT.test(data)
|
|
578
|
+
|| ZENTUI_PAGE_DOWN_INPUT.test(data)
|
|
579
|
+
) {
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
const packets = parseSgrMousePackets(data);
|
|
583
|
+
return Boolean(packets?.some((packet) => {
|
|
584
|
+
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
585
|
+
return packet.final === "M" && (baseButton === 64 || baseButton === 65);
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function directRenderLines(component: any, width: number): string[] {
|
|
590
|
+
try {
|
|
591
|
+
const lines = component?.render?.(width);
|
|
592
|
+
return Array.isArray(lines) ? lines : [];
|
|
593
|
+
} catch {
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Render only the transcript portion that fixed-editor leaves scrollable. */
|
|
599
|
+
function renderFixedScrollableRoot(tui: any, width: number): string[] {
|
|
600
|
+
const children = Array.isArray(tui?.children) ? tui.children : [];
|
|
601
|
+
const editorIndex = children.findIndex((child: any) =>
|
|
602
|
+
containsEditorLike(child, tui.focusedComponent),
|
|
603
|
+
);
|
|
604
|
+
const end = editorIndex >= 2 ? editorIndex - 2 : children.length;
|
|
605
|
+
return children.slice(0, end).flatMap((child: any) => directRenderLines(child, width));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function isFixedEditorAtBottom(tui: any): boolean {
|
|
609
|
+
if (!isFixedEditorTui(tui)) return true;
|
|
610
|
+
const visibleLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
611
|
+
if (visibleLines.length === 0) return true;
|
|
612
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
613
|
+
const rootLines = renderFixedScrollableRoot(tui, width);
|
|
614
|
+
if (rootLines.length <= visibleLines.length) return true;
|
|
615
|
+
const tail = rootLines.slice(-visibleLines.length);
|
|
616
|
+
return tail.every((line, index) =>
|
|
617
|
+
stripTerminalSequences(String(line)) === stripTerminalSequences(String(visibleLines[index] ?? "")),
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function hideScrollButton(tui: any): void {
|
|
622
|
+
const changed = scrollButtonVisible || pendingScrollMessages > 0;
|
|
623
|
+
scrollButtonVisible = false;
|
|
624
|
+
pendingScrollMessages = 0;
|
|
625
|
+
if (changed) tui.requestRender?.();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
629
|
+
if (!isScrollNavigationInput(data) || scrollButtonSyncScheduled) return;
|
|
630
|
+
scrollButtonSyncScheduled = true;
|
|
631
|
+
const previousLines = tui.previousLines;
|
|
632
|
+
const check = (attempt: number) => {
|
|
633
|
+
scrollButtonSyncScheduled = false;
|
|
634
|
+
if (toolMouseTui !== tui) return;
|
|
635
|
+
// Pi renders on its own frame timer. Inspect the resulting viewport before
|
|
636
|
+
// showing the button so empty or non-scrollable transcripts never flash it.
|
|
637
|
+
const rendered = tui.previousLines !== previousLines;
|
|
638
|
+
if (!rendered && attempt < 4) {
|
|
639
|
+
scrollButtonSyncScheduled = true;
|
|
640
|
+
const timer = setTimeout(() => check(attempt + 1), 16);
|
|
641
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
642
|
+
(timer as { unref: () => void }).unref();
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const nextVisible = !isFixedEditorAtBottom(tui);
|
|
647
|
+
if (!nextVisible) pendingScrollMessages = 0;
|
|
648
|
+
if (nextVisible !== scrollButtonVisible) {
|
|
649
|
+
scrollButtonVisible = nextVisible;
|
|
650
|
+
tui.requestRender?.();
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
process.nextTick(() => check(0));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function updateScrollButtonFromInput(tui: any, data: string): void {
|
|
657
|
+
if (!isFixedEditorTui(tui)) return;
|
|
658
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || isScrollBottomInput(data)) {
|
|
659
|
+
hideScrollButton(tui);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function renderComponentTree(component: any, width: number): string[] {
|
|
664
|
+
if (!component || typeof component !== "object") return [];
|
|
665
|
+
try {
|
|
666
|
+
const lines = component.render?.(width);
|
|
667
|
+
if (Array.isArray(lines) && lines.length > 0) return lines;
|
|
668
|
+
} catch {
|
|
669
|
+
// Fall through to children for hidden container renderers.
|
|
670
|
+
}
|
|
671
|
+
if (!Array.isArray(component.children)) return [];
|
|
672
|
+
return component.children.flatMap((child: any) => renderComponentTree(child, width));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function renderTreeWithTarget(
|
|
676
|
+
component: any,
|
|
677
|
+
target: any,
|
|
678
|
+
width: number,
|
|
679
|
+
seen = new Set<any>(),
|
|
680
|
+
): { lines: string[]; targetStart: number | null } {
|
|
681
|
+
if (!component || typeof component !== "object" || seen.has(component)) {
|
|
682
|
+
return { lines: [], targetStart: null };
|
|
683
|
+
}
|
|
684
|
+
seen.add(component);
|
|
685
|
+
if (component === target) {
|
|
686
|
+
return { lines: renderComponentTree(component, width), targetStart: 0 };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (Array.isArray(component.children)) {
|
|
690
|
+
const lines: string[] = [];
|
|
691
|
+
let targetStart: number | null = null;
|
|
692
|
+
for (const child of component.children) {
|
|
693
|
+
const result = renderTreeWithTarget(child, target, width, seen);
|
|
694
|
+
if (result.targetStart !== null) targetStart = lines.length + result.targetStart;
|
|
695
|
+
lines.push(...result.lines);
|
|
696
|
+
}
|
|
697
|
+
if (targetStart !== null) return { lines, targetStart };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return { lines: renderComponentTree(component, width), targetStart: null };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function normalizedClusterLines(component: any, width: number): string[] {
|
|
704
|
+
if (!component) return [];
|
|
705
|
+
const lines = renderComponentTree(component, width);
|
|
706
|
+
let end = lines.length;
|
|
707
|
+
while (end > 0 && visibleWidth(lines[end - 1] ?? "") === 0) end--;
|
|
708
|
+
return lines.slice(0, Math.max(end, 1));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function rawTerminalRows(tui: any): number {
|
|
712
|
+
const terminal = tui?.terminal;
|
|
713
|
+
if (!terminal) return 0;
|
|
714
|
+
const prototype = Object.getPrototypeOf(terminal);
|
|
715
|
+
const rows = prototype ? Object.getOwnPropertyDescriptor(prototype, "rows") : undefined;
|
|
716
|
+
if (typeof rows?.get === "function") {
|
|
717
|
+
try {
|
|
718
|
+
const value = rows.get.call(terminal);
|
|
719
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
720
|
+
} catch {
|
|
721
|
+
// Fall through to the current terminal value.
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return typeof terminal.rows === "number" && Number.isFinite(terminal.rows) ? terminal.rows : 0;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function containsEditorLike(component: any, focused: any, seen = new Set<any>()): boolean {
|
|
728
|
+
if (!component || typeof component !== "object" || seen.has(component)) return false;
|
|
729
|
+
seen.add(component);
|
|
730
|
+
if (component === focused) return true;
|
|
731
|
+
if (
|
|
732
|
+
typeof component.getText === "function"
|
|
733
|
+
&& typeof component.setText === "function"
|
|
734
|
+
&& typeof component.handleInput === "function"
|
|
735
|
+
) return true;
|
|
736
|
+
return Array.isArray(component.children)
|
|
737
|
+
&& component.children.some((child: any) => containsEditorLike(child, focused, seen));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function scrollButtonScreenRow(tui: any, width: number): number | null {
|
|
741
|
+
if (!scrollButtonVisible || !isFixedEditorTui(tui) || !scrollButtonWidget) return null;
|
|
742
|
+
const children = Array.isArray(tui?.children) ? tui.children : [];
|
|
743
|
+
const editorIndex = children.findIndex((child: any) =>
|
|
744
|
+
containsEditorLike(child, tui.focusedComponent),
|
|
745
|
+
);
|
|
746
|
+
if (editorIndex < 2 || editorIndex + 2 >= children.length) return null;
|
|
747
|
+
|
|
748
|
+
const above = children[editorIndex - 1];
|
|
749
|
+
const widthValue = Math.max(1, width || Number(tui?.terminal?.columns) || 80);
|
|
750
|
+
const target = renderTreeWithTarget(above, scrollButtonWidget, widthValue);
|
|
751
|
+
if (target.targetStart === null) return null;
|
|
752
|
+
|
|
753
|
+
const rawRows = rawTerminalRows(tui);
|
|
754
|
+
if (rawRows <= 0) return null;
|
|
755
|
+
const maxRows = Math.max(1, rawRows - 1);
|
|
756
|
+
const status = normalizedClusterLines(children[editorIndex - 2], widthValue);
|
|
757
|
+
const editor = normalizedClusterLines(children[editorIndex], widthValue);
|
|
758
|
+
const below = normalizedClusterLines(children[editorIndex + 1], widthValue);
|
|
759
|
+
const footer = normalizedClusterLines(children[editorIndex + 2], widthValue);
|
|
760
|
+
const aboveLines = target.lines.length > 0 ? target.lines : normalizedClusterLines(above, widthValue);
|
|
761
|
+
|
|
762
|
+
const takeLast = (lines: string[], count: number): string[] =>
|
|
763
|
+
count > 0 ? lines.slice(-count) : [];
|
|
764
|
+
const editorVisible = takeLast(editor, Math.min(editor.length, maxRows));
|
|
765
|
+
let remaining = Math.max(0, maxRows - editorVisible.length);
|
|
766
|
+
const footerVisible = takeLast(footer, remaining);
|
|
767
|
+
remaining -= footerVisible.length;
|
|
768
|
+
const belowVisible = takeLast(below, remaining);
|
|
769
|
+
remaining -= belowVisible.length;
|
|
770
|
+
const aboveVisible = takeLast(aboveLines, remaining);
|
|
771
|
+
const statusVisible = takeLast(status, Math.max(0, remaining - aboveVisible.length));
|
|
772
|
+
const aboveStart = aboveLines.length - aboveVisible.length;
|
|
773
|
+
const targetRow = target.targetStart - aboveStart;
|
|
774
|
+
if (targetRow < 0 || targetRow >= aboveVisible.length) return null;
|
|
775
|
+
|
|
776
|
+
const allLines = [...statusVisible, ...aboveVisible, ...editorVisible, ...belowVisible, ...footerVisible];
|
|
777
|
+
let leadingBlank = 0;
|
|
778
|
+
while (leadingBlank < allLines.length - 1 && visibleWidth(allLines[leadingBlank] ?? "") === 0) {
|
|
779
|
+
leadingBlank++;
|
|
780
|
+
}
|
|
781
|
+
const clusterRow = statusVisible.length + targetRow - leadingBlank;
|
|
782
|
+
if (clusterRow < 0 || clusterRow >= allLines.length - leadingBlank) return null;
|
|
783
|
+
return rawRows - allLines.length + clusterRow + 1;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function isScrollButtonAtScreenRow(tui: any, packet: SgrMousePacket): boolean {
|
|
787
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
788
|
+
if (scrollButtonScreenRow(tui, width) !== packet.row || !scrollButtonWidget) return false;
|
|
789
|
+
const rendered = scrollButtonWidget.render?.(width)?.[0];
|
|
790
|
+
if (typeof rendered !== "string") return false;
|
|
791
|
+
const plain = rendered
|
|
792
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
793
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
794
|
+
const leading = plain.length - plain.trimStart().length;
|
|
795
|
+
const end = visibleWidth(plain.trimEnd());
|
|
796
|
+
return packet.col >= leading + 1 && packet.col <= end;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function jumpToBottomWithoutSubmit(tui: any): boolean {
|
|
800
|
+
const originalHandle = toolMouseInputPatchTui === tui
|
|
801
|
+
? toolMouseInputPatchOriginalHandle
|
|
802
|
+
: null;
|
|
803
|
+
if (!originalHandle) return false;
|
|
804
|
+
|
|
805
|
+
// Route Enter through Pi's normal listener chain so pi-zentui can update its
|
|
806
|
+
// private scroll offset, but suppress the focused editor for this synthetic
|
|
807
|
+
// dispatch so clicking the button never submits the current input.
|
|
808
|
+
const focused = tui.focusedComponent;
|
|
809
|
+
try {
|
|
810
|
+
tui.focusedComponent = null;
|
|
811
|
+
Reflect.apply(originalHandle, tui, ["\r"]);
|
|
812
|
+
} finally {
|
|
813
|
+
tui.focusedComponent = focused;
|
|
814
|
+
}
|
|
815
|
+
hideScrollButton(tui);
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function handleScrollButtonClick(tui: any, packet: SgrMousePacket): boolean {
|
|
820
|
+
if (!isScrollButtonAtScreenRow(tui, packet)) return false;
|
|
821
|
+
return jumpToBottomWithoutSubmit(tui);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
825
|
+
const hit = findToolAtScreenRow(tui, packet.row);
|
|
826
|
+
if (!hit) return false;
|
|
827
|
+
|
|
828
|
+
const nextExpanded = !Boolean(hit.component.expanded);
|
|
829
|
+
hit.component.setExpanded(nextExpanded);
|
|
830
|
+
hit.component.invalidate?.();
|
|
831
|
+
tui.requestRender?.();
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function renderScrollButton(width: number, theme: any): string[] {
|
|
836
|
+
if (!scrollButtonVisible || !isFixedEditorTui(toolMouseTui)) return [];
|
|
837
|
+
const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
|
|
838
|
+
const messageText = pendingScrollMessages > 0
|
|
839
|
+
? `${pendingScrollMessages} new message${pendingScrollMessages === 1 ? "" : "s"}`
|
|
840
|
+
: "Back to bottom";
|
|
841
|
+
const label = theme.fg("accent", `[ ↓ ${messageText} · ${shortcut} · click ]`);
|
|
842
|
+
const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
|
|
843
|
+
return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* pi-zentui consumes left-button presses for text selection. Intercept only a
|
|
848
|
+
* tool-row click at the TUI input boundary, before extension listeners run.
|
|
849
|
+
* Keyboard, wheel, drag, release, and non-tool clicks continue through Pi's
|
|
850
|
+
* original dispatcher, preserving pi-zentui's scroll-to-bottom behavior.
|
|
851
|
+
*/
|
|
852
|
+
function patchToolMouseInputCapture(tui: any): void {
|
|
853
|
+
if (toolMouseInputPatchTui === tui) return;
|
|
854
|
+
|
|
855
|
+
restoreToolMouseInputCapture();
|
|
856
|
+
const originalHandle = tui?.handleInput;
|
|
857
|
+
if (typeof originalHandle !== "function") return;
|
|
858
|
+
|
|
859
|
+
const wrapper = function (this: any, ...args: any[]): any {
|
|
860
|
+
const data = args[0];
|
|
861
|
+
if (typeof data === "string") {
|
|
862
|
+
updateScrollButtonFromInput(this, data);
|
|
863
|
+
// Capture the current viewport before Pi/Zentui applies the scroll input.
|
|
864
|
+
scheduleScrollButtonSync(this, data);
|
|
865
|
+
if (isFixedEditorTui(this) && isScrollBottomInput(data) && jumpToBottomWithoutSubmit(this)) return;
|
|
866
|
+
const packets = parseSgrMousePackets(data);
|
|
867
|
+
if (packets) {
|
|
868
|
+
for (const packet of packets) {
|
|
869
|
+
if (!isSgrLeftPress(packet)) continue;
|
|
870
|
+
if (handleScrollButtonClick(this, packet) || toggleToolAtMouseClick(this, packet)) return;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const result = Reflect.apply(originalHandle, this, args);
|
|
875
|
+
if (typeof data === "string") scheduleScrollButtonSync(this, data);
|
|
876
|
+
return result;
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
tui.handleInput = wrapper;
|
|
881
|
+
} catch {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
toolMouseInputPatchTui = tui;
|
|
885
|
+
toolMouseInputPatchOriginalHandle = originalHandle;
|
|
886
|
+
toolMouseInputPatchWrapper = wrapper;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function restoreToolMouseInputCapture(): void {
|
|
890
|
+
if (
|
|
891
|
+
toolMouseInputPatchTui
|
|
892
|
+
&& toolMouseInputPatchOriginalHandle
|
|
893
|
+
&& toolMouseInputPatchTui.handleInput === toolMouseInputPatchWrapper
|
|
894
|
+
) {
|
|
895
|
+
toolMouseInputPatchTui.handleInput = toolMouseInputPatchOriginalHandle;
|
|
896
|
+
}
|
|
897
|
+
toolMouseInputPatchTui = null;
|
|
898
|
+
toolMouseInputPatchOriginalHandle = null;
|
|
899
|
+
toolMouseInputPatchWrapper = null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
903
|
+
if (!toolMouseTui) return undefined;
|
|
904
|
+
updateScrollButtonFromInput(toolMouseTui, data);
|
|
905
|
+
if (isFixedEditorTui(toolMouseTui) && isScrollBottomInput(data) && jumpToBottomWithoutSubmit(toolMouseTui)) {
|
|
906
|
+
return { consume: true };
|
|
907
|
+
}
|
|
908
|
+
const packets = parseSgrMousePackets(data);
|
|
909
|
+
if (!packets) {
|
|
910
|
+
scheduleScrollButtonSync(toolMouseTui, data);
|
|
911
|
+
return undefined;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
let consumed = false;
|
|
915
|
+
for (const packet of packets) {
|
|
916
|
+
if (!isSgrLeftPress(packet)) continue;
|
|
917
|
+
if (handleScrollButtonClick(toolMouseTui, packet) || toggleToolAtMouseClick(toolMouseTui, packet)) {
|
|
918
|
+
consumed = true;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Let scrolling, motion, release, and clicks outside tool results reach the
|
|
923
|
+
// normal TUI input chain (including other extensions such as pi-zentui).
|
|
924
|
+
scheduleScrollButtonSync(toolMouseTui, data);
|
|
925
|
+
return consumed ? { consume: true } : undefined;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function teardownToolMouseInteraction(): void {
|
|
929
|
+
if (sessionRenderTimer) {
|
|
930
|
+
clearTimeout(sessionRenderTimer);
|
|
931
|
+
sessionRenderTimer = null;
|
|
932
|
+
}
|
|
933
|
+
toolMouseInputUnsubscribe?.();
|
|
934
|
+
toolMouseInputUnsubscribe = null;
|
|
935
|
+
try {
|
|
936
|
+
toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
937
|
+
} catch {
|
|
938
|
+
// The terminal may already be closed during shutdown.
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
toolMouseUi?.setWidget?.(TOOL_MOUSE_WIDGET_KEY, undefined);
|
|
942
|
+
} catch {
|
|
943
|
+
// The UI context may already have been reset during /reload.
|
|
944
|
+
}
|
|
945
|
+
restoreToolMouseInputCapture();
|
|
946
|
+
scrollButtonVisible = false;
|
|
947
|
+
scrollButtonWidget = null;
|
|
948
|
+
pendingScrollMessages = 0;
|
|
949
|
+
assistantMessageActive = false;
|
|
950
|
+
scrollButtonSyncScheduled = false;
|
|
951
|
+
toolMouseTui = null;
|
|
952
|
+
toolMouseUi = null;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function installToolMouseInteraction(ctx: any): void {
|
|
956
|
+
teardownToolMouseInteraction();
|
|
957
|
+
if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
|
|
958
|
+
if (typeof ctx.ui?.onTerminalInput !== "function" || typeof ctx.ui?.setWidget !== "function") return;
|
|
959
|
+
|
|
960
|
+
toolMouseUi = ctx.ui;
|
|
961
|
+
ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
|
|
962
|
+
toolMouseTui = tui;
|
|
963
|
+
patchToolMouseInputCapture(tui);
|
|
964
|
+
tui?.terminal?.write?.(TOOL_MOUSE_ENABLE);
|
|
965
|
+
const widget = {
|
|
966
|
+
render: (width: number) => renderScrollButton(width, theme),
|
|
967
|
+
invalidate() {},
|
|
968
|
+
};
|
|
969
|
+
scrollButtonWidget = widget;
|
|
970
|
+
return widget;
|
|
971
|
+
});
|
|
972
|
+
toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function scheduleSessionRender(): void {
|
|
976
|
+
const tui = toolMouseTui;
|
|
977
|
+
if (!tui || typeof tui.requestRender !== "function") return;
|
|
978
|
+
if (sessionRenderTimer) clearTimeout(sessionRenderTimer);
|
|
979
|
+
// Restored transcripts are populated at different points for startup, reload,
|
|
980
|
+
// and session replacement. Repaint after session_start and the surrounding UI
|
|
981
|
+
// rebuild finish so messages are not left hidden until the next terminal input.
|
|
982
|
+
sessionRenderTimer = setTimeout(() => {
|
|
983
|
+
sessionRenderTimer = null;
|
|
984
|
+
if (toolMouseTui === tui) tui.requestRender(true);
|
|
985
|
+
}, 0);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Bright green for success icon (truecolor ANSI escape)
|
|
989
|
+
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
990
|
+
const ANSI_RESET = "\x1b[0m";
|
|
991
|
+
|
|
992
|
+
function fileAction(args: any, verb: string): string {
|
|
993
|
+
const path = shortenPath(args?.path || "...");
|
|
994
|
+
return `${verb}(${path})`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
type DiffStats = { added: number; removed: number };
|
|
998
|
+
type FileDiffPreview = {
|
|
999
|
+
diff: string;
|
|
1000
|
+
error?: string;
|
|
1001
|
+
notice?: string;
|
|
1002
|
+
stats?: DiffStats;
|
|
1003
|
+
/** Pre-split outside the render hot path. */
|
|
1004
|
+
lines?: string[];
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// Diff generation is intentionally bounded: it runs before the mutation, but
|
|
1008
|
+
// should never add unbounded latency or memory pressure to a tool call.
|
|
1009
|
+
const MAX_DIFF_INPUT_BYTES = 2 * 1024 * 1024;
|
|
1010
|
+
const COLLAPSED_DIFF_LINES = 12;
|
|
1011
|
+
const EXPANDED_DIFF_LINES = 40;
|
|
1012
|
+
const diffPreviewByCall = new Map<string, FileDiffPreview | undefined>();
|
|
1013
|
+
|
|
1014
|
+
function diffStats(lines: readonly string[]): DiffStats {
|
|
1015
|
+
let added = 0;
|
|
1016
|
+
let removed = 0;
|
|
1017
|
+
for (const line of lines) {
|
|
1018
|
+
if (/^\+\s*\d+\s/.test(line)) added++;
|
|
1019
|
+
else if (/^-\s*\d+\s/.test(line)) removed++;
|
|
1020
|
+
}
|
|
1021
|
+
return { added, removed };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function diffPreview(diff: string): FileDiffPreview {
|
|
1025
|
+
const lines = diff.split("\n");
|
|
1026
|
+
return { diff, lines, stats: diffStats(lines) };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function normalizeDiffText(text: string): string {
|
|
1030
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function editEntries(args: any): Array<{ oldText: string; newText: string }> {
|
|
1034
|
+
if (Array.isArray(args?.edits)) {
|
|
1035
|
+
return args.edits.filter((edit: any) =>
|
|
1036
|
+
typeof edit?.oldText === "string" && typeof edit?.newText === "string",
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
if (typeof args?.oldText === "string" && typeof args?.newText === "string") {
|
|
1040
|
+
return [{ oldText: args.oldText, newText: args.newText }];
|
|
1041
|
+
}
|
|
1042
|
+
return [];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** Build the preview before mutation, outside the renderer's hot path. */
|
|
1046
|
+
async function buildFileDiffPreview(toolName: string, args: any, cwd: string): Promise<FileDiffPreview | undefined> {
|
|
1047
|
+
const rawPath = typeof args?.path === "string" ? args.path : typeof args?.file_path === "string" ? args.file_path : "";
|
|
1048
|
+
if (!rawPath) return undefined;
|
|
1049
|
+
|
|
1050
|
+
if (toolName === "write" && typeof args?.content === "string" && Buffer.byteLength(args.content, "utf8") > MAX_DIFF_INPUT_BYTES) {
|
|
1051
|
+
return { diff: "", notice: "Diff omitted: new content exceeds 2 MiB" };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const absolutePath = resolve(cwd || process.cwd(), rawPath);
|
|
1055
|
+
let before = "";
|
|
1056
|
+
try {
|
|
1057
|
+
const info = await stat(absolutePath);
|
|
1058
|
+
if (info.size > MAX_DIFF_INPUT_BYTES) {
|
|
1059
|
+
return { diff: "", notice: "Diff omitted: existing file exceeds 2 MiB" };
|
|
1060
|
+
}
|
|
1061
|
+
before = await readFile(absolutePath, "utf8");
|
|
1062
|
+
} catch (error: any) {
|
|
1063
|
+
if (error?.code !== "ENOENT") {
|
|
1064
|
+
return { diff: "", error: `Cannot read ${shortenPath(rawPath)}: ${oneLine(error, 72)}` };
|
|
1065
|
+
}
|
|
1066
|
+
if (toolName === "edit") return { diff: "", error: `File not found: ${shortenPath(rawPath)}` };
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
before = normalizeDiffText(before);
|
|
1070
|
+
let after = before;
|
|
1071
|
+
if (toolName === "write") {
|
|
1072
|
+
if (typeof args?.content !== "string") return undefined;
|
|
1073
|
+
after = normalizeDiffText(args.content);
|
|
1074
|
+
} else {
|
|
1075
|
+
const edits = editEntries(args);
|
|
1076
|
+
if (edits.length === 0) return undefined;
|
|
1077
|
+
const replacements: Array<{ start: number; end: number; text: string }> = [];
|
|
1078
|
+
for (const edit of edits) {
|
|
1079
|
+
const oldText = normalizeDiffText(edit.oldText);
|
|
1080
|
+
const start = after.indexOf(oldText);
|
|
1081
|
+
if (start < 0) return { diff: "", error: "Preview unavailable: old text was not found" };
|
|
1082
|
+
if (after.indexOf(oldText, start + 1) >= 0) {
|
|
1083
|
+
return { diff: "", error: "Preview unavailable: old text is not unique" };
|
|
1084
|
+
}
|
|
1085
|
+
replacements.push({ start, end: start + oldText.length, text: normalizeDiffText(edit.newText) });
|
|
1086
|
+
}
|
|
1087
|
+
for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
|
|
1088
|
+
after = after.slice(0, replacement.start) + replacement.text + after.slice(replacement.end);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (Buffer.byteLength(after, "utf8") > MAX_DIFF_INPUT_BYTES) {
|
|
1093
|
+
return { diff: "", notice: "Diff omitted: resulting content exceeds 2 MiB" };
|
|
1094
|
+
}
|
|
1095
|
+
const diff = generateDiffString(before, after).diff;
|
|
1096
|
+
return diff ? diffPreview(diff) : undefined;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function diffLineCount(preview: FileDiffPreview | undefined): number {
|
|
1100
|
+
if (!preview?.diff) return 0;
|
|
1101
|
+
return preview.lines?.length ?? preview.diff.split("\n").length;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function hasExpandableDiff(preview: FileDiffPreview | undefined): boolean {
|
|
1105
|
+
return diffLineCount(preview) > COLLAPSED_DIFF_LINES;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function renderDiffPreview(preview: FileDiffPreview | undefined, theme: any, expanded: boolean): string {
|
|
1109
|
+
if (!preview) return "";
|
|
1110
|
+
if (preview.error) return theme.fg("error", ` ${preview.error}`);
|
|
1111
|
+
if (preview.notice) return theme.fg("muted", ` ${preview.notice}`);
|
|
1112
|
+
if (!preview.diff.trim()) return "";
|
|
1113
|
+
|
|
1114
|
+
// Bound work before calling pi's renderer. Rendering only the visible raw
|
|
1115
|
+
// lines avoids repeatedly running intra-line diffing over a multi-megabyte diff.
|
|
1116
|
+
const rawLines = preview.lines ?? preview.diff.split("\n");
|
|
1117
|
+
const maxLines = expanded ? EXPANDED_DIFF_LINES : COLLAPSED_DIFF_LINES;
|
|
1118
|
+
const shown = renderDiff(rawLines.slice(0, maxLines).join("\n"))
|
|
1119
|
+
.split("\n")
|
|
1120
|
+
.map((line) => ` ${line}`);
|
|
1121
|
+
if (rawLines.length > maxLines) {
|
|
1122
|
+
shown.push(theme.fg("muted", ` … ${rawLines.length - maxLines} more diff lines`));
|
|
1123
|
+
}
|
|
1124
|
+
return shown.join("\n");
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function renderFileDiffPreview(_toolName: string, _args: any, theme: any, context: any): string {
|
|
1128
|
+
const state = (context.state ??= {});
|
|
1129
|
+
if (diffPreviewByCall.has(context.toolCallId)) {
|
|
1130
|
+
state.ccstyleDiffPreview = diffPreviewByCall.get(context.toolCallId);
|
|
1131
|
+
diffPreviewByCall.delete(context.toolCallId);
|
|
1132
|
+
}
|
|
1133
|
+
return renderDiffPreview(state.ccstyleDiffPreview, theme, isToolExpanded(undefined, context));
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function applyEnabledState(enabled: boolean, ctx: any): void {
|
|
1137
|
+
config.enabled = enabled;
|
|
1138
|
+
saveConfig();
|
|
1139
|
+
ctx.ui.notify(`Claude Code style: ${enabled ? "on" : "off"}`, "info");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function showCcstylePanel(ctx: any): Promise<void> {
|
|
1143
|
+
if (ctx?.mode !== "tui" || !ctx?.hasUI || typeof ctx.ui?.custom !== "function") {
|
|
1144
|
+
ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
await ctx.ui.custom((tui: any, theme: any, _keybindings: any, done: (value?: void) => void) => {
|
|
1149
|
+
const container = new Container();
|
|
1150
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Claude Code Style")), 1, 1));
|
|
1151
|
+
const settingsList = new SettingsList(
|
|
1152
|
+
[{
|
|
1153
|
+
id: "enabled",
|
|
1154
|
+
label: "Output style",
|
|
1155
|
+
description: "Use compact Claude Code-like tool rendering",
|
|
1156
|
+
currentValue: config.enabled ? "on" : "off",
|
|
1157
|
+
values: ["on", "off"],
|
|
1158
|
+
}],
|
|
1159
|
+
3,
|
|
1160
|
+
getSettingsListTheme(),
|
|
1161
|
+
(id: string, newValue: string) => {
|
|
1162
|
+
if (id !== "enabled" || (newValue !== "on" && newValue !== "off")) return;
|
|
1163
|
+
applyEnabledState(newValue === "on", ctx);
|
|
1164
|
+
settingsList.updateValue(id, newValue);
|
|
1165
|
+
tui.requestRender();
|
|
1166
|
+
},
|
|
1167
|
+
() => done(),
|
|
1168
|
+
);
|
|
1169
|
+
container.addChild(settingsList);
|
|
1170
|
+
return {
|
|
1171
|
+
render: (width: number) => container.render(width),
|
|
1172
|
+
invalidate: () => container.invalidate(),
|
|
1173
|
+
handleInput(data: string) {
|
|
1174
|
+
settingsList.handleInput?.(data);
|
|
1175
|
+
tui.requestRender();
|
|
1176
|
+
},
|
|
1177
|
+
};
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
|
|
1182
|
+
try {
|
|
1183
|
+
if (typeof tool?.[slot] === "function") return tool[slot](...args);
|
|
1184
|
+
} catch {
|
|
1185
|
+
// Fall through to raw fallback.
|
|
1186
|
+
}
|
|
1187
|
+
return new Text(fallback, 0, 0);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function createBuiltInTools(cwd: string) {
|
|
1191
|
+
return {
|
|
1192
|
+
// Keep complete definitions so wrapped tools retain prompt metadata,
|
|
1193
|
+
// argument preparation, execution mode, and native fallback renderers.
|
|
1194
|
+
read: createReadToolDefinition(cwd),
|
|
1195
|
+
bash: createBashToolDefinition(cwd),
|
|
1196
|
+
edit: createEditToolDefinition(cwd),
|
|
1197
|
+
write: createWriteToolDefinition(cwd),
|
|
1198
|
+
find: createFindToolDefinition(cwd),
|
|
1199
|
+
grep: createGrepToolDefinition(cwd),
|
|
1200
|
+
ls: createLsToolDefinition(cwd),
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
|
|
1205
|
+
function getBuiltInTools(cwd: string) {
|
|
1206
|
+
let tools = toolCache.get(cwd);
|
|
1207
|
+
if (!tools) {
|
|
1208
|
+
tools = createBuiltInTools(cwd);
|
|
1209
|
+
toolCache.set(cwd, tools);
|
|
1210
|
+
}
|
|
1211
|
+
return tools;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** Tools explicitly wrapped with bespoke call/result renderers. Skip these in the generic interceptor. */
|
|
1215
|
+
const WRAPPED_BUILTINS = new Set<string>();
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Generate a descriptive label for an unknown tool from its args.
|
|
1219
|
+
* Uses the tool label + first stringable arg value.
|
|
1220
|
+
*/
|
|
1221
|
+
function toolCallLabel(toolName: string, toolLabel: string, args: any): string {
|
|
1222
|
+
if (!args) return toolLabel;
|
|
1223
|
+
const keys = Object.keys(args);
|
|
1224
|
+
if (keys.length === 0) return toolLabel;
|
|
1225
|
+
// Prefer "query" or "question" args, then fall back to the first stringable value
|
|
1226
|
+
const preferred = ["query", "question", "command", "pattern", "name", "path", "url", "message"];
|
|
1227
|
+
for (const key of preferred) {
|
|
1228
|
+
const val = args[key];
|
|
1229
|
+
if (val !== undefined && val !== null && typeof val !== "object") {
|
|
1230
|
+
return `${toolLabel}(${oneLine(val, 60)})`;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const firstKey = keys[0];
|
|
1234
|
+
const firstVal = args[firstKey];
|
|
1235
|
+
if (firstVal !== undefined && firstVal !== null && typeof firstVal !== "object") {
|
|
1236
|
+
return `${toolLabel}(${oneLine(firstVal, 60)})`;
|
|
1237
|
+
}
|
|
1238
|
+
return toolLabel;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/** Wrap an arbitrary tool definition with ccstyle call/result rendering. */
|
|
1242
|
+
function createCcstyleTool(originalTool: any): any {
|
|
1243
|
+
const toolName = originalTool.name;
|
|
1244
|
+
const label = isMcpToolDefinition(originalTool, toolName)
|
|
1245
|
+
? toolName
|
|
1246
|
+
: originalTool.label || toolName;
|
|
1247
|
+
|
|
1248
|
+
return {
|
|
1249
|
+
...originalTool,
|
|
1250
|
+
renderShell: "self",
|
|
1251
|
+
renderCall(args: any, theme: any, context: any) {
|
|
1252
|
+
if (!config.enabled) {
|
|
1253
|
+
return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const visualState = resolveToolVisualState(context);
|
|
1257
|
+
const isPending =
|
|
1258
|
+
visualState === "pending" ||
|
|
1259
|
+
(!visualState && (context?.isPartial || context?.executionStarted));
|
|
1260
|
+
if (isPending) scheduleAnimation(context);
|
|
1261
|
+
const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
|
|
1262
|
+
const icon =
|
|
1263
|
+
visualState === "success"
|
|
1264
|
+
? `${BRIGHT_GREEN}${rawIcon}${ANSI_RESET}`
|
|
1265
|
+
: theme.fg(toolIconColor(context), rawIcon);
|
|
1266
|
+
|
|
1267
|
+
const title = `${icon} ${theme.fg("toolTitle", toolCallLabel(toolName, label, args))}`;
|
|
1268
|
+
return new Text(title, 0, 0);
|
|
1269
|
+
},
|
|
1270
|
+
renderResult(result: any, options: any, theme: any, context: any) {
|
|
1271
|
+
if (!config.enabled) {
|
|
1272
|
+
return renderDefault(originalTool, "renderResult", [result, options, theme, context], textFromResult(result));
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (options?.isPartial) {
|
|
1276
|
+
return new Text(theme.fg("muted", " ⎿ Pending…"), 0, 0);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const isError = options?.isError || context?.isError;
|
|
1280
|
+
setToolVisualState(context, isError ? "error" : "success");
|
|
1281
|
+
const expanded = isToolExpanded(options, context);
|
|
1282
|
+
|
|
1283
|
+
const text = textFromResult(result);
|
|
1284
|
+
const rendered = !expanded ? (text ? oneLine(text, 96) : "Done") : text;
|
|
1285
|
+
|
|
1286
|
+
const hint = !expanded && hasExpandableResult(text) ? expandHint(theme) : "";
|
|
1287
|
+
if (expanded) return renderExpandedToolResult(rendered, theme, isError, context?.lastComponent);
|
|
1288
|
+
return new Text(
|
|
1289
|
+
theme.fg(isError ? "error" : "muted", renderCollapsedToolResult(rendered, hint)),
|
|
1290
|
+
0,
|
|
1291
|
+
0,
|
|
1292
|
+
);
|
|
1293
|
+
},
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Extension APIs are isolated per plugin, so replacing this extension's
|
|
1299
|
+
* pi.registerTool cannot see SDK tools or tools registered by other plugins.
|
|
1300
|
+
* ToolExecutionComponent is shared by the TUI and exported by pi; patch its
|
|
1301
|
+
* renderer lookup once so every generic tool uses the same compact shell.
|
|
1302
|
+
* Only subagent tools keep their specialized renderers. Every other tool,
|
|
1303
|
+
* including MCP and extension tools, uses the same compact ccstyle shell.
|
|
1304
|
+
*/
|
|
1305
|
+
const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
|
|
1306
|
+
const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
|
|
1307
|
+
const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
|
|
1308
|
+
const SUBAGENT_TOOL_NAMES = new Set(["Agent"]);
|
|
1309
|
+
const globalToolRenderOwner = {};
|
|
1310
|
+
|
|
1311
|
+
type GlobalToolRenderPatch = {
|
|
1312
|
+
prototype: any;
|
|
1313
|
+
owner: object;
|
|
1314
|
+
enabled: () => boolean;
|
|
1315
|
+
wrap: (tool: any) => any;
|
|
1316
|
+
byDefinition: WeakMap<object, any>;
|
|
1317
|
+
byName: Map<string, any>;
|
|
1318
|
+
originalHasRendererDefinition: (...args: any[]) => boolean;
|
|
1319
|
+
originalGetRenderShell: (...args: any[]) => "default" | "self";
|
|
1320
|
+
originalGetCallRenderer: (...args: any[]) => any;
|
|
1321
|
+
originalGetResultRenderer: (...args: any[]) => any;
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
function isMcpToolDefinition(definition: any, toolName: string): boolean {
|
|
1325
|
+
const label = typeof definition?.label === "string" ? definition.label : "";
|
|
1326
|
+
return toolName === "mcp" || label === "MCP" || label.startsWith("MCP: ");
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function preservesOriginalRenderer(_definition: any, toolName: string): boolean {
|
|
1330
|
+
return SUBAGENT_TOOL_NAMES.has(toolName);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
|
|
1334
|
+
const selectedMode = component[COMPONENT_TOOL_RENDER_MODE];
|
|
1335
|
+
if (typeof selectedMode === "boolean") return selectedMode;
|
|
1336
|
+
|
|
1337
|
+
const definition = component.toolDefinition ?? component.builtInToolDefinition;
|
|
1338
|
+
const toolName = String(component.toolName || definition?.name || "");
|
|
1339
|
+
const useCcstyle =
|
|
1340
|
+
patch.enabled() &&
|
|
1341
|
+
!preservesOriginalRenderer(definition, toolName) &&
|
|
1342
|
+
definition?.renderShell !== "self";
|
|
1343
|
+
// ToolExecutionComponent chooses its child shell in the constructor. Keep that
|
|
1344
|
+
// choice stable for this row so toggling ccstyle cannot switch containers later.
|
|
1345
|
+
component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
|
|
1346
|
+
return useCcstyle;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function shouldUseSelfShell(component: any, patch: GlobalToolRenderPatch): boolean {
|
|
1350
|
+
const selectedMode = component[COMPONENT_TOOL_SELF_SHELL_MODE];
|
|
1351
|
+
if (typeof selectedMode === "boolean") return selectedMode;
|
|
1352
|
+
|
|
1353
|
+
const definition = component.toolDefinition ?? component.builtInToolDefinition;
|
|
1354
|
+
const toolName = String(component.toolName || definition?.name || "");
|
|
1355
|
+
const useSelfShell =
|
|
1356
|
+
patch.enabled() &&
|
|
1357
|
+
preservesOriginalRenderer(definition, toolName) &&
|
|
1358
|
+
definition?.renderShell !== "self";
|
|
1359
|
+
component[COMPONENT_TOOL_SELF_SHELL_MODE] = useSelfShell;
|
|
1360
|
+
return useSelfShell;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): any {
|
|
1364
|
+
const definition = component.toolDefinition ?? component.builtInToolDefinition;
|
|
1365
|
+
if (definition && typeof definition === "object") {
|
|
1366
|
+
let wrapped = patch.byDefinition.get(definition);
|
|
1367
|
+
if (!wrapped) {
|
|
1368
|
+
wrapped = patch.wrap(definition);
|
|
1369
|
+
patch.byDefinition.set(definition, wrapped);
|
|
1370
|
+
}
|
|
1371
|
+
return wrapped;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const name = String(component.toolName || "tool");
|
|
1375
|
+
let wrapped = patch.byName.get(name);
|
|
1376
|
+
if (!wrapped) {
|
|
1377
|
+
wrapped = patch.wrap({ name, label: name });
|
|
1378
|
+
patch.byName.set(name, wrapped);
|
|
1379
|
+
}
|
|
1380
|
+
return wrapped;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function installGlobalToolRendering() {
|
|
1384
|
+
const prototype = (ToolExecutionComponent as any).prototype;
|
|
1385
|
+
const host = globalThis as any;
|
|
1386
|
+
let patch = host[GLOBAL_TOOL_RENDER_PATCH] as GlobalToolRenderPatch | undefined;
|
|
1387
|
+
|
|
1388
|
+
if (!patch || patch.prototype !== prototype) {
|
|
1389
|
+
patch = {
|
|
1390
|
+
prototype,
|
|
1391
|
+
owner: globalToolRenderOwner,
|
|
1392
|
+
enabled: () => config.enabled,
|
|
1393
|
+
wrap: createCcstyleTool,
|
|
1394
|
+
byDefinition: new WeakMap(),
|
|
1395
|
+
byName: new Map(),
|
|
1396
|
+
originalHasRendererDefinition: prototype.hasRendererDefinition,
|
|
1397
|
+
originalGetRenderShell: prototype.getRenderShell,
|
|
1398
|
+
originalGetCallRenderer: prototype.getCallRenderer,
|
|
1399
|
+
originalGetResultRenderer: prototype.getResultRenderer,
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
host[GLOBAL_TOOL_RENDER_PATCH] = patch;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
patch.owner = globalToolRenderOwner;
|
|
1406
|
+
patch.enabled = () => config.enabled;
|
|
1407
|
+
patch.wrap = createCcstyleTool;
|
|
1408
|
+
patch.byDefinition = new WeakMap();
|
|
1409
|
+
patch.byName.clear();
|
|
1410
|
+
|
|
1411
|
+
// Rebind on every load. /reload preserves the shared prototype and global
|
|
1412
|
+
// patch object, so leaving these closures installed only in the initialization
|
|
1413
|
+
// branch would keep the previous extension version's rendering logic alive.
|
|
1414
|
+
prototype.hasRendererDefinition = function (...args: any[]) {
|
|
1415
|
+
if (shouldGloballyStyleTool(this, patch!)) return true;
|
|
1416
|
+
return patch!.originalHasRendererDefinition.apply(this, args);
|
|
1417
|
+
};
|
|
1418
|
+
prototype.getRenderShell = function (...args: any[]) {
|
|
1419
|
+
if (shouldUseSelfShell(this, patch!) || shouldGloballyStyleTool(this, patch!)) return "self";
|
|
1420
|
+
return patch!.originalGetRenderShell.apply(this, args);
|
|
1421
|
+
};
|
|
1422
|
+
prototype.getCallRenderer = function (...args: any[]) {
|
|
1423
|
+
if (shouldGloballyStyleTool(this, patch!)) return getGloballyStyledTool(this, patch!).renderCall;
|
|
1424
|
+
return patch!.originalGetCallRenderer.apply(this, args);
|
|
1425
|
+
};
|
|
1426
|
+
prototype.getResultRenderer = function (...args: any[]) {
|
|
1427
|
+
if (shouldGloballyStyleTool(this, patch!)) return getGloballyStyledTool(this, patch!).renderResult;
|
|
1428
|
+
return patch!.originalGetResultRenderer.apply(this, args);
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function deactivateGlobalToolRendering() {
|
|
1433
|
+
const patch = (globalThis as any)[GLOBAL_TOOL_RENDER_PATCH] as GlobalToolRenderPatch | undefined;
|
|
1434
|
+
if (patch?.owner !== globalToolRenderOwner) return;
|
|
1435
|
+
patch.enabled = () => false;
|
|
1436
|
+
patch.byDefinition = new WeakMap();
|
|
1437
|
+
patch.byName.clear();
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
|
|
1441
|
+
|
|
1442
|
+
type LegacyCompactionRenderPatch = {
|
|
1443
|
+
enabled?: () => boolean;
|
|
1444
|
+
};
|
|
1445
|
+
|
|
1446
|
+
/** Disable the pre-native compaction monkey patch left alive by /reload. */
|
|
1447
|
+
function deactivateLegacyCompactionRendering() {
|
|
1448
|
+
const patch = (globalThis as any)[GLOBAL_COMPACTION_RENDER_PATCH] as LegacyCompactionRenderPatch | undefined;
|
|
1449
|
+
if (patch) patch.enabled = () => false;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function registerWrappedTool(pi: ExtensionAPI, name: keyof ReturnType<typeof createBuiltInTools>, renderer: {
|
|
1453
|
+
call: (args: any, theme: any) => string;
|
|
1454
|
+
result: (result: any, options: any, theme: any, context: any) => string;
|
|
1455
|
+
diff?: (args: any, theme: any, context: any) => string;
|
|
1456
|
+
/** Use pi's built-in call/result renderers, including native diff panels. */
|
|
1457
|
+
defaultRenderer?: boolean;
|
|
1458
|
+
defaultResult?: boolean;
|
|
1459
|
+
diffAfterResult?: boolean;
|
|
1460
|
+
}) {
|
|
1461
|
+
WRAPPED_BUILTINS.add(name);
|
|
1462
|
+
const initial = getBuiltInTools(process.cwd())[name] as any;
|
|
1463
|
+
|
|
1464
|
+
pi.registerTool({
|
|
1465
|
+
// Spread the full definition instead of copying a subset. In particular,
|
|
1466
|
+
// edit requires prepareArguments and tools may declare executionMode.
|
|
1467
|
+
...initial,
|
|
1468
|
+
name,
|
|
1469
|
+
label: initial.label ?? name,
|
|
1470
|
+
renderShell: "self",
|
|
1471
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1472
|
+
const tool = (getBuiltInTools(ctx.cwd)[name] as any);
|
|
1473
|
+
if (config.enabled && (name === "edit" || name === "write")) {
|
|
1474
|
+
// Await the old-file snapshot before mutation to avoid races, while
|
|
1475
|
+
// keeping all filesystem and diff work out of the TUI renderer.
|
|
1476
|
+
diffPreviewByCall.set(toolCallId, await buildFileDiffPreview(name, params, ctx.cwd));
|
|
1477
|
+
}
|
|
1478
|
+
let result: any;
|
|
1479
|
+
try {
|
|
1480
|
+
result = await tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
diffPreviewByCall.delete(toolCallId);
|
|
1483
|
+
throw error;
|
|
1484
|
+
}
|
|
1485
|
+
if (name === "edit" && typeof result?.details?.diff === "string") {
|
|
1486
|
+
const actualDiff = result.details.diff as string;
|
|
1487
|
+
diffPreviewByCall.set(toolCallId,
|
|
1488
|
+
Buffer.byteLength(actualDiff, "utf8") <= MAX_DIFF_INPUT_BYTES
|
|
1489
|
+
? diffPreview(actualDiff)
|
|
1490
|
+
: { diff: "", notice: "Diff omitted: result exceeds 2 MiB" },
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
return result;
|
|
1494
|
+
},
|
|
1495
|
+
renderCall(args, theme, context) {
|
|
1496
|
+
const tool = (getBuiltInTools(context.cwd)[name] as any);
|
|
1497
|
+
if (!config.enabled || renderer.defaultRenderer) {
|
|
1498
|
+
return renderDefault(tool, "renderCall", [args, theme, context], String(name));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const toolName = String(name);
|
|
1502
|
+
const visualState = resolveToolVisualState(context);
|
|
1503
|
+
const isPending = visualState === "pending" || (!visualState && (context?.isPartial || context?.executionStarted));
|
|
1504
|
+
if (isPending) scheduleAnimation(context);
|
|
1505
|
+
const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
|
|
1506
|
+
const icon = visualState === "success"
|
|
1507
|
+
? `${BRIGHT_GREEN}${rawIcon}${ANSI_RESET}`
|
|
1508
|
+
: theme.fg(toolIconColor(context), rawIcon);
|
|
1509
|
+
const title = `${icon} ${theme.fg("toolTitle", renderer.call(args, theme))}`;
|
|
1510
|
+
// Prime the per-tool snapshot, but keep the call row stable while arguments
|
|
1511
|
+
// stream in. The diff is rendered only after the result is finalized below.
|
|
1512
|
+
if (renderer.diffAfterResult) renderer.diff?.(args, theme, context);
|
|
1513
|
+
return new Text(title, 0, 0);
|
|
1514
|
+
},
|
|
1515
|
+
renderResult(result, options, theme, context) {
|
|
1516
|
+
const tool = (getBuiltInTools(context.cwd)[name] as any);
|
|
1517
|
+
if (!config.enabled || renderer.defaultResult) {
|
|
1518
|
+
diffPreviewByCall.delete(context.toolCallId);
|
|
1519
|
+
return renderDefault(tool, "renderResult", [result, options, theme, context], textFromResult(result));
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
if (options?.isPartial) {
|
|
1523
|
+
return new Text(theme.fg("muted", " ⎿ Pending…"), 0, 0);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const isError = options?.isError || context?.isError;
|
|
1527
|
+
if (isError) {
|
|
1528
|
+
diffPreviewByCall.delete(context.toolCallId);
|
|
1529
|
+
if (context.state) context.state.ccstyleDiffPreview = undefined;
|
|
1530
|
+
}
|
|
1531
|
+
setToolVisualState(context, isError ? "error" : "success");
|
|
1532
|
+
const expanded = isToolExpanded(options, context);
|
|
1533
|
+
// Hydrate the cached preview before building the summary so edit uses
|
|
1534
|
+
// the authoritative result.details.diff rather than its estimate.
|
|
1535
|
+
const diff = !isError && renderer.diffAfterResult
|
|
1536
|
+
? renderer.diff?.(context.args, theme, context)
|
|
1537
|
+
: "";
|
|
1538
|
+
const rendered = renderer.result(result, { ...options, expanded }, theme, context);
|
|
1539
|
+
if (!rendered) return new Text("", 0, 0);
|
|
1540
|
+
const fullText = textFromResult(result);
|
|
1541
|
+
const preview = context?.state?.ccstyleDiffPreview as FileDiffPreview | undefined;
|
|
1542
|
+
const hint = !expanded && (hasExpandableResult(fullText) || hasExpandableDiff(preview))
|
|
1543
|
+
? expandHint(theme)
|
|
1544
|
+
: "";
|
|
1545
|
+
const body = `${rendered}${diff ? `\n${diff}` : ""}`;
|
|
1546
|
+
if (expanded) return renderExpandedToolResult(body, theme, isError, context?.lastComponent);
|
|
1547
|
+
const output = ` ⎿ ${rendered}${hint}${diff ? `\n${diff}` : ""}`;
|
|
1548
|
+
return new Text(
|
|
1549
|
+
theme.fg(isError ? "error" : "muted", output),
|
|
1550
|
+
0,
|
|
1551
|
+
0,
|
|
1552
|
+
);
|
|
1553
|
+
},
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function notePendingScrollMessage(role: unknown): void {
|
|
1558
|
+
if (!toolMouseTui || !isFixedEditorTui(toolMouseTui) || !scrollButtonVisible) return;
|
|
1559
|
+
if (role === "assistant") {
|
|
1560
|
+
if (assistantMessageActive) return;
|
|
1561
|
+
assistantMessageActive = true;
|
|
1562
|
+
} else if (role !== "toolResult") {
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
pendingScrollMessages += 1;
|
|
1566
|
+
toolMouseTui.requestRender?.();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
export default function (pi: ExtensionAPI) {
|
|
1570
|
+
installGlobalToolRendering();
|
|
1571
|
+
deactivateLegacyCompactionRendering();
|
|
1572
|
+
|
|
1573
|
+
pi.registerCommand("ccstyle", {
|
|
1574
|
+
description: "Configure Claude Code style",
|
|
1575
|
+
getArgumentCompletions: (prefix) => {
|
|
1576
|
+
const topLevel = [
|
|
1577
|
+
{ value: "on", label: "on", description: "Enable Claude Code style" },
|
|
1578
|
+
{ value: "off", label: "off", description: "Disable Claude Code style" },
|
|
1579
|
+
{ value: "status", label: "status", description: "Show current state" },
|
|
1580
|
+
];
|
|
1581
|
+
return topLevel.filter((item) => item.value.startsWith(prefix));
|
|
1582
|
+
},
|
|
1583
|
+
handler: async (args, ctx) => {
|
|
1584
|
+
const arg = args.trim().toLowerCase();
|
|
1585
|
+
if (!arg) {
|
|
1586
|
+
await showCcstylePanel(ctx);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (arg === "on" || arg === "off") {
|
|
1590
|
+
applyEnabledState(arg === "on", ctx);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (arg === "status") {
|
|
1594
|
+
ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
ctx.ui.notify("Usage: /ccstyle [on|off|status]", "warning");
|
|
1598
|
+
},
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
pi.registerShortcut("ctrl+shift+o", {
|
|
1602
|
+
description: "Toggle Claude Code-like output style",
|
|
1603
|
+
handler: async (ctx) => {
|
|
1604
|
+
config.enabled = !config.enabled;
|
|
1605
|
+
saveConfig();
|
|
1606
|
+
ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
|
|
1607
|
+
},
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1611
|
+
diffPreviewByCall.clear();
|
|
1612
|
+
pendingScrollMessages = 0;
|
|
1613
|
+
assistantMessageActive = false;
|
|
1614
|
+
ctx.ui.setStatus("ccstyle", undefined);
|
|
1615
|
+
installToolMouseInteraction(ctx);
|
|
1616
|
+
scheduleSessionRender();
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
pi.on("message_start", async (event) => {
|
|
1620
|
+
notePendingScrollMessage(event?.message?.role);
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
pi.on("message_update", async (event) => {
|
|
1624
|
+
if (event?.message?.role === "assistant") notePendingScrollMessage("assistant");
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
pi.on("message_end", async (event) => {
|
|
1628
|
+
if (event?.message?.role === "assistant") assistantMessageActive = false;
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
pi.on("session_shutdown", async () => {
|
|
1632
|
+
teardownToolMouseInteraction();
|
|
1633
|
+
deactivateGlobalToolRendering();
|
|
1634
|
+
deactivateLegacyCompactionRendering();
|
|
1635
|
+
clearAllAnimations();
|
|
1636
|
+
diffPreviewByCall.clear();
|
|
1637
|
+
toolCache.clear();
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
registerWrappedTool(pi, "read", {
|
|
1641
|
+
call: (args) => fileAction(args, "Read"),
|
|
1642
|
+
result: (result, { expanded }) => {
|
|
1643
|
+
const text = textFromResult(result);
|
|
1644
|
+
if (!expanded) return `${countLines(text)} lines read`;
|
|
1645
|
+
return text;
|
|
1646
|
+
},
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
registerWrappedTool(pi, "bash", {
|
|
1650
|
+
call: (args) => `Bash(${oneLine(args?.command, 86) || "..."})`,
|
|
1651
|
+
result: (result, { expanded }) => {
|
|
1652
|
+
const text = textFromResult(result).trim();
|
|
1653
|
+
if (!expanded) return summarizeBash(text);
|
|
1654
|
+
return text;
|
|
1655
|
+
},
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
registerWrappedTool(pi, "edit", {
|
|
1659
|
+
call: (args) => fileAction(args, "Edit"),
|
|
1660
|
+
diff: (args, theme, context) => renderFileDiffPreview("edit", args, theme, context),
|
|
1661
|
+
diffAfterResult: true,
|
|
1662
|
+
result: (result, { expanded }, _theme, context) => {
|
|
1663
|
+
const text = textFromResult(result).trim();
|
|
1664
|
+
return summarizeDiffStats(context?.state?.ccstyleDiffPreview) ?? summarizeEdit(text);
|
|
1665
|
+
},
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
registerWrappedTool(pi, "write", {
|
|
1669
|
+
call: (args) => {
|
|
1670
|
+
const path = shortenPath(args?.path || "...");
|
|
1671
|
+
const lines = args?.content ? countLines(args.content) : 0;
|
|
1672
|
+
return `Write(${path}${lines ? `, ${lines} lines` : ""})`;
|
|
1673
|
+
},
|
|
1674
|
+
diff: (args, theme, context) => renderFileDiffPreview("write", args, theme, context),
|
|
1675
|
+
diffAfterResult: true,
|
|
1676
|
+
result: (result, { expanded }, _theme, context) => {
|
|
1677
|
+
const text = textFromResult(result).trim();
|
|
1678
|
+
return summarizeDiffStats(context?.state?.ccstyleDiffPreview) ?? (text ? oneLine(text, 96) : "Written");
|
|
1679
|
+
},
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
registerWrappedTool(pi, "find", {
|
|
1683
|
+
call: (args) => `Find(${args?.pattern || "..."}${args?.path ? ` in ${shortenPath(args.path)}` : ""})`,
|
|
1684
|
+
result: (result, { expanded }) => {
|
|
1685
|
+
const text = textFromResult(result).trim();
|
|
1686
|
+
if (!expanded) return `${countLines(text)} files`;
|
|
1687
|
+
return text;
|
|
1688
|
+
},
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
registerWrappedTool(pi, "grep", {
|
|
1692
|
+
call: (args) => `Grep(${args?.pattern ? `/${oneLine(args.pattern, 48)}/` : "..."})`,
|
|
1693
|
+
result: (result, { expanded }) => {
|
|
1694
|
+
const text = textFromResult(result).trim();
|
|
1695
|
+
if (!expanded) {
|
|
1696
|
+
const matches = countLines(text);
|
|
1697
|
+
const files = parsePathCount(text);
|
|
1698
|
+
return files ? `Found ${matches} matches in ${files} files` : `${matches} matches`;
|
|
1699
|
+
}
|
|
1700
|
+
return text;
|
|
1701
|
+
},
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
registerWrappedTool(pi, "ls", {
|
|
1705
|
+
call: (args) => `List(${shortenPath(args?.path || ".")})`,
|
|
1706
|
+
result: (result, { expanded }) => {
|
|
1707
|
+
const text = textFromResult(result).trim();
|
|
1708
|
+
if (!expanded) return `${countLines(text)} entries`;
|
|
1709
|
+
return text;
|
|
1710
|
+
},
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
}
|