pi-cc-extensions 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/claude-code-style.ts +189 -0
- package/package.json +2 -3
|
@@ -301,6 +301,193 @@ 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
|
+
for (const packet of packets) {
|
|
450
|
+
if (isSgrLeftPress(packet)) {
|
|
451
|
+
toggleToolAtMouseClick(toolMouseTui, packet);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// The default editor cannot interpret mouse escape sequences. Consume them
|
|
455
|
+
// even when the click was outside a tool result.
|
|
456
|
+
return { consume: true };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function teardownToolMouseInteraction(): void {
|
|
460
|
+
toolMouseInputUnsubscribe?.();
|
|
461
|
+
toolMouseInputUnsubscribe = null;
|
|
462
|
+
try {
|
|
463
|
+
toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
464
|
+
} catch {
|
|
465
|
+
// The terminal may already be closed during shutdown.
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
toolMouseUi?.setWidget?.(TOOL_MOUSE_WIDGET_KEY, undefined);
|
|
469
|
+
} catch {
|
|
470
|
+
// The UI context may already have been reset during /reload.
|
|
471
|
+
}
|
|
472
|
+
toolMouseTui = null;
|
|
473
|
+
toolMouseUi = null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function installToolMouseInteraction(ctx: any): void {
|
|
477
|
+
teardownToolMouseInteraction();
|
|
478
|
+
if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
|
|
479
|
+
if (typeof ctx.ui?.onTerminalInput !== "function" || typeof ctx.ui?.setWidget !== "function") return;
|
|
480
|
+
|
|
481
|
+
toolMouseUi = ctx.ui;
|
|
482
|
+
ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any) => {
|
|
483
|
+
toolMouseTui = tui;
|
|
484
|
+
tui?.terminal?.write?.(TOOL_MOUSE_ENABLE);
|
|
485
|
+
// Keep the widget visually empty so Pi's default input bar/layout remains.
|
|
486
|
+
return { render: () => [], invalidate() {} };
|
|
487
|
+
});
|
|
488
|
+
toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
|
|
489
|
+
}
|
|
490
|
+
|
|
304
491
|
// Bright green for success icon (truecolor ANSI escape)
|
|
305
492
|
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
306
493
|
const ANSI_RESET = "\x1b[0m";
|
|
@@ -895,9 +1082,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
895
1082
|
pi.on("session_start", async (_event, ctx) => {
|
|
896
1083
|
diffPreviewByCall.clear();
|
|
897
1084
|
updateStatus(ctx);
|
|
1085
|
+
installToolMouseInteraction(ctx);
|
|
898
1086
|
});
|
|
899
1087
|
|
|
900
1088
|
pi.on("session_shutdown", async () => {
|
|
1089
|
+
teardownToolMouseInteraction();
|
|
901
1090
|
deactivateGlobalToolRendering();
|
|
902
1091
|
deactivateLegacyCompactionRendering();
|
|
903
1092
|
clearAllAnimations();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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": "*",
|