pi-cc-extensions 0.8.18 → 0.8.20

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.
@@ -1,5 +1,4 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { bottomDialogBounds, isDialogCloseClick } from "../../closable-dialog.ts";
3
2
  import { getKeybindings, type Input } from "@earendil-works/pi-tui";
4
3
  import type { QuestionData, QuestionnaireResult, QuestionParams } from "../tool/types.js";
5
4
  import type { WrappingSelectItem } from "../view/components/wrapping-select.js";
@@ -94,26 +93,10 @@ export class QuestionnaireSession {
94
93
  // so the transcript regains space while Ctrl+] can still expand the questionnaire.
95
94
  const collapsedRender = (_width: number): string[] => [theme.fg("dim", ` ${COLLAPSED_HINT} `)];
96
95
 
97
- let lastWidth = 0;
98
- let lastHeight = 0;
99
96
  this.component = {
100
- render: (width) => {
101
- const lines = this.state.collapsed ? collapsedRender(width) : built.render(width);
102
- lastWidth = width;
103
- lastHeight = lines.length;
104
- return lines;
105
- },
97
+ render: (width) => (this.state.collapsed ? collapsedRender(width) : built.render(width)),
106
98
  invalidate: built.invalidate,
107
- handleInput: (data) => {
108
- if (
109
- !this.state.collapsed &&
110
- isDialogCloseClick(data, bottomDialogBounds(this.tui.terminal, lastWidth, lastHeight))
111
- ) {
112
- this.dispatch("\x1b");
113
- return;
114
- }
115
- this.dispatch(data);
116
- },
99
+ handleInput: (data) => this.dispatch(data),
117
100
  };
118
101
 
119
102
  this.viewAdapter.apply(this.state);
@@ -145,6 +145,7 @@ export class DialogView implements StatefulView<DialogProps> {
145
145
  (text) => this.config.theme.fg("accent", text),
146
146
  (text) => this.config.theme.fg("toolTitle", text),
147
147
  (text) => this.config.theme.fg("muted", text),
148
+ false,
148
149
  ),
149
150
  );
150
151
  }
@@ -793,8 +793,9 @@ type ToolRenderHit = {
793
793
  };
794
794
 
795
795
  const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
796
- const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h";
797
- const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1000l";
796
+ const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
797
+ const TOOL_MOUSE_HOVER_ENABLE = "\x1b[?1003h\x1b[?1006h";
798
+ const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
798
799
  const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
799
800
  const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
800
801
  const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
@@ -816,6 +817,8 @@ let pendingScrollMessages = 0;
816
817
  let assistantMessageActive = false;
817
818
  let scrollButtonSyncScheduled = false;
818
819
  let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
820
+ let hoveredToolSummaryRow: number | null = null;
821
+ let hoveredToolCallId: string | null = null;
819
822
 
820
823
  function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
821
824
  const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
@@ -938,8 +941,8 @@ function fixedEditorContextScore(
938
941
  return score;
939
942
  }
940
943
 
941
- /** Collapsed tool rows expose "expand / click"; expanded body clicks do not. */
942
- const COLLAPSED_TOOL_CLICK_HINT = /(?:expand|\/ click)/i;
944
+ /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
945
+ const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿)/;
943
946
  /** fixedEditorContextScore max ≈ 100 + 2*(5+4+3+2); stop once we hit a near-perfect match. */
944
947
  const TOOL_CLICK_EARLY_EXIT_SCORE = 120;
945
948
 
