pi-cc-extensions 0.8.64 → 0.8.66

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
@@ -40,7 +40,7 @@ Run `/reload` after installation.
40
40
  | Claude Code Output | Tool summaries, expand/collapse, rich edit/write diffs, and`on` / `compact` / `off` modes | `/ccstyle` |
41
41
  | Markdown enhancements | Mermaid art, admonitions, URL linking, and more | Automatic |
42
42
  | Fullscreen mode | Tool card/group click to expand, double-click to collapse, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
43
- | Settings panel | `Style / Diff / Thinking / Feature` tabs: startup header toggle and wheel step | `/ccstyle` |
43
+ | Settings panel | `Style / Diff / Thinking / UI / Feature` tabs | `/ccstyle` |
44
44
  | Context inspection | Usage breakdown and previews for the system prompt, tools, skills, and messages | `/context` |
45
45
  | Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
46
46
  | Theme | Bundled CC Dark and CC Light themes | `/theme` |
@@ -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">
@@ -40,7 +40,7 @@ pi install git:github.com/minuque/pi-cc-extensions
40
40
  | Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `compact` / `off` 三种模式 | `/ccstyle` |
41
41
  | Markdown 增强 | Mermaid 图、提示框、URL 链接化等 | 自动生效 |
42
42
  | Fullscreen mode | 工具卡/group 单击展开、双击收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
43
- | 配置面板 | `Style / Diff / Thinking / Feature` 四页签,含启动头开关与滚轮步进 | `/ccstyle` |
43
+ | 配置面板 | `Style / Diff / Thinking / UI / Feature` 五页签 | `/ccstyle` |
44
44
  | 上下文检查 | 查看上下文占用,并预览 System prompt、Tools、Skills 和消息内容 | `/context` |
45
45
  | Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
46
46
  | 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/theme` |
@@ -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
 
@@ -182,7 +182,7 @@ function buildNumberInputSubmenu(
182
182
 
183
183
  /** Section tabs for /ccstyle — matches Zentui-style "A / B / C" headers. */
184
184
  type CcstyleSection = {
185
- id: "style" | "editor" | "diff" | "thinking" | "feature";
185
+ id: "style" | "diff" | "thinking" | "ui" | "feature";
186
186
  label: string;
187
187
  items: any[];
188
188
  };
@@ -556,12 +556,15 @@ export async function showCcstylePanel(
556
556
  label: "Thinking",
557
557
  items: [thinkingTitleSetting, thinkingPreviewSetting, thinkingAnimationSetting],
558
558
  },
559
+ {
560
+ id: "ui",
561
+ label: "UI",
562
+ items: [startupHeaderSetting, scrollStepSetting],
563
+ },
559
564
  {
560
565
  id: "feature",
561
566
  label: "Feature",
562
567
  items: [
563
- startupHeaderSetting,
564
- scrollStepSetting,
565
568
  sessionReferenceToggle.setting,
566
569
  subagentAutocompleteToggle.setting,
567
570
  contextCommandToggle.setting,
@@ -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.66",
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",