pi-cc-extensions 1.0.8 → 1.0.9

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  # pi-cc-extensions
6
6
 
7
- > Claude Code-style TUI output with some personal touches and handy utilities.
7
+ > Claude Code-style TUI output with some personal touches and handy utilities (no extra built-in tools).
8
8
 
9
9
  ## Preview
10
10
 
@@ -34,7 +34,7 @@ Run `/reload` after installation.
34
34
  | Context inspection | Usage breakdown and previews for the system prompt, memory, skills, tools definition, and messages | `/context` |
35
35
  | Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
36
36
  | Status bar | Shows model, context, cache, cost, and git; also works with `@narumitw/pi-usage` for live quota. Git/cache Nerd Font icons can be turned off | `/ccstyle` |
37
- | Theme | Bundled CC Dark and CC Light themes | `/theme` |
37
+ | Theme | Bundled CC Dark and CC Light themes | `/settings` → Theme |
38
38
 
39
39
  Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-examples-default.md) · [compact](./docs/tool-render-examples-compact.md)
40
40
 
@@ -55,7 +55,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
55
55
  "enableAgentSummary": true, // per-turn tool summary
56
56
  "enableWorkingMessage": true, // Working... bottom token/elapsed
57
57
  "enableAliases": true, // /clear, /exit aliases
58
- "enableMarkdownEnhance": true, // markdown pass: Mermaid dialects and circled-number conversion; false hands it to other extensions
58
+ "enableMarkdownEnhance": true, // markdown pass: Mermaid dialects and circled-number conversion; false hands it to other extensions
59
59
  "enableEditorPrefix": true, // editor ❯ prefix
60
60
 
61
61
  // ui
@@ -97,12 +97,10 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
97
97
  ```
98
98
 
99
99
  > [!TIP]
100
- > **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. An expanded diff always shows every line; when expanded Input/Output exceeds the line cap, the footer `… +N more lines · click to show more` opens a full preview. Click to collapse.
101
- >
102
- > Both `scrollButtonMode` values show the same `Back to bottom` / `N new message(s)` text: `dock` draws it in a dedicated row, `native` in Pi’s floating overlay.
100
+ > **Fullscreen**: expanded diffs always show every line. When expanded Input/Output exceeds the line cap, the footer shows `… +N more lines`; click for the full preview, click again to collapse.
103
101
 
104
102
  > [!NOTE]
105
- > **Mermaid rendering**: 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.
103
+ > **Mermaid rendering**: set `markdown.mermaid` to `final` via `~/.pi/agent/settings.json` or `/settings`. Default `streaming` redraws every frame; `final` renders once.
106
104
 
107
105
  ## Local development
108
106
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # pi-cc-extensions
6
6
 
7
- > 类 Claude Code TUI 输出风格,并融入了一些个人喜好,和一些实用小功能。
7
+ > 类 Claude Code TUI 输出风格,并融入了一些个人喜好,和一些实用小功能(无额外内置工具)。
8
8
 
9
9
  ## 界面预览
10
10
 
@@ -34,7 +34,7 @@ pi install git:github.com/minuque/pi-cc-extensions
34
34
  | 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools definition 和消息内容 | `/context` |
35
35
  | Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
36
36
  | 状态栏 | 显示:模型、上下文、缓存、费用、git并适配 @narumitw/pi-usage 实时显示额度;git/缓存图标可关 Nerd Font | `/ccstyle` |
37
- | 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/theme` |
37
+ | 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/settings` → Theme |
38
38
 
39
39
  `on` / `compact` 两种模式的渲染样例:[默认模式](./docs/tool-render-examples-default.md) · [紧凑模式](./docs/tool-render-examples-compact.md)
40
40
 
@@ -55,7 +55,7 @@ pi install git:github.com/minuque/pi-cc-extensions
55
55
  "enableAgentSummary": true, // 每回合工具摘要
56
56
  "enableWorkingMessage": true, // Working... 底部 token/耗时
57
57
  "enableAliases": true, // /clear、/exit 别名
58
- "enableMarkdownEnhance": true, // markdown 转换:Mermaid 方言渲染、圈数字转半角括号;false 让位给其他扩展
58
+ "enableMarkdownEnhance": true, // markdown 转换:Mermaid 方言渲染、圈数字转半角括号;false 让位给其他扩展
59
59
  "enableEditorPrefix": true, // 输入框 ❯ 前缀
60
60
 
61
61
  // ui
@@ -97,12 +97,10 @@ pi install git:github.com/minuque/pi-cc-extensions
97
97
  ```