@@ -956,12 +959,11 @@ function findToolAtFixedEditorRow(
956
959
 
957
960
  const tools: any[] = [];
958
961
  collectToolComponents(tui, tools);
959
- // Expand-hint → only collapsed tools; body click → only expanded (collapse-any-line).
960
- // Skips re-rendering the opposite half of the transcript tools.
961
- const wantExpanded = !COLLAPSED_TOOL_CLICK_HINT.test(clickedLine);
962
+ const clickedCollapsedSummary = COLLAPSED_TOOL_SUMMARY.test(clickedLine);
962
963
  let best: { hit: ToolRenderHit; score: number } | null = null;
963
964
  for (const component of tools) {
964
- if (Boolean(component.expanded) !== wantExpanded) continue;
965
+ // Collapsed tools open only from their summary row; expanded tools collapse from any body row.
966
+ if (!component.expanded && !clickedCollapsedSummary) continue;
965
967
  const renderedLines = renderComponentTree(component, width).map((line) =>
966
968
  stripTerminalSequences(String(line)),
967
969
  );
@@ -998,12 +1000,11 @@ function findToolAtScreenRow(tui: any, screenRow: number): ToolRenderHit | null
998
1000
  collectToolRenderHits(tui, 0, width, hits, cache, new Set<object>());
999
1001
 
1000
1002
  const clickedLine = stripTerminalSequences(String(previousLines[bufferRow] ?? ""));
1001
- const wantExpanded = !COLLAPSED_TOOL_CLICK_HINT.test(clickedLine);
1002
1003
  for (const hit of hits) {
1003
1004
  if (bufferRow < hit.start || bufferRow >= hit.end) continue;
1004
- // Same分流 as fixed-editor: hint clicks expand collapsed rows; body clicks
1005
- // collapse expanded rows. Avoids accidental toggles on the wrong set.
1006
- if (Boolean(hit.component.expanded) !== wantExpanded) continue;
1005
+ // The stable summary marker keeps the whole row clickable even when the
1006
+ // trailing keyboard/click hint was truncated (common for subagent output).
1007
+ if (!hit.component.expanded && !COLLAPSED_TOOL_SUMMARY.test(clickedLine)) continue;
1007
1008
  return hit;
1008
1009
  }
1009
1010
  return null;
@@ -1445,6 +1446,24 @@ function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRender
1445
1446
  return true;
1446
1447
  }
1447
1448
 
1449
+ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1450
+ if ((packet.code & 32) === 0 || packet.final !== "M") return;
1451
+ const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
1452
+ const lineIndex = useFixedEditorFeatures(tui)
1453
+ ? packet.row - 1
1454
+ : (Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0) + packet.row - 1;
1455
+ const line = stripTerminalSequences(String(previousLines[lineIndex] ?? ""));
1456
+ const hit = COLLAPSED_TOOL_SUMMARY.test(line) ? findToolAtScreenRow(tui, packet.row) : null;
1457
+ const nextRow = hit && !hit.component.expanded ? packet.row : null;
1458
+ const nextToolCallId = nextRow ? (hit?.component?.toolCallId ?? null) : null;
1459
+ if (nextRow === hoveredToolSummaryRow && nextToolCallId === hoveredToolCallId) return;
1460
+ hoveredToolSummaryRow = nextRow;
1461
+ hoveredToolCallId = nextToolCallId;
1462
+ // Collapsed result components read hoveredToolCallId during render. Keeping
1463
+ // hover in the render path avoids an async TUI repaint overwriting raw ANSI.
1464
+ tui.requestRender?.();
1465
+ }
1466
+
1448
1467
  function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
1449
1468
  const hit = findToolAtScreenRow(tui, packet.row);
1450
1469
  if (!hit) return false;
