pi-cc-extensions 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/claude-code-style.ts +191 -0
- package/package.json +2 -3
|
@@ -301,6 +301,195 @@ function expandHint(theme: any): string {
|
|
|
301
301
|
return `${theme.fg("muted", " (")}${keyHint("app.tools.expand", "expand")}${theme.fg("muted", " / click)")}`;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
type SgrMousePacket = {
|
|
305
|
+
code: number;
|
|
306
|
+
col: number;
|
|
307
|
+
row: number;
|
|
308
|
+
final: "M" | "m";
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
type ToolRenderHit = {
|
|
312
|
+
component: any;
|
|
313
|
+
start: number;
|
|
314
|
+
end: number;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
|
|
318
|
+
const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h";
|
|
319
|
+
const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1000l";
|
|
320
|
+
let toolMouseTui: any = null;
|
|
321
|
+
let toolMouseUi: any = null;
|
|
322
|
+
let toolMouseInputUnsubscribe: (() => void) | null = null;
|
|
323
|
+
|
|
324
|
+
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
325
|
+
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
326
|
+
const packets: SgrMousePacket[] = [];
|
|
327
|
+
let offset = 0;
|
|
328
|
+
|
|
329
|
+
for (const match of data.matchAll(pattern)) {
|
|
330
|
+
if (match.index !== offset) return null;
|
|
331
|
+
offset = match.index + match[0].length;
|
|
332
|
+
packets.push({
|
|
333
|
+
code: Number(match[1]),
|
|
334
|
+
col: Number(match[2]),
|
|
335
|
+
row: Number(match[3]),
|
|
336
|
+
final: match[4] as "M" | "m",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return packets.length > 0 && offset === data.length ? packets : null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isSgrLeftPress(packet: SgrMousePacket): boolean {
|
|
344
|
+
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
345
|
+
return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function stripTerminalSequences(value: string): string {
|
|
349
|
+
return value
|
|
350
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
351
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
352
|
+
.replace(/\s+/g, " ")
|
|
353
|
+
.trim();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function isToolExecutionComponent(value: any): boolean {
|
|
357
|
+
return Boolean(
|
|
358
|
+
value
|
|
359
|
+
&& typeof value === "object"
|
|
360
|
+
&& typeof value.toolCallId === "string"
|
|
361
|
+
&& typeof value.setExpanded === "function"
|
|
362
|
+
&& typeof value.render === "function",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderedLineCount(component: any, width: number, cache: WeakMap<object, number>): number {
|
|
367
|
+
if (!component || typeof component !== "object") return 0;
|
|
368
|
+
const cached = cache.get(component);
|
|
369
|
+
if (cached !== undefined) return cached;
|
|
370
|
+
|
|
371
|
+
let count = 0;
|
|
372
|
+
try {
|
|
373
|
+
const lines = component.render(width);
|
|
374
|
+
count = Array.isArray(lines) ? lines.length : 0;
|
|
375
|
+
} catch {
|
|
376
|
+
count = 0;
|
|
377
|
+
}
|
|
378
|
+
cache.set(component, count);
|
|
379
|
+
return count;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function collectToolRenderHits(
|
|
383
|
+
component: any,
|
|
384
|
+
start: number,
|
|
385
|
+
width: number,
|
|
386
|
+
hits: ToolRenderHit[],
|
|
387
|
+
cache: WeakMap<object, number>,
|
|
388
|
+
seen: Set<object>,
|
|
389
|
+
): number {
|
|
390
|
+
if (!component || typeof component !== "object" || seen.has(component)) return start;
|
|
391
|
+
seen.add(component);
|
|
392
|
+
|
|
393
|
+
const count = renderedLineCount(component, width, cache);
|
|
394
|
+
if (isToolExecutionComponent(component)) {
|
|
395
|
+
if (count > 0) hits.push({ component, start, end: start + count });
|
|
396
|
+
return start + count;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let childStart = start;
|
|
400
|
+
if (Array.isArray(component.children)) {
|
|
401
|
+
for (const child of component.children) {
|
|
402
|
+
collectToolRenderHits(child, childStart, width, hits, cache, seen);
|
|
403
|
+
childStart += renderedLineCount(child, width, cache);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return start + count;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function findToolAtScreenRow(tui: any, screenRow: number): ToolRenderHit | null {
|
|
410
|
+
const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
411
|
+
const viewportTop = Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0;
|
|
412
|
+
const bufferRow = viewportTop + screenRow - 1;
|
|
413
|
+
if (bufferRow < 0 || bufferRow >= previousLines.length) return null;
|
|
414
|
+
|
|
415
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
416
|
+
const hits: ToolRenderHit[] = [];
|
|
417
|
+
const cache = new WeakMap<object, number>();
|
|
418
|
+
collectToolRenderHits(tui, 0, width, hits, cache, new Set<object>());
|
|
419
|
+
|
|
420
|
+
const clickedLine = stripTerminalSequences(String(previousLines[bufferRow] ?? ""));
|
|
421
|
+
for (const hit of hits) {
|
|
422
|
+
if (bufferRow < hit.start || bufferRow >= hit.end) continue;
|
|
423
|
+
const component = hit.component;
|
|
424
|
+
const expanded = Boolean(component.expanded);
|
|
425
|
+
// Match Pi's old click contract: collapsed results expose a click hint;
|
|
426
|
+
// expanded results can be collapsed by clicking any rendered line.
|
|
427
|
+
if (!expanded && !/(?:expand|\/ click)/i.test(clickedLine)) continue;
|
|
428
|
+
return hit;
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
434
|
+
const hit = findToolAtScreenRow(tui, packet.row);
|
|
435
|
+
if (!hit) return false;
|
|
436
|
+
|
|
437
|
+
const nextExpanded = !Boolean(hit.component.expanded);
|
|
438
|
+
hit.component.setExpanded(nextExpanded);
|
|
439
|
+
hit.component.invalidate?.();
|
|
440
|
+
tui.requestRender?.();
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
445
|
+
if (!toolMouseTui) return undefined;
|
|
446
|
+
const packets = parseSgrMousePackets(data);
|
|
447
|
+
if (!packets) return undefined;
|
|
448
|
+
|
|
449
|
+
let consumed = false;
|
|
450
|
+
for (const packet of packets) {
|
|
451
|
+
if (isSgrLeftPress(packet) && toggleToolAtMouseClick(toolMouseTui, packet)) {
|
|
452
|
+
consumed = true;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Let scrolling, motion, release, and clicks outside tool results reach the
|
|
457
|
+
// normal TUI input chain (including other extensions such as pi-zentui).
|
|
458
|
+
return consumed ? { consume: true } : undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function teardownToolMouseInteraction(): void {
|
|
462
|
+
toolMouseInputUnsubscribe?.();
|
|
463
|
+
toolMouseInputUnsubscribe = null;
|
|
464
|
+
try {
|
|
465
|
+
toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
466
|
+
} catch {
|
|
467
|
+
// The terminal may already be closed during shutdown.
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
toolMouseUi?.setWidget?.(TOOL_MOUSE_WIDGET_KEY, undefined);
|
|
471
|
+
} catch {
|
|
472
|
+
// The UI context may already have been reset during /reload.
|
|
473
|
+
}
|
|
474
|
+
toolMouseTui = null;
|
|
475
|
+
toolMouseUi = null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function installToolMouseInteraction(ctx: any): void {
|
|
479
|
+
teardownToolMouseInteraction();
|
|
480
|
+
if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
|
|
481
|
+
if (typeof ctx.ui?.onTerminalInput !== "function" || typeof ctx.ui?.setWidget !== "function") return;
|
|
482
|
+
|
|
483
|
+
toolMouseUi = ctx.ui;
|
|
484
|
+
ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any) => {
|
|
485
|
+
toolMouseTui = tui;
|
|
486
|
+
tui?.terminal?.write?.(TOOL_MOUSE_ENABLE);
|
|
487
|
+
// Keep the widget visually empty so Pi's default input bar/layout remains.
|
|
488
|
+
return { render: () => [], invalidate() {} };
|
|
489
|
+
});
|
|
490
|
+
toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
|
|
491
|
+
}
|
|
492
|
+
|
|
304
493
|
// Bright green for success icon (truecolor ANSI escape)
|
|
305
494
|
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
306
495
|
const ANSI_RESET = "\x1b[0m";
|
|
@@ -895,9 +1084,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
895
1084
|
pi.on("session_start", async (_event, ctx) => {
|
|
896
1085
|
diffPreviewByCall.clear();
|
|
897
1086
|
updateStatus(ctx);
|
|
1087
|
+
installToolMouseInteraction(ctx);
|
|
898
1088
|
});
|
|
899
1089
|
|
|
900
1090
|
pi.on("session_shutdown", async () => {
|
|
1091
|
+
teardownToolMouseInteraction();
|
|
901
1092
|
deactivateGlobalToolRendering();
|
|
902
1093
|
deactivateLegacyCompactionRendering();
|
|
903
1094
|
clearAllAnimations();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Personal extensions for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
"preversion": "npm run typecheck && npm test",
|
|
27
27
|
"release": "npm version patch && npm publish",
|
|
28
28
|
"release:minor": "npm version minor && npm publish",
|
|
29
|
-
"release:major": "npm version major && npm publish"
|
|
30
|
-
"postpublish": "git push && git push --tags"
|
|
29
|
+
"release:major": "npm version major && npm publish"
|
|
31
30
|
},
|
|
32
31
|
"peerDependencies": {
|
|
33
32
|
"@earendil-works/pi-ai": "*",
|