98
98
 
99
99
  > [!TIP]
100
- > **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;工具卡展开态 diff 全量显示,Input/Output 超行时末行 `… +N more lines · click to show more` 打开全量预览,单击收起。
101
- >
102
- > `scrollButtonMode` 两种模式文案相同(`Back to bottom` / `N new message(s)`):`dock` 画在独占行,`native` 画在 Pi 原生浮层。
100
+ > **全屏模式**:展开态 diff 全量显示。Input/Output 超行时末行给 `… +N more lines`,单击看全量,再击收起。
103
101
 
104
102
  > [!NOTE]
105
- > **Mermaid 渲染**:建议把 `markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
103
+ > **Mermaid 渲染**:`markdown.mermaid` 建议设为 `final`(`~/.pi/agent/settings.json` 或 `/settings`)。默认 `streaming` 逐帧重绘,`final` 只渲染最终版。
106
104
 
107
105
  ## 本地开发
108
106
 
@@ -431,6 +431,11 @@ export async function showCcstylePanel(
431
431
  return;
432
432
  }
433
433
 
434
+ // 编辑器插槽只能在面板关闭后替换:面板自己就挂在 editorContainer 上,面板开着时调
435
+ // setEditorComponent 会把面板踢出渲染树、done() 再也走不到,`ctx.ui.custom()` 永不落定,
436
+ // 整个输入管线随之卡死。
437
+ let editorPrefixChanged = false;
438
+
434
439
  await ctx.ui.custom((tui: any, theme: any, _keybindings: any, done: () => void) => {
435
440
  const modeSetting = {
436
441
  id: "mode",
@@ -880,8 +885,7 @@ export async function showCcstylePanel(
880
885
  editorPrefixSetting.description = enabled
881
886
  ? "Show ❯ before the input line; color follows thinking level / bash mode."
882
887
  : "Plain input line, no ❯ glyph.";
883
- if (enabled) applyPromptEditor(ctx);
884
- else clearPromptEditor(ctx);
888
+ editorPrefixChanged = true;
885
889
  ctx.ui.notify(`Updated ${id}: ${value}`, "info");
886
890
  return;
887
891
  }
@@ -1208,6 +1212,11 @@ export async function showCcstylePanel(
1208
1212
  },
1209
1213
  };
1210
1214
  });
1215
+ // 面板已经卸下,插槽此时替换才不会踢掉对话框。
1216
+ if (editorPrefixChanged) {
1217
+ if (config.enableEditorPrefix) applyPromptEditor(ctx);
1218
+ else clearPromptEditor(ctx);
1219
+ }
1211
1220
  // 面板卸下后主 transcript 重新挂载;再刷一次,吃掉打开期间扫树失败的切换。
1212
1221
  hooks.refreshCurrentTranscript(ctx, toolGrouping);
1213
1222
  }
@@ -385,12 +385,24 @@ const createCustomFooterFactory =
385
385
  return 0;
386
386
  };
387
387
 
388
- const render = (width: number): string[] => {
388
+ // 三个会话统计每帧重算会全量遍历 session,长会话把 pi 钉在 100% CPU(issue #80)。
389
+ // leaf/model/thinking 任一变化立即重算;不变时 1s TTL,流式期间每秒刷新一次。
390
+ const SESSION_STATS_TTL_MS = 1_000;
391
+ let statsMemoKey = "";
392
+ let statsMemoAt = 0;
393
+ let statsMemo:
394
+ | { contextUsage: ReturnType<typeof ctx.getContextUsage>; cost: number; cachePct: number }
395
+ | undefined;
396
+ const sessionStats = () => {
389
397
  const model = ctx.model;
390
- const contextUsage = ctx.getContextUsage();
391
- const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
392
- const percent = contextUsage?.percent;
393
-
398
+ const key = `${ctx.sessionManager.getLeafId() ?? ""}|${model?.provider}/${model?.id}|${ctx.thinkingLevel ?? ""}`;
399
+ if (
400
+ statsMemo &&
401
+ key === statsMemoKey &&
402
+ performance.now() - statsMemoAt < SESSION_STATS_TTL_MS
403
+ ) {
404
+ return statsMemo;
405
+ }
394
406
  let cost = 0;
395
407
  for (const entry of ctx.sessionManager.getEntries()) {
396
408
  if (entry.type === "message") {
@@ -404,6 +416,17 @@ const createCustomFooterFactory =
404
416
  cost += entry.usage.cost?.total || 0;
405
417
  }
406
418
  }
419
+ statsMemo = { contextUsage: ctx.getContextUsage(), cost, cachePct: getCachePct() };
420
+ statsMemoKey = key;
421
+ statsMemoAt = performance.now();
422
+ return statsMemo;
423
+ };
424
+
425
+ const render = (width: number): string[] => {
426
+ const model = ctx.model;
427
+ const { contextUsage, cost, cachePct } = sessionStats();
428
+ const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
429
+ const percent = contextUsage?.percent;
407
430
 
408
431
  // cwd 默认只显示最底层路径(根目录时回退完整路径)
409
432
  const cwdLabel = ctx.cwd.split(/[\\/]/).filter(Boolean).pop() || ctx.cwd;
@@ -430,7 +453,6 @@ const createCustomFooterFactory =
430
453
  const dimPlugin = (text: string) => colorUsageChip(theme, text);
431
454
 
432
455
  const pctLabel = percent === null || percent === undefined ? "?" : `${Math.floor(percent)}%`;
433
- const cachePct = getCachePct();
434
456
  const glyphs = footerGlyphs(config.footerNerdIcons);
435
457
  const cacheLabel =
436
458
  cachePct > 0 ? `${glyphs.cache ? `${glyphs.cache} ` : ""}${Math.floor(cachePct)}%` : "";
@@ -59,27 +59,48 @@ const promptEditorFactory: EditorFactory = (tui, theme, keybindings) =>
59
59
  let active = false;
60
60
  let previousEditorFactory: EditorFactory | undefined;
61
61
 
62
- /** 撤销本扩展占据的插槽;从未占用就不碰插槽。 */
62
+ /**
63
+ * 撤销本扩展占据的插槽;从未占用就不碰插槽。
64
+ *
65
+ * 插槽可能已经被其他扩展包在外层(pi-inline-skills 会把当前工厂再包一层),
66
+ * 那种结构里无法只拆内层,直接还回占据前的工厂;外层扩展会在下次终端输入时
67
+ * 自己重新包装,前缀不会因此复活。
68
+ *
69
+ * 例外:插槽被 pi 重置回占据前的工厂时(/new、/reload 走 resetExtensionUI),
70
+ * 我们已经不在槽里,再写一次会把 pi 刚重置的结果又覆盖掉;此时只清状态。
71
+ */
63
72
  function releasePromptEditor(ctx: ExtensionContext): void {
64
73
  if (!active) return;
65
- ctx.ui.setEditorComponent(previousEditorFactory);
74
+ const previous = previousEditorFactory;
66
75
  active = false;
67
76
  previousEditorFactory = undefined;
77
+ if (ctx.ui.getEditorComponent?.() === previous) return;
78
+ ctx.ui.setEditorComponent(previous);
68
79
  }
69
80
 
70
- /** 应用输入框前缀;enableEditorPrefix=false 时只撤自己装的那层。 */
81
+ /**
82
+ * 应用输入框前缀;enableEditorPrefix=false 时只撤自己装的那层。
83
+ *
84
+ * 只在没有扩展对话框开着时调用:pi 的 setEditorComponent 会清空 editorContainer 并
85
+ * 抢走焦点,把正在设置面板里的对话框踢出渲染树,对话框的 done() 再也走不到,
86
+ * `ctx.ui.custom()` 的 Promise 永不落定,后续输入全部卡死。设置面板自己不做替换,
87
+ * 等 `ctx.ui.custom()` 返回后再调本函数。
88
+ *
89
+ * 插槽已经装着本扩展的编辑器时直接返回:重复安装只会重建实例,丢掉撤销栈、
90
+ * 历史等编辑器状态,还会打断其他扩展包在外层的编辑器。
91
+ */
71
92
  export function applyPromptEditor(ctx: ExtensionContext): void {
72
93
  if (!ctx?.hasUI || typeof ctx.ui?.setEditorComponent !== "function") return;
94
+ if (!config.enableEditorPrefix) {
95
+ releasePromptEditor(ctx);
96
+ return;
97
+ }
98
+ const current = ctx.ui.getEditorComponent?.();
99
+ if (current === promptEditorFactory) return;
73
100
  try {
74
- if (!config.enableEditorPrefix) {
75
- releasePromptEditor(ctx);
76
- return;
77
- }
78
- if (!active) {
79
- // pi 的插槽是硬替换、没有栈,先记住是谁占着,撤销时还回去。
80
- previousEditorFactory = ctx.ui.getEditorComponent?.();
81
- active = true;
82
- }
101
+ // pi 的插槽是硬替换、没有栈,先记住是谁占着,撤销时还回去。
102
+ previousEditorFactory = current;
103
+ active = true;
83
104
  ctx.ui.setEditorComponent(promptEditorFactory);
84
105
  } catch (err) {
85
106
  ctx.ui.notify(
@@ -1057,6 +1057,14 @@ function compactStopStatusLine(status: string, pad = 0): any {
1057
1057
  /** 展开入口的动作词:我们的 `click to show more`、pi 的 `<key> to expand`(键名可能取不到)。 */
1058
1058
  const EXPAND_ACTION = String.raw`(?:click to show more|(?:[\w/+]+\s+)?to (?:show more|expand))`;
1059
1059
 
1060
+ /**
1061
+ * 展开入口只挂在行尾:正则只看行尾这一段,单行代价与行长无关。
1062
+ * 图片负载(kitty 图形数据的 Base64)一行可达数百 KB,整行过正则会拖住渲染帧。
1063
+ */
1064
+ const SLOT_HINT_WINDOW = 200;
1065
+ /** 窗口里出现这些词才可能有展开入口,其余行(负载、代码)直接放过正则。 */
1066
+ const SLOT_HINT_MARKERS = ["to expand", "to show more"];
1067
+
1060
1068
  const SLOT_HINT_PATTERNS: RegExp[] = [
1061
1069
  // 卡在括号里:`(9 more lines, click to show more)` / `(195 earlier lines, ctrl+o to expand)`,
1062
1070
  // 只去掉动作词,收尾括号与计数保留。
@@ -1066,13 +1074,18 @@ const SLOT_HINT_PATTERNS: RegExp[] = [
1066
1074
  new RegExp(String.raw`\s*(?:[•·]\s*)?${EXPAND_ACTION}\s*$`),
1067
1075
  ];
1068
1076
 
1069
- function stripSlotHint(line: string): string {
1070
- const plain = stripTerminalSequencesPreservingLayout(line);
1077
+ /** 导出供单测:窗口边界与键名长度是性能与裁剪行为的关键,需要逐条钉住。 */
1078
+ export function stripSlotHint(line: string): string {
1079
+ // 切点可能落在转义序列中间,残片只当作窗口头部文本;两个模式都锚在行尾,不受影响。
1080
+ const head = line.slice(0, Math.max(0, line.length - SLOT_HINT_WINDOW));
1081
+ const tail = line.slice(head.length);
1082
+ const plain = stripTerminalSequencesPreservingLayout(tail);
1083
+ if (!SLOT_HINT_MARKERS.some((marker) => plain.includes(marker))) return line;
1071
1084
  for (const re of SLOT_HINT_PATTERNS) {
1072
1085
  const match = re.exec(plain);
1073
1086
  if (!match) continue;
1074
1087
  // 删除区间而非截断后拼纯文本:收尾括号留在原样式里(dim),不会掉回默认前景色。
1075
- return removeStyledRange(line, match.index, match.index + match[0].length);
1088
+ return head + removeStyledRange(tail, match.index, match.index + match[0].length);
1076
1089
  }
1077
1090
  return line;
1078
1091
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "license": "MIT",
5
5
  "description": "Claude Code-style TUI output with some personal touches and handy utilities.",
6
6
  "type": "module",