pi-cc-extensions 0.8.20 → 0.8.22

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 { DynamicBorder, type Theme } from "@earendil-works/pi-coding-agent";
2
- import { renderDialogHeader, renderDialogTopBorder } from "../../closable-dialog.ts";
3
2
  import { type Component, Container, type Input, Spacer } from "@earendil-works/pi-tui";
4
3
  import type { QuestionnaireState } from "../state/state.js";
5
4
  import type { QuestionData } from "../tool/types.js";
@@ -134,25 +133,10 @@ export class DialogView implements StatefulView<DialogProps> {
134
133
  // Render only the active tab's natural content; blank height equalization would
135
134
  // unnecessarily consume transcript rows above this full-width editor component.
136
135
  const natural = this.buildContainerFromStrategy(strategy, headingRowCache).render(width);
137
- if (natural.length > 0) {
138
- natural[0] = renderDialogTopBorder(width, (text) => this.config.theme.fg("accent", text));
139
- natural.splice(
140
- 1,
141
- 0,
142
- renderDialogHeader(
143
- " Questions",
144
- width,
145
- (text) => this.config.theme.fg("accent", text),
146
- (text) => this.config.theme.fg("toolTitle", text),
147
- (text) => this.config.theme.fg("muted", text),
148
- false,
149
- ),
150
- );
151
- }
152
136
 
153
137
  // Fixed region sizes (deterministic from structure).
154
138
  // TabBar.render() returns [tabLine, ""] — always 2 rows.
155
- const topFixed = 2 + (this.config.isMulti && this.config.tabBar ? 2 : 0) + 1;
139
+ const topFixed = 1 + (this.config.isMulti && this.config.tabBar ? 2 : 0) + 1;
156
140
  const bottomFixed = 1 + strategy.footerRowCount;
157
141
  const middleRows = natural.length - topFixed - bottomFixed;
158
142
 
@@ -29,12 +29,6 @@ import {
29
29
  wrapTextWithAnsi,
30
30
  } from "@earendil-works/pi-tui";
31
31
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
32
- import {
33
- bottomDialogBounds,
34
- isDialogCloseClick,
35
- renderDialogHeader,
36
- renderDialogTopBorder,
37
- } from "./closable-dialog.ts";
38
32
  import { homedir } from "node:os";
39
33
  import { join } from "node:path";
40
34
 
@@ -794,7 +788,6 @@ type ToolRenderHit = {
794
788
 
795
789
  const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
796
790
  const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
797
- const TOOL_MOUSE_HOVER_ENABLE = "\x1b[?1003h\x1b[?1006h";
798
791
  const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
799
792
  const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
800
793
  const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
@@ -817,7 +810,6 @@ let pendingScrollMessages = 0;
817
810
  let assistantMessageActive = false;
818
811
  let scrollButtonSyncScheduled = false;
819
812
  let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
820
- let hoveredToolSummaryRow: number | null = null;
821
813
  let hoveredToolCallId: string | null = null;
822
814
 
823
815
  function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
@@ -1448,19 +1440,10 @@ function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRender
1448
1440
 
1449
1441
  function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1450
1442
  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;
1443
+ const hit = findToolAtScreenRow(tui, packet.row);
1444
+ const nextToolCallId = hit && !hit.component.expanded ? hit.component.toolCallId : null;
1445
+ if (nextToolCallId === hoveredToolCallId) return;
1461
1446
  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
1447
  tui.requestRender?.();
1465
1448
  }
1466
1449
 
@@ -1612,7 +1595,6 @@ function teardownToolMouseInteraction(): void {
1612
1595
  }
1613
1596
  toolMouseInputUnsubscribe?.();
1614
1597
  toolMouseInputUnsubscribe = null;
1615
- hoveredToolSummaryRow = null;
1616
1598
  hoveredToolCallId = null;
1617
1599
  try {
1618
1600
  toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
@@ -1653,7 +1635,7 @@ export function installToolMouseInteraction(
1653
1635
  toolMouseTui = tui;
1654
1636
  if (fixedEditorFeatures) patchToolMouseInputCapture(tui);
1655
1637
  // 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);
1638
+ tui?.terminal?.write?.(fixedEditorFeatures ? TOOL_MOUSE_ENABLE : "\x1b[?1003h\x1b[?1006h");
1657
1639
  const widget = {
1658
1640
  render: (width: number) => renderScrollButton(width, theme),
1659
1641
  invalidate() {},
@@ -2004,8 +1986,6 @@ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Prom
2004
1986
  ];
2005
1987
 
2006
1988
  let activeSection = 0;
2007
- let lastWidth = 0;
2008
- let lastHeight = 0;
2009
1989
  const settingsTheme = getSettingsListTheme();
2010
1990
  const lists = sections.map(
2011
1991
  (section) =>
@@ -2042,15 +2022,8 @@ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Prom
2042
2022
  while (listBody.length > 0 && listBody[listBody.length - 1] === "") listBody.pop();
2043
2023
 
2044
2024
  // Frame: top rule · tabs · mid rule · settings · mid rule · footer · bottom rule
2045
- const lines = [
2046
- renderDialogTopBorder(safeWidth, (text) => theme.fg("border", text)),
2047
- renderDialogHeader(
2048
- " CC Style",
2049
- safeWidth,
2050
- (text) => theme.fg("border", text),
2051
- (text) => theme.fg("toolTitle", text),
2052
- (text) => theme.fg("muted", text),
2053
- ),
2025
+ return [
2026
+ rule,
2054
2027
  renderSectionTabBar(theme, sections, activeSection, safeWidth),
2055
2028
  rule,
2056
2029
  ...listBody,
@@ -2064,22 +2037,11 @@ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Prom
2064
2037
  ),
2065
2038
  rule,
2066
2039
  ];
2067
- lastWidth = safeWidth;
2068
- lastHeight = lines.length;
2069
- return lines;
2070
2040
  },
2071
2041
  invalidate() {
2072
2042
  for (const list of lists) list.invalidate();
2073
2043
  },
2074
2044
  handleInput(data: string) {
2075
- if (
2076
- !excludeSubmenuOpen &&
2077
- tui.terminal &&
2078
- isDialogCloseClick(data, bottomDialogBounds(tui.terminal, lastWidth, lastHeight))
2079
- ) {
2080
- done();
2081
- return;
2082
- }
2083
2045
  if (!excludeSubmenuOpen && isForwardTabKey(data)) {
2084
2046
  switchSection(1);
2085
2047
  tui.requestRender();
@@ -2130,15 +2092,22 @@ function toolCallArgument(args: any): { key: string; value: string } | undefined
2130
2092
  : undefined;
2131
2093
  }
2132
2094
 
2133
- function tailToWidth(text: string, width: number): string {
2095
+ export function middleTruncateToWidth(text: string, width: number): string {
2134
2096
  if (visibleWidth(text) <= width) return text;
2135
2097
  if (width <= 1) return "…";
2136
- let tail = "";
2137
- for (const char of Array.from(text).reverse()) {
2138
- if (visibleWidth(`…${char}${tail}`) > width) break;
2139
- tail = char + tail;
2098
+ const chars = Array.from(text);
2099
+ const leftWidth = Math.ceil((width - 1) / 2);
2100
+ let left = "";
2101
+ let right = "";
2102
+ for (const char of chars) {
2103
+ if (visibleWidth(left + char) > leftWidth) break;
2104
+ left += char;
2105
+ }
2106
+ for (const char of chars.reverse()) {
2107
+ if (visibleWidth(left + "…" + char + right) > width) break;
2108
+ right = char + right;
2140
2109
  }
2141
- return `…${tail}`;
2110
+ return `${left}…${right}`;
2142
2111
  }
2143
2112
 
2144
2113
  export function shouldRenderRichDiff(
@@ -2182,11 +2151,7 @@ function createCcstyleTool(
2182
2151
  return {
2183
2152
  render(width: number) {
2184
2153
  const argumentWidth = Math.max(1, width - visibleWidth(`${rawIcon} ${label}()`));
2185
- const shown = argument
2186
- ? argument.key === "path"
2187
- ? tailToWidth(argument.value, argumentWidth)
2188
- : truncateToWidth(argument.value, argumentWidth, "…")
2189
- : undefined;
2154
+ const shown = argument ? middleTruncateToWidth(argument.value, argumentWidth) : undefined;
2190
2155
  const call = shown === undefined ? label : `${label}(${shown})`;
2191
2156
  // Leave one cell unused so the host shell never performs a second,
2192
2157
  // unthemed truncation after this renderer has applied toolTitle.
@@ -2244,8 +2209,7 @@ function createCcstyleTool(
2244
2209
  );
2245
2210
  }
2246
2211
  if (context?.state) context.state.ccstyleIoView = undefined;
2247
- const toolCallId =
2248
- context?.toolCallId ?? context?.id ?? context?.state?.toolCallId ?? context?.state?.id;
2212
+ const toolCallId = context?.toolCallId;
2249
2213
  const summary = theme.fg(
2250
2214
  isError ? "error" : "muted",
2251
2215
  renderCollapsedToolResult(rendered, hint),
@@ -2254,8 +2218,10 @@ function createCcstyleTool(
2254
2218
  render(width: number) {
2255
2219
  const line = truncateToWidth(summary, width, "…");
2256
2220
  if (!toolCallId || hoveredToolCallId !== toolCallId) return [line];
2257
- const padding = " ".repeat(Math.max(0, width - visibleWidth(line)));
2258
- return [theme.bg("selectedBg", `${line}${padding}`)];
2221
+ const highlighted = line.replace(/\x1b\[0m/g, "\x1b[0;7m");
2222
+ return [
2223
+ `\x1b[7m${highlighted}${" ".repeat(Math.max(0, width - visibleWidth(line)))}\x1b[0m`,
2224
+ ];
2259
2225
  },
2260
2226
  invalidate() {},
2261
2227
  };
@@ -10,12 +10,6 @@ import {
10
10
  visibleWidth,
11
11
  wrapTextWithAnsi,
12
12
  } from "@earendil-works/pi-tui";
13
- import {
14
- centeredDialogBounds,
15
- isDialogCloseClick,
16
- renderDialogHeader,
17
- renderDialogTopBorder,
18
- } from "./closable-dialog.ts";
19
13
 
20
14
  export type ContextPart = {
21
15
  label: string;
@@ -71,8 +65,6 @@ export async function showTextPreview(
71
65
  let scrollOffset = 0;
72
66
  let pageSize = 1;
73
67
  let totalLines = 1;
74
- let lastWidth = 0;
75
- let lastHeight = 0;
76
68
  const wrappedContent = createWrappedTextCache(content);
77
69
 
78
70
  const scrollTo = (nextOffset: number): void => {
@@ -85,10 +77,6 @@ export async function showTextPreview(
85
77
  wrappedContent.invalidate();
86
78
  },
87
79
  handleInput(data: string) {
88
- if (isDialogCloseClick(data, centeredDialogBounds(tui.terminal, lastWidth, lastHeight))) {
89
- done(undefined);
90
- return;
91
- }
92
80
  if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
93
81
  done(undefined);
94
82
  return;
@@ -150,24 +138,15 @@ export async function showTextPreview(
150
138
  const end = Math.min(totalLines, scrollOffset + pageSize);
151
139
  const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · Esc close`;
152
140
 
153
- const lines = [
154
- renderDialogTopBorder(width, border),
155
- renderDialogHeader(
156
- ` ${title}`,
157
- width,
158
- border,
159
- (text) => theme.bold(theme.fg("accent", text)),
160
- (text) => theme.fg("muted", text),
161
- ),
141
+ return [
142
+ border(`╭${"─".repeat(inner)}╮`),
143
+ `${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`)}${border("│")}`,
162
144
  `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
163
145
  ...bodyRows,
164
146
  `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
165
147
  `${border("│")}${padLine(theme.fg("dim", ` ${status}`))}${border("│")}`,
166
148
  border(`╰${"─".repeat(inner)}╯`),
167
149
  ];
168
- lastWidth = width;
169
- lastHeight = lines.length;
170
- return lines;
171
150
  },
172
151
  };
173
152
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.20",
3
+ "version": "0.8.22",
4
4
  "description": "A Pi productivity suite with Claude Code-style UI, structured questionnaires, context inspection, and agent/session references",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,60 +0,0 @@
1
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
-
3
- export type DialogBounds = { left: number; top: number; width: number };
4
-
5
- export function renderDialogTopBorder(width: number, color: (text: string) => string): string {
6
- const safeWidth = Math.max(4, Math.floor(width));
7
- return color(`╭${"─".repeat(safeWidth - 2)}╮`);
8
- }
9
-
10
- /** Render title and a larger [ × ] target on the row below the top border. */
11
- export function renderDialogHeader(
12
- title: string,
13
- width: number,
14
- color: (text: string) => string,
15
- titleColor: (text: string) => string,
16
- closeColor: (text: string) => string,
17
- showClose = true,
18
- ): string {
19
- const safeWidth = Math.max(8, Math.floor(width));
20
- const innerWidth = safeWidth - 2;
21
- const button = showClose ? "[ × ]" : "";
22
- const titleWidth = Math.max(0, innerWidth - visibleWidth(button) - (showClose ? 2 : 0));
23
- const shownTitle = truncateToWidth(title, titleWidth, "…");
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("│")}`;
28
- }
29
-
30
- /** Match a left-button press anywhere on the [ × ] target. */
31
- export function isDialogCloseClick(data: string, bounds: DialogBounds): boolean {
32
- const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
33
- if (!match || match[4] !== "M") return false;
34
- const code = Number(match[1]);
35
- if ((code & 3) !== 0 || (code & 32) !== 0) return false;
36
- const col = Number(match[2]);
37
- const firstButtonCol = bounds.left + bounds.width - 7;
38
- const lastButtonCol = bounds.left + bounds.width - 3;
39
- return col >= firstButtonCol && col <= lastButtonCol && Number(match[3]) === bounds.top + 1;
40
- }
41
-
42
- export function centeredDialogBounds(
43
- terminal: { columns: number; rows: number },
44
- width: number,
45
- height: number,
46
- ): DialogBounds {
47
- return {
48
- left: Math.floor((terminal.columns - width) / 2) + 1,
49
- top: Math.floor((terminal.rows - height) / 2) + 1,
50
- width,
51
- };
52
- }
53
-
54
- export function bottomDialogBounds(
55
- terminal: { rows: number },
56
- width: number,
57
- height: number,
58
- ): DialogBounds {
59
- return { left: 1, top: Math.max(1, terminal.rows - height + 1), width };
60
- }