pi-cc-extensions 0.8.64 → 0.8.65

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/README.en.md CHANGED
@@ -51,24 +51,38 @@ Run `/reload` after installation.
51
51
 
52
52
  ```js
53
53
  {
54
- "mode": "on", // on: Claude Code style; compact: one-line summaries; off: Pi native rendering
55
- "excludeRenderers": [], // exact tool names that keep their native renderer; Agent always keeps its dedicated renderer
56
- "diffViewMode": "auto", // diff layout: auto / split / unified
57
- "diffIndicatorMode": "bars", // diff change indicators: bars / classic / none
58
- "diffSplitMinWidth": 120, // min terminal width before auto layout uses side-by-side columns
59
- "editDiffCollapsedLines": 24, // Edit collapsed lines; beyond that shows the expand hint (Ctrl+O / click)
60
- "writeDiffCollapsedLines": 0, // write collapsed body lines; 0 = ↳ created • expand hint (stats stay on the title)
61
- "diffWordWrap": true, // whether long diff lines wrap (otherwise truncated)
62
- "expandedPreviewMaxLines": 40, // max body lines for expanded output/diff
63
- "useSummaryTitlesAsThinkingTitle": true, // use the latest provider summary as the active thinking title
64
- "previewLines": 3, // thinking preview lines; 0 hides the preview body
65
- "animationIntervalMs": 90, // thinking title animation interval in ms
66
- "showStartupHeader": true, // custom startup header (logo + tips) toggle
67
- "scrollStepLines": 3 // fullscreen mouse wheel scroll lines
54
+ "mode": "on", // on / compact / off
55
+ "excludeRenderers": [], // tools keeping the native renderer; Agent always keeps its dedicated renderer
56
+
57
+ // diff
58
+ "diffViewMode": "auto", // layout: auto / split / unified
59
+ "diffIndicatorMode": "bars", // change indicators: bars / classic / none
60
+ "diffSplitMinWidth": 120, // min terminal width for side-by-side columns
61
+ "editDiffCollapsedLines": 24, // Edit collapse lines; beyond that shows the expand hint
62
+ "writeDiffCollapsedLines": 0, // write collapse lines; 0 = creation summary only
63
+ "diffWordWrap": true, // wrap long diff lines
64
+ "expandedPreviewMaxLines": 40, // max lines for expanded bodies
65
+
66
+ // thinking
67
+ "useSummaryTitlesAsThinkingTitle": true, // use latest summary as thinking title
68
+ "previewLines": 3, // preview lines; 0 hides
69
+ "animationIntervalMs": 90, // title animation interval (ms)
70
+
71
+ // ui
72
+ "showStartupHeader": true, // startup header (logo + tips) toggle
73
+ "scrollStepLines": 3, // fullscreen wheel scroll step
74
+
75
+ // features
76
+ "enableSessionReference": true, // @ session references
77
+ "enableSubagentAutocomplete": true, // @ subagent completion and delegation hints
78
+ "enableContextCommand": true, // /context usage check
79
+ "enableAgentSummary": true, // per-turn tool summary
80
+ "enableWorkingMessage": true, // Working... bottom token/elapsed
81
+ "enableAliases": true // /clear, /exit aliases
68
82
  }
69
83
  ```
70
84
 
71
- > **Fullscreen clicks**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. Click a truncated Input/Output header to open the full preview. Double-click an expanded panel to collapse it. Single clicks on an expanded card still select text and open links.
85
+ > **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. Double-click an expanded panel to collapse it.
72
86
  >
73
87
  > **Tip**: set `markdown.mermaid` to `final` via `~/.pi/agent/settings.json` or the Mermaid diagrams option in `/settings`. Default `streaming` redraws per frame; `final` renders once at completion.
74
88
 
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="./assets/readme/hero.svg" width="100%" alt="pi-cc-extensions:Pi 终端效率扩展套件">
2
+ <img src="./assets/readme/hero.svg" width="100%">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -51,30 +51,38 @@ pi install git:github.com/minuque/pi-cc-extensions
51
51
 