@@ -1504,6 +1523,9 @@ function patchToolMouseInputCapture(tui: any): void {
1504
1523
  const packets = parseSgrMousePackets(data);
1505
1524
  if (packets) {
1506
1525
  for (const packet of packets) {
1526
+ // Fixed-editor owners may consume motion before extension listeners run,
1527
+ // so hover must be handled at the root input boundary too.
1528
+ updateToolSummaryHover(this, packet);
1507
1529
  if (!isSgrLeftPress(packet)) continue;
1508
1530
  if (handleScrollButtonClick(this, packet) || toggleToolAtMouseClick(this, packet)) return;
1509
1531
  }
@@ -1567,6 +1589,7 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
1567
1589
 
1568
1590
  let consumed = false;
1569
1591
  for (const packet of packets) {
1592
+ updateToolSummaryHover(toolMouseTui, packet);
1570
1593
  if (!isSgrLeftPress(packet)) continue;
1571
1594
  if (
1572
1595
  handleScrollButtonClick(toolMouseTui, packet) ||
@@ -1589,6 +1612,8 @@ function teardownToolMouseInteraction(): void {
1589
1612
  }
1590
1613
  toolMouseInputUnsubscribe?.();
1591
1614
  toolMouseInputUnsubscribe = null;
1615
+ hoveredToolSummaryRow = null;
1616
+ hoveredToolCallId = null;
1592
1617
  try {
1593
1618
  toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
1594
1619
  } catch {
@@ -1626,10 +1651,9 @@ export function installToolMouseInteraction(
1626
1651
  toolMouseFixedFeaturesEnabled = fixedEditorFeatures;
1627
1652
  ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
1628
1653
  toolMouseTui = tui;
1629
- if (fixedEditorFeatures) {
1630
- patchToolMouseInputCapture(tui);
1631
- tui?.terminal?.write?.(TOOL_MOUSE_ENABLE);
1632
- }
1654
+ if (fixedEditorFeatures) patchToolMouseInputCapture(tui);
1655
+ // Motion reporting is required for hover in both native and fixed-editor layouts.
1656
+ tui?.terminal?.write?.(fixedEditorFeatures ? TOOL_MOUSE_ENABLE : TOOL_MOUSE_HOVER_ENABLE);
1633
1657
  const widget = {
1634
1658
  render: (width: number) => renderScrollButton(width, theme),
1635
1659
  invalidate() {},
@@ -2164,7 +2188,9 @@ function createCcstyleTool(
2164
2188
  : truncateToWidth(argument.value, argumentWidth, "…")
2165
2189
  : undefined;
2166
2190
  const call = shown === undefined ? label : `${label}(${shown})`;
2167
- const callWidth = Math.max(0, width - visibleWidth(icon) - 1);
2191
+ // Leave one cell unused so the host shell never performs a second,
2192
+ // unthemed truncation after this renderer has applied toolTitle.
2193
+ const callWidth = Math.max(0, width - visibleWidth(icon) - 2);
2168
2194
  return [`${icon} ${theme.fg("toolTitle", truncateToWidth(call, callWidth, "…"))}`];
2169
2195
  },
2170
2196
  invalidate() {},
@@ -2218,9 +2244,21 @@ function createCcstyleTool(
2218
2244
  );
2219
2245
  }
2220
2246
  if (context?.state) context.state.ccstyleIoView = undefined;
2221
- return singleLine(
2222
- theme.fg(isError ? "error" : "muted", renderCollapsedToolResult(rendered, hint)),
2247
+ const toolCallId =
2248
+ context?.toolCallId ?? context?.id ?? context?.state?.toolCallId ?? context?.state?.id;
2249
+ const summary = theme.fg(
2250
+ isError ? "error" : "muted",
2251
+ renderCollapsedToolResult(rendered, hint),
2223
2252
  );
2253
+ return {
2254
+ render(width: number) {
2255
+ const line = truncateToWidth(summary, width, "…");
2256
+ if (!toolCallId || hoveredToolCallId !== toolCallId) return [line];
2257
+ const padding = " ".repeat(Math.max(0, width - visibleWidth(line)));
2258
+ return [theme.bg("selectedBg", `${line}${padding}`)];
2259
+ },
2260
+ invalidate() {},
2261
+ };
2224
2262
  },
2225
2263
  };
2226
2264
  }
@@ -14,14 +14,17 @@ export function renderDialogHeader(
14
14
  color: (text: string) => string,
15
15
  titleColor: (text: string) => string,
16
16
  closeColor: (text: string) => string,
17
+ showClose = true,
17
18
  ): string {
18
19
  const safeWidth = Math.max(8, Math.floor(width));
19
20
  const innerWidth = safeWidth - 2;
20
- const button = "[ × ]";
21
- const titleWidth = Math.max(0, innerWidth - visibleWidth(button) - 2);
21
+ const button = showClose ? "[ × ]" : "";
22
+ const titleWidth = Math.max(0, innerWidth - visibleWidth(button) - (showClose ? 2 : 0));
22
23
  const shownTitle = truncateToWidth(title, titleWidth, "…");
23
- const gap = " ".repeat(Math.max(1, innerWidth - visibleWidth(shownTitle) - visibleWidth(button)));
24
- return `${color("│")}${titleColor(shownTitle)}${gap}${closeColor(button)}${color("│")}`;
24
+ const gap = " ".repeat(
25
+ Math.max(showClose ? 1 : 0, innerWidth - visibleWidth(shownTitle) - visibleWidth(button)),
26
+ );
27
+ return `${color("│")}${titleColor(shownTitle)}${gap}${showClose ? closeColor(button) : ""}${color("│")}`;
25
28
  }
26
29
 
27
30
  /** Match a left-button press anywhere on the [ × ] target. */
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
- {
2
- "name": "pi-cc-extensions",
3
- "version": "0.8.18",
4
- "description": "A Pi productivity suite with Claude Code-style UI, structured questionnaires, context inspection, and agent/session references",
5
- "type": "module",
6
- "keywords": [
7
- "pi-package",
8
- "pi-extension",
9
- "pi-coding-agent",
10
- "claude-code",
11
- "questionnaire"
12
- ],
13
- "homepage": "https://github.com/minuque/pi-cc-extensions#readme",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/minuque/pi-cc-extensions.git"
17
- },
18
- "bugs": {
19
- "url": "https://github.com/minuque/pi-cc-extensions/issues"
20
- },
21
- "files": [
22
- "extensions",
23
- "assets/readme",
24
- "themes",
25
- "README.md",
26
- "README.en.md"
27
- ],
28
- "pi": {
29
- "extensions": [
30
- "./extensions/pi-aliases.ts",
31
- "./extensions/pi-startup-header.ts",
32
- "./extensions/claude-code-style.ts",
33
- "./extensions/context.ts",
34
- "./extensions/session-reference.ts",
35
- "./extensions/agent-autocomplete.ts",
36
- "./extensions/ask-user-question/index.ts"
37
- ],
38
- "themes": [
39
- "./themes/github-dark-default.json"
40
- ],
41
- "image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/welcome.webp"
42
- },
43
- "scripts": {
44
- "test": "node --test tests/**/*.test.ts",
45
- "typecheck": "node scripts/typecheck.mjs",
46
- "format": "biome format --write .",
47
- "format:check": "biome format .",
48
- "preversion": "npm run format:check && npm run typecheck && npm test",
49
- "release": "npm version patch && npm publish",
50
- "release:minor": "npm version minor && npm publish",
51
- "release:major": "npm version major && npm publish"
52
- },
53
- "dependencies": {
54
- "@juicesharp/rpiv-config": "^2.1.0",
55
- "typebox": "^1.1.24"
56
- },
57
- "peerDependencies": {
58
- "@earendil-works/pi-ai": "*",
59
- "@earendil-works/pi-coding-agent": "*",
60
- "@earendil-works/pi-tui": "*"
61
- },
62
- "engines": {
63
- "node": ">=22.19.0"
64
- },
65
- "devDependencies": {
66
- "@biomejs/biome": "2.5.5",
67
- "@types/node": "^26.1.1"
68
- }
69
- }
1
+ {
2
+ "name": "pi-cc-extensions",
3
+ "version": "0.8.20",
4
+ "description": "A Pi productivity suite with Claude Code-style UI, structured questionnaires, context inspection, and agent/session references",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "pi-coding-agent",
10
+ "claude-code",
11
+ "questionnaire"
12
+ ],
13
+ "homepage": "https://github.com/minuque/pi-cc-extensions#readme",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/minuque/pi-cc-extensions.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/minuque/pi-cc-extensions/issues"
20
+ },
21
+ "files": [
22
+ "extensions",
23
+ "assets/readme",
24
+ "themes",
25
+ "README.md",
26
+ "README.en.md"
27
+ ],
28
+ "pi": {
29
+ "extensions": [
30
+ "./extensions/pi-aliases.ts",
31
+ "./extensions/pi-startup-header.ts",
32
+ "./extensions/claude-code-style.ts",
33
+ "./extensions/context.ts",
34
+ "./extensions/session-reference.ts",
35
+ "./extensions/agent-autocomplete.ts",
36
+ "./extensions/ask-user-question/index.ts"
37
+ ],
38
+ "themes": [
39
+ "./themes/github-dark-default.json"
40
+ ],
41
+ "image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/welcome.webp"
42
+ },
43
+ "scripts": {
44
+ "test": "node --test tests/**/*.test.ts",
45
+ "typecheck": "node scripts/typecheck.mjs",
46
+ "format": "biome format --write .",
47
+ "format:check": "biome format .",
48
+ "preversion": "npm run format:check && npm run typecheck && npm test",
49
+ "release": "npm version patch && npm publish",
50
+ "release:minor": "npm version minor && npm publish",
51
+ "release:major": "npm version major && npm publish"
52
+ },
53
+ "dependencies": {
54
+ "@juicesharp/rpiv-config": "^2.1.0",
55
+ "typebox": "^1.1.24"
56
+ },
57
+ "peerDependencies": {
58
+ "@earendil-works/pi-ai": "*",
59
+ "@earendil-works/pi-coding-agent": "*",
60
+ "@earendil-works/pi-tui": "*"
61
+ },
62
+ "engines": {
63
+ "node": ">=22.19.0"
64
+ },
65
+ "devDependencies": {
66
+ "@biomejs/biome": "2.5.5",
67
+ "@types/node": "^26.1.1"
68
+ }
69
+ }