52
52
  ```js
53
53
  {
54
- "mode": "on", // on:Claude Code 风格;compact:单行摘要;off:Pi 原生渲染
55
- "excludeRenderers": [], // 走原生渲染的工具名列表(精确匹配),Agent 始终保留专用渲染器
56
- "diffViewMode": "auto", // diff 布局:auto / split / unified
57
- "diffIndicatorMode": "bars", // diff 变更指示:bars / classic / none
58
- "diffSplitMinWidth": 120, // 自动布局下使用左右分栏的最小终端宽度
59
- "editDiffCollapsedLines": 24, // Edit 折叠行数;超出显示展开提示(Ctrl+O / 点击)
60
- "writeDiffCollapsedLines": 0, // write 折叠行数;0 = ↳ created • 展开提示(统计在标题)
61
- "diffWordWrap": true, // 长 diff 行是否换行(否则截断)
62
- "expandedPreviewMaxLines": 40, // 展开后输出/diff 正文的最大行数
63
- "useSummaryTitlesAsThinkingTitle": true, // 用最新 provider 摘要作为思考标题
64
- "previewLines": 3, // thinking 预览行数,0 隐藏预览正文
65
- "animationIntervalMs": 90, // thinking 标题动画间隔(毫秒)
66
- "showStartupHeader": true, // 自定义启动头(logo + tips)开关
67
- "scrollStepLines": 3, // fullscreen 滚轮滚动步进行数
68
- "enableSessionReference": true, // @ session 引用(搜历史会话注入上下文)
69
- "enableSubagentAutocomplete": true, // @ subagent 自动补全与委派提示
70
- "enableContextCommand": true, // /context 上下文占用检查
71
- "enableAgentSummary": true, // 每回合工具统计摘要
72
- "enableWorkingMessage": true, // Working... 底部 token/耗时展示
73
- "enableAliases": true // /clear、/exit 别名
54
+ "mode": "on", // on / compact / off
55
+ "excludeRenderers": [], // 走原生渲染的工具名;Agent 始终保留专用渲染器
56
+
57
+ // diff
58
+ "diffViewMode": "auto", // 布局:auto / split / unified
59
+ "diffIndicatorMode": "bars", // 变更指示:bars / classic / none
60
+ "diffSplitMinWidth": 120, // 左右分栏的最小终端宽度
61
+ "editDiffCollapsedLines": 24, // Edit 折叠行数,超出显示展开提示
62
+ "writeDiffCollapsedLines": 0, // write 折叠行数,0 仅显示创建摘要
63
+ "diffWordWrap": true, // 长 diff 行换行
64
+ "expandedPreviewMaxLines": 40, // 展开正文最大行数
65
+
66
+ // thinking
67
+ "useSummaryTitlesAsThinkingTitle": true, // 用最新摘要作思考标题
68
+ "previewLines": 3, // 预览行数,0 隐藏
69
+ "animationIntervalMs": 90, // 标题动画间隔(毫秒)
70
+
71
+ // ui
72
+ "showStartupHeader": true, // 启动头(logo + tips)开关
73
+ "scrollStepLines": 3, // fullscreen 滚轮步进
74
+
75
+ // features
76
+ "enableSessionReference": true, // @ session 引用
77
+ "enableSubagentAutocomplete": true, // @ subagent 补全与委派提示
78
+ "enableContextCommand": true, // /context 上下文检查
79
+ "enableAgentSummary": true, // 每回合工具摘要
80
+ "enableWorkingMessage": true, // Working... 底部 token/耗时
81
+ "enableAliases": true // /clear、/exit 别名
74
82
  }
75
83
  ```
76
84
 
77
- > **Fullscreen 点击**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要。单击截断的 Input/Output 打开全量预览。双击展开面板收起。展开卡上的单击仍可选择文本、打开链接。
85
+ > **Fullscreen**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要,双击展开面板收起。
78
86
  >
79
87
  > **建议**:`markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
80
88
 
@@ -88,7 +96,7 @@ npm run typecheck
88
96
 
89
97
  ## 兼容性
90
98
 
91
- - Node.js `>=22.19.0`,Pi `^0.84.0`(通过根目录 `package.json` 的 `pi.extensions` 和 `pi.themes` 加载)
99
+ - Node.js `>=22.19.0`,Pi `^0.84.0`
92
100
 
93
101
  ## 推荐搭配
94
102
 
@@ -13,10 +13,10 @@ import { padLine } from "../utils/format.ts";
13
13
  export type ContextPart = {
14
14
  label: string;
15
15
  tokens: number;
16
- color: "accent" | "success" | "warning" | "customMessageLabel" | "muted" | "dim";
16
+ color: "accent" | "success" | "warning" | "customMessageLabel" | "muted" | "dim" | "error";
17
17
  };
18
18
 
19
- type PreviewKey = "systemPrompt" | "tools" | "toolResults" | "contextFiles";
19
+ type PreviewKey = "systemPrompt" | "memoryFiles" | "tools" | "toolResults" | "contextFiles";
20
20
 
21
21
  type ContextPreview = {
22
22
  key: PreviewKey;
@@ -47,11 +47,16 @@ export function escCloseHitbox(bounds: DialogBounds): {
47
47
  };
48
48
  }
49
49
 
50
- let activeTextPreviews = 0;
50
+ let activeContextOverlays = 0;
51
51
 
52
- /** fullscreen 输入包装用于把鼠标事件继续传给当前文本预览 overlay。 */
52
+ /** fullscreen 输入包装用于把鼠标事件继续传给当前 context 主弹框或文本预览 overlay。 */
53
53
  export function hasActiveTextPreview(): boolean {
54
- return activeTextPreviews > 0;
54
+ return activeContextOverlays > 0;
55
+ }
56
+
57
+ /** 官方 fullscreen 打开 overlay 时会退回 1002;重新启用 1003 才能收到无按键 hover。 */
58
+ function ensureFullscreenMouseMotion(tui: any): void {
59
+ if (tui.mode === "fullscreen") tui.terminal?.write?.("\x1b[?1003h\x1b[?1006h");
55
60
  }
56
61
 
57
62
  export async function showTextPreview(
@@ -60,10 +65,11 @@ export async function showTextPreview(
60
65
  rawContent: string,
61
66
  ): Promise<void> {
62
67
  const content = normalizePreviewText(rawContent);
63
- activeTextPreviews++;
68
+ activeContextOverlays++;
64
69
  try {
65
70
  await ctx.ui.custom(
66
71
  (tui, theme, _keybindings, done) => {
72
+ ensureFullscreenMouseMotion(tui);
67
73
  let scrollOffset = 0;
68
74
  let pageSize = 1;
69
75
  let totalLines = 1;
@@ -249,7 +255,7 @@ export async function showTextPreview(
249
255
  },
250
256
  );
251
257
  } finally {
252
- activeTextPreviews--;
258
+ activeContextOverlays--;
253
259
  }
254
260
  }
255
261
 
@@ -337,6 +343,13 @@ export function collectContextBreakdown(
337
343
  let toolResultTokens = 0;
338
344
  let contextTokens = 0;
339
345
 
346
+ const memoryPreview: string[] = [];
347
+ let memoryTokens = 0;
348
+ for (const file of options.contextFiles ?? []) {
349
+ memoryTokens += tokenEstimate(file.content);
350
+ memoryPreview.push(`## ${file.path}\n\n${previewValue(file.content)}`);
351
+ }
352
+
340
353
  for (const tool of allTools) {
341
354
  if (!selectedTools.has(tool.name)) continue;
342
355
  const definition = {
@@ -397,12 +410,14 @@ export function collectContextBreakdown(
397
410
  return {
398
411
  parts: [
399
412
  { label: "System prompt", tokens: tokenEstimate(systemPrompt), color: "accent" },
413
+ { label: "Memory", tokens: memoryTokens, color: "error" },
400
414
  { label: "Tools", tokens: toolDefinitionTokens, color: "success" },
401
415
  { label: "Tool results", tokens: toolResultTokens, color: "customMessageLabel" },
402
416
  { label: "Context", tokens: contextTokens, color: "warning" },
403
417
  ] satisfies ContextPart[],
404
418
  previews: {
405
419
  systemPrompt: systemPrompt || "No system prompt.",
420
+ memoryFiles: memoryPreview.join("\n\n") || "No memory files in context.",
406
421
  tools: toolDefinitionPreview.join("\n\n") || "No active tool definitions.",
407
422
  toolResults: toolResultPreview.join("\n\n") || "No tool results in the current context.",
408
423
  contextFiles: contextPreview.join("\n\n") || "No conversation context.",
@@ -419,9 +434,10 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
419
434
  const tools = pi.getAllTools();
420
435
  const breakdown = collectContextBreakdown(ctx, tools);
421
436
  const estimated = breakdown.parts.reduce((sum, part) => sum + part.tokens, 0);
422
- const fixedTokens = breakdown.parts.slice(0, 2).reduce((sum, part) => sum + part.tokens, 0);
437
+ // System prompt、Memory、Tools 为固定项,capParts 时保持原值不压缩。
438
+ const fixedTokens = breakdown.parts.slice(0, 3).reduce((sum, part) => sum + part.tokens, 0);
423
439
  const used = Math.max(resolveUsedTokens(usage, estimated, contextWindow), fixedTokens);
424
- const parts = capParts(breakdown.parts, used, 2);
440
+ const parts = capParts(breakdown.parts, used, 3);
425
441
  const attributed = parts.reduce((sum, part) => sum + part.tokens, 0);
426
442
  const other = Math.max(0, used - attributed);
427
443
  const free = Math.max(0, contextWindow - used);
@@ -444,6 +460,12 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
444
460
  title: "System Prompt",
445
461
  content: breakdown.previews.systemPrompt,
446
462
  },
463
+ {
464
+ key: "memoryFiles",
465
+ label: "Memory",
466
+ title: "Memory Files",
467
+ content: breakdown.previews.memoryFiles,
468
+ },
447
469
  {
448
470
  key: "tools",
449
471
  label: "Tools",
@@ -474,158 +496,187 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
474
496
  let selectedPreviewIndex = 0;
475
497
 
476
498
  while (true) {
477
- const action = await ctx.ui.custom(
478
- (tui, theme, _keybindings, done) => {
479
- let previewHitboxes: Array<{
480
- key: PreviewKey;
481
- row: number;
482
- startCol: number;
483
- endCol: number;
484
- }> = [];
485
- let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
486
-
487
- return {
488
- invalidate() {},
489
- handleInput(data: string) {
490
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
491
- done(undefined);
492
- return;
493
- }
494
- if (matchesKey(data, Key.up) && visiblePreviews.length > 0) {
495
- selectedPreviewIndex =
496
- (selectedPreviewIndex - 1 + visiblePreviews.length) % visiblePreviews.length;
497
- tui.requestRender();
498
- return;
499
- }
500
- if (matchesKey(data, Key.down) && visiblePreviews.length > 0) {
501
- selectedPreviewIndex = (selectedPreviewIndex + 1) % visiblePreviews.length;
502
- tui.requestRender();
503
- return;
504
- }
505
- if (matchesKey(data, Key.enter)) {
506
- done(visiblePreviews[selectedPreviewIndex]?.key);
507
- return;
508
- }
509
-
510
- const mouse = parseSgrMousePacket(data);
511
- if (
512
- mouse?.final !== "M" ||
513
- mouseBaseButton(mouse.code) !== 0 ||
514
- (mouse.code & 32) !== 0
515
- )
516
- return;
517
- if (
518
- escHitbox &&
519
- mouse.row === escHitbox.row &&
520
- mouse.col >= escHitbox.startCol &&
521
- mouse.col <= escHitbox.endCol
522
- ) {
523
- done(undefined);
524
- return;
525
- }
526
- const hitbox = previewHitboxes.find(
527
- (candidate) =>
528
- mouse.row === candidate.row &&
529
- mouse.col >= candidate.startCol &&
530
- mouse.col <= candidate.endCol,
531
- );
532
- if (hitbox) {
533
- selectedPreviewIndex = Math.max(
534
- 0,
535
- visiblePreviews.findIndex((preview) => preview.key === hitbox.key),
499
+ // 主弹框也计入活动 overlay,fullscreen 下官方输入链才会把鼠标包放行给行点击。
500
+ activeContextOverlays++;
501
+ let action;
502
+ try {
503
+ action = await ctx.ui.custom(
504
+ (tui, theme, _keybindings, done) => {
505
+ ensureFullscreenMouseMotion(tui);
506
+ let previewHitboxes: Array<{
507
+ key: PreviewKey;
508
+ row: number;
509
+ startCol: number;
510
+ endCol: number;
511
+ }> = [];
512
+ let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
513
+ let escHovered = false;
514
+ let hoveredKey: PreviewKey | undefined;
515
+
516
+ return {
517
+ invalidate() {},
518
+ handleInput(data: string) {
519
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
520
+ done(undefined);
521
+ return;
522
+ }
523
+ if (matchesKey(data, Key.up) && visiblePreviews.length > 0) {
524
+ selectedPreviewIndex =
525
+ (selectedPreviewIndex - 1 + visiblePreviews.length) % visiblePreviews.length;
526
+ tui.requestRender();
527
+ return;
528
+ }
529
+ if (matchesKey(data, Key.down) && visiblePreviews.length > 0) {
530
+ selectedPreviewIndex = (selectedPreviewIndex + 1) % visiblePreviews.length;
531
+ tui.requestRender();
532
+ return;
533
+ }
534
+ if (matchesKey(data, Key.enter)) {
535
+ done(visiblePreviews[selectedPreviewIndex]?.key);
536
+ return;
537
+ }
538
+
539
+ const mouse = parseSgrMousePacket(data);
540
+ if (!mouse || mouse.final !== "M") return;
541
+ if ((mouse.code & 32) !== 0) {
542
+ // SGR 1003 的无按键 hover code 为 35,不能按左键事件过滤。
543
+ const overEsc = Boolean(
544
+ escHitbox &&
545
+ mouse.row === escHitbox.row &&
546
+ mouse.col >= escHitbox.startCol &&
547
+ mouse.col <= escHitbox.endCol,
548
+ );
549
+ const hovered = previewHitboxes.find(
550
+ (candidate) =>
551
+ mouse.row === candidate.row &&
552
+ mouse.col >= candidate.startCol &&
553
+ mouse.col <= candidate.endCol,
554
+ );
555
+ if (overEsc !== escHovered || hovered?.key !== hoveredKey) {
556
+ escHovered = overEsc;
557
+ hoveredKey = hovered?.key;
558
+ tui.requestRender();
559
+ }
560
+ return;
561
+ }
562
+ if (mouseBaseButton(mouse.code) !== 0) return;
563
+ if (
564
+ escHitbox &&
565
+ mouse.row === escHitbox.row &&
566
+ mouse.col >= escHitbox.startCol &&
567
+ mouse.col <= escHitbox.endCol
568
+ ) {
569
+ done(undefined);
570
+ return;
571
+ }
572
+ const hitbox = previewHitboxes.find(
573
+ (candidate) =>
574
+ mouse.row === candidate.row &&
575
+ mouse.col >= candidate.startCol &&
576
+ mouse.col <= candidate.endCol,
536
577
  );
537
- done(hitbox.key);
538
- }
539
- },
540
- render(width: number) {
541
- const inner = Math.max(1, width - 2);
542
- const escWidth = visibleWidth("[esc]");
543
- const percent = contextWindow > 0 ? (used / contextWindow) * 100 : 0;
544
- const title = theme.bold(theme.fg("accent", "Context Usage"));
545
- const subtitle = `${formatTokens(used)} / ${formatTokens(contextWindow)} tokens (${percent.toFixed(1)}%)`;
546
- const barWidth = Math.max(1, Math.min(60, inner - 2));
547
- let remaining = barWidth;
548
- const segments = allParts
549
- .map((part, index) => {
550
- const cells =
551
- index === allParts.length - 1
552
- ? remaining
553
- : Math.min(
554
- remaining,
555
- Math.round((part.tokens / Math.max(1, contextWindow)) * barWidth),
556
- );
557
- remaining -= cells;
558
- return theme.fg(part.color, "█".repeat(Math.max(0, cells)));
559
- })
560
- .join("");
561
- const labelWidth = Math.min(
562
- 24,
563
- Math.max(...allParts.map((part) => part.label.length)),
564
- );
565
- const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
566
- const partRows = allParts.map((part) => {
567
- const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
568
- const swatch = theme.fg(part.color, "■");
569
- const label = part.label.padEnd(labelWidth);
570
- const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
571
- const selected = part.label === selectedLabel;
572
- const prefix = selected ? "› " : " ";
573
- const row = padLine(`${prefix}${swatch} ${label} ${amount}`, inner);
574
- return selected ? theme.bg("selectedBg", row) : row;
575
- });
576
- const border = (text: string) => theme.fg("border", text);
577
- const lines = [
578
- border(`╭${"─".repeat(inner)}╮`),
579
- `${border("│")}${padLine(` ${title} ${theme.fg("muted", subtitle)}`, inner - escWidth)}${theme.fg("muted", "[esc]")}${border("│")}`,
580
- `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
581
- `${border("│")}${padLine(` ${segments}`, inner)}${border("│")}`,
582
- `${border("│")}${" ".repeat(inner)}${border("│")}`,
583
- ...partRows.map((row) => `${border("│")}${row}${border("│")}`),
584
- `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
585
- `${border("│")}${padLine(theme.fg("dim", " ↑↓ select · Click / Enter to preview · [esc] close"), inner)}${border("│")}`,
586
- border(`╰${"─".repeat(inner)}╯`),
587
- ];
588
-
589
- const terminalHeight = Math.max(1, tui.terminal.rows);
590
- const maxHeight = Math.min(
591
- Math.max(1, Math.floor(terminalHeight * 0.9)),
592
- Math.max(1, terminalHeight - 2),
593
- );
594
- const visibleHeight = Math.min(lines.length, maxHeight);
595
- const overlayTop =
596
- 1 + Math.floor((Math.max(1, terminalHeight - 2) - visibleHeight) / 2);
597
- const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
598
- escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
599
- previewHitboxes = visiblePreviews.flatMap((preview) => {
600
- const partIndex = allParts.findIndex((part) => part.label === preview.label);
601
- const line = 5 + partIndex;
602
- return partIndex >= 0 && line < visibleHeight
603
- ? [
604
- {
605
- key: preview.key,
606
- row: overlayTop + line + 1,
607
- startCol: overlayLeft + 1,
608
- endCol: overlayLeft + width,
609
- },
610
- ]
611
- : [];
612
- });
613
-
614
- return lines;
578
+ if (hitbox) {
579
+ selectedPreviewIndex = Math.max(
580
+ 0,
581
+ visiblePreviews.findIndex((preview) => preview.key === hitbox.key),
582
+ );
583
+ done(hitbox.key);
584
+ }
585
+ },
586
+ render(width: number) {
587
+ const inner = Math.max(1, width - 2);
588
+ const escWidth = visibleWidth("[esc]");
589
+ const percent = contextWindow > 0 ? (used / contextWindow) * 100 : 0;
590
+ const title = theme.bold(theme.fg("accent", "Context Usage"));
591
+ const subtitle = `${formatTokens(used)} / ${formatTokens(contextWindow)} tokens (${percent.toFixed(1)}%)`;
592
+ const barWidth = Math.max(1, Math.min(60, inner - 2));
593
+ let remaining = barWidth;
594
+ const segments = allParts
595
+ .map((part, index) => {
596
+ const cells =
597
+ index === allParts.length - 1
598
+ ? remaining
599
+ : Math.min(
600
+ remaining,
601
+ Math.round((part.tokens / Math.max(1, contextWindow)) * barWidth),
602
+ );
603
+ remaining -= cells;
604
+ return theme.fg(part.color, "█".repeat(Math.max(0, cells)));
605
+ })
606
+ .join("");
607
+ const labelWidth = Math.min(
608
+ 24,
609
+ Math.max(...allParts.map((part) => part.label.length)),
610
+ );
611
+ const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
612
+ const partRows = allParts.map((part) => {
613
+ const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
614
+ const swatch = theme.fg(part.color, "■");
615
+ const label = part.label.padEnd(labelWidth);
616
+ const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
617
+ const selected = part.label === selectedLabel;
618
+ const hoverLabel = visiblePreviews.find((p) => p.key === hoveredKey)?.label;
619
+ const prefix = selected ? "› " : " ";
620
+ const row = padLine(`${prefix}${swatch} ${label} ${amount}`, inner);
621
+ if (selected) return theme.bg("selectedBg", row);
622
+ return part.label === hoverLabel ? theme.bg("customMessageBg", row) : row;
623
+ });
624
+ const border = (text: string) => theme.fg("border", text);
625
+ const lines = [
626
+ border(`╭${"─".repeat(inner)}╮`),
627
+ `${border("│")}${padLine(` ${title} ${theme.fg("muted", subtitle)}`, inner - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
628
+ `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
629
+ `${border("│")}${padLine(` ${segments}`, inner)}${border("│")}`,
630
+ `${border("│")}${" ".repeat(inner)}${border("│")}`,
631
+ ...partRows.map((row) => `${border("│")}${row}${border("│")}`),
632
+ `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
633
+ `${border("│")}${padLine(theme.fg("dim", " ↑↓ select · Click / Enter to preview · [esc] close"), inner)}${border("│")}`,
634
+ border(`╰${"─".repeat(inner)}╯`),
635
+ ];
636
+
637
+ const terminalHeight = Math.max(1, tui.terminal.rows);
638
+ const maxHeight = Math.min(
639
+ Math.max(1, Math.floor(terminalHeight * 0.9)),
640
+ Math.max(1, terminalHeight - 2),
641
+ );
642
+ const visibleHeight = Math.min(lines.length, maxHeight);
643
+ const overlayTop =
644
+ 1 + Math.floor((Math.max(1, terminalHeight - 2) - visibleHeight) / 2);
645
+ const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
646
+ escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
647
+ previewHitboxes = visiblePreviews.flatMap((preview) => {
648
+ const partIndex = allParts.findIndex((part) => part.label === preview.label);
649
+ const line = 5 + partIndex;
650
+ return partIndex >= 0 && line < visibleHeight
651
+ ? [
652
+ {
653
+ key: preview.key,
654
+ row: overlayTop + line + 1,
655
+ startCol: overlayLeft + 1,
656
+ endCol: overlayLeft + width,
657
+ },
658
+ ]
659
+ : [];
660
+ });
661
+
662
+ return lines;
663
+ },
664
+ };
665
+ },
666
+ {
667
+ overlay: true,
668
+ overlayOptions: {
669
+ anchor: "center",
670
+ width: 64,
671
+ minWidth: 44,
672
+ maxHeight: "90%",
673
+ margin: 1,
615
674
  },
616
- };
617
- },
618
- {
619
- overlay: true,
620
- overlayOptions: {
621
- anchor: "center",
622
- width: 64,
623
- minWidth: 44,
624
- maxHeight: "90%",
625
- margin: 1,
626
675
  },
627
- },
628
- );
676
+ );
677
+ } finally {
678
+ activeContextOverlays--;
679
+ }
629
680
 
630
681
  if (!action) break;
631
682
  const preview = previewByKey.get(action as PreviewKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.64",
3
+ "version": "0.8.65",
4
4
  "license": "MIT",
5
5
  "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",