pi-cc-extensions 0.5.6 → 0.7.0

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.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <p align="center">
6
6
  <a href="#安装"><img alt="Pi package" src="https://img.shields.io/badge/Pi-package-58B7FF?style=flat-square"></a>
7
- <a href="./package.json"><img alt="Version 0.2.0" src="https://img.shields.io/badge/version-0.2.0-66E3C4?style=flat-square"></a>
7
+ <a href="./package.json"><img alt="Version 0.5.6" src="https://img.shields.io/badge/version-0.5.6-66E3C4?style=flat-square"></a>
8
8
  <a href="#兼容性"><img alt="Node.js 22.19 or newer" src="https://img.shields.io/badge/Node.js-%E2%89%A522.19-66E3C4?style=flat-square"></a>
9
9
  <a href="./extensions"><img alt="TypeScript extensions" src="https://img.shields.io/badge/TypeScript-extensions-3178C6?style=flat-square"></a>
10
10
  </p>
@@ -15,10 +15,15 @@
15
15
 
16
16
  ---
17
17
 
18
- ## 截图
18
+ ## 界面预览
19
19
 
20
20
  <table>
21
21
  <tr>
22
+ <td width="50%" align="center">
23
+ <img src="./assets/readme/fixed-editor-navigation.webp" width="100%" alt="Pi fixed-editor 中的工具结果、回到底部按钮与 Ctrl+End 快捷键">
24
+ <br>
25
+ <sub><b>固定编辑器工作流</b><br>工具结果摘要、折叠展开与键盘导航</sub>
26
+ </td>
22
27
  <td width="50%" align="center">
23
28
  <img src="./assets/readme/session-reference.webp" width="100%" alt="在 Pi 中通过 @ 补全选择历史 Session">
24
29
  <br>
@@ -54,10 +59,10 @@
54
59
  常用命令:
55
60
 
56
61
  ```text
57
- /ccstyle # 切换开关
62
+ /ccstyle # 打开交互式配置面板
63
+ /ccstyle on # 开启
64
+ /ccstyle off # 关闭
58
65
  /ccstyle status # 查看状态
59
- /ccstyle compact # 紧凑摘要
60
- /ccstyle minimal # 最小化输出
61
66
  ```
62
67
 
63
68
  ### 上下文窗口查看
@@ -82,6 +87,16 @@
82
87
 
83
88
  Session 模糊查询默认显示 5 个候选;若已加载 `pi-subagents`,也可直接引用现有 SubAgent。一次提示词可以引用多个 Session;扩展会自动去重,并限制注入规模以避免上下文无限膨胀。更多细节见 [`extensions/session-reference/README.md`](./extensions/session-reference/README.md)。
84
89
 
90
+ ### SubAgent `@` 补全
91
+
92
+ `extensions/agent-autocomplete.ts` 将 `~/.pi/agent/agents/*.md` 中定义的 SubAgent 接入 `@` 补全:
93
+
94
+ 1. 在提示词中输入 `@agent-name`。
95
+ 2. 从补全列表中选择 SubAgent。
96
+ 3. 提交后自动注入委托指令;提示词中提到的每个 SubAgent 都会分别交由 Agent 工具处理。
97
+
98
+ 该补全不会占用 `@session:` 的历史 Session 引用语法;修改 SubAgent 定义后执行 `/reload` 即可重新加载。
99
+
85
100
  ## 安装
86
101
 
87
102
  从 npm 安装(推荐):
@@ -100,7 +115,7 @@ pi install git:github.com/minuque/pi-cc-extensions
100
115
 
101
116
  ```text
102
117
  /context
103
- /ccstyle status
118
+ /ccstyle on
104
119
  ```
105
120
 
106
121
  ## 本地开发
@@ -126,20 +141,3 @@ pi install /absolute/path/to/pi-cc-extensions
126
141
 
127
142
  - Node.js `>=22.19.0`
128
143
  - 作为 Pi package 加载,入口由根目录 `package.json` 的 `pi.extensions` 显式声明
129
-
130
- ## 发布
131
-
132
- 发布前检查:
133
-
134
- ```bash
135
- npm test
136
- npm pack --dry-run
137
- ```
138
-
139
- 使用 npm 发布。未指定 tag 时使用 `latest`:
140
-
141
- ```bash
142
- npm run release
143
- npm run release -- --tag next
144
- npm run release -- --tag beta
145
- ```
@@ -9,10 +9,11 @@ import {
9
9
  createWriteToolDefinition,
10
10
  generateDiffString,
11
11
  keyHint,
12
+ getSettingsListTheme,
12
13
  renderDiff,
13
14
  ToolExecutionComponent,
14
15
  } from "@earendil-works/pi-coding-agent";
15
- import { Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
16
+ import { Container, SettingsList, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
16
17
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
18
  import { readFile, stat } from "node:fs/promises";
18
19
  import { homedir } from "node:os";
@@ -26,22 +27,17 @@ import { join, resolve } from "node:path";
26
27
  * expand only that tool.
27
28
  *
28
29
  * Dynamic commands:
29
- * /ccstyle toggle on/off
30
+ * /ccstyle open interactive settings
30
31
  * /ccstyle on enable
31
32
  * /ccstyle off disable
32
33
  * /ccstyle status show current state
33
- * /ccstyle minimal hide most tool output in collapsed view
34
- * /ccstyle compact show short summaries in collapsed view
35
34
  *
36
35
  * Shortcut:
37
36
  * ctrl+shift+o toggle on/off
38
37
  */
39
38
 
40
- type StyleMode = "compact" | "minimal";
41
-
42
39
  type Config = {
43
40
  enabled: boolean;
44
- mode: StyleMode;
45
41
  };
46
42
 
47
43
  const CONFIG_PATH = join(homedir(), ".pi", "agent", "claude-code-style.json");
@@ -54,13 +50,12 @@ function loadConfig(): Config {
54
50
  const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<Config>;
55
51
  return {
56
52
  enabled: parsed.enabled ?? true,
57
- mode: parsed.mode === "minimal" ? "minimal" : "compact",
58
53
  };
59
54
  }
60
55
  } catch {
61
56
  // Ignore bad config and fall back to defaults.
62
57
  }
63
- return { enabled: true, mode: "compact" };
58
+ return { enabled: true };
64
59
  }
65
60
 
66
61
  function saveConfig() {
@@ -331,6 +326,7 @@ let scrollButtonWidget: any = null;
331
326
  let pendingScrollMessages = 0;
332
327
  let assistantMessageActive = false;
333
328
  let scrollButtonSyncScheduled = false;
329
+ let resumeRenderTimer: ReturnType<typeof setTimeout> | null = null;
334
330
 
335
331
  function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
336
332
  const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
@@ -592,15 +588,14 @@ function hideScrollButton(tui: any): void {
592
588
  }
593
589
 
594
590
  function scheduleScrollButtonSync(tui: any, data: string): void {
595
- if (!scrollButtonVisible || !isScrollNavigationInput(data) || scrollButtonSyncScheduled) return;
591
+ if (!isScrollNavigationInput(data) || scrollButtonSyncScheduled) return;
596
592
  scrollButtonSyncScheduled = true;
597
593
  const previousLines = tui.previousLines;
598
594
  const check = (attempt: number) => {
599
595
  scrollButtonSyncScheduled = false;
600
- if (toolMouseTui !== tui || !scrollButtonVisible) return;
601
- // Pi renders on its own frame timer. Do not inspect the old visible window
602
- // before Zentui has applied the new scroll offset, or PageUp at bottom would
603
- // immediately hide the button it just requested.
596
+ if (toolMouseTui !== tui) return;
597
+ // Pi renders on its own frame timer. Inspect the resulting viewport before
598
+ // showing the button so empty or non-scrollable transcripts never flash it.
604
599
  const rendered = tui.previousLines !== previousLines;
605
600
  if (!rendered && attempt < 4) {
606
601
  scrollButtonSyncScheduled = true;
@@ -610,35 +605,20 @@ function scheduleScrollButtonSync(tui: any, data: string): void {
610
605
  }
611
606
  return;
612
607
  }
613
- if (isFixedEditorAtBottom(tui)) hideScrollButton(tui);
608
+ const nextVisible = !isFixedEditorAtBottom(tui);
609
+ if (!nextVisible) pendingScrollMessages = 0;
610
+ if (nextVisible !== scrollButtonVisible) {
611
+ scrollButtonVisible = nextVisible;
612
+ tui.requestRender?.();
613
+ }
614
614
  };
615
615
  process.nextTick(() => check(0));
616
616
  }
617
617
 
618
618
  function updateScrollButtonFromInput(tui: any, data: string): void {
619
619
  if (!isFixedEditorTui(tui)) return;
620
-
621
- let movedUp = matchesKey(data, "pageUp") || ZENTUI_PAGE_UP_INPUT.test(data);
622
- const packets = parseSgrMousePackets(data);
623
- if (packets) {
624
- movedUp = packets.some((packet) => {
625
- const baseButton = packet.code & ~(4 | 8 | 16 | 32);
626
- return packet.final === "M" && baseButton === 64;
627
- });
628
- }
629
-
630
- const jumpedBottom =
631
- matchesKey(data, "enter")
632
- || matchesKey(data, "return")
633
- || isScrollBottomInput(data);
634
- if (jumpedBottom) {
620
+ if (matchesKey(data, "enter") || matchesKey(data, "return") || isScrollBottomInput(data)) {
635
621
  hideScrollButton(tui);
636
- return;
637
- }
638
- const nextVisible = movedUp ? true : scrollButtonVisible;
639
- if (nextVisible !== scrollButtonVisible) {
640
- scrollButtonVisible = nextVisible;
641
- tui.requestRender?.();
642
622
  }
643
623
  }
644
624
 
@@ -842,6 +822,8 @@ function patchToolMouseInputCapture(tui: any): void {
842
822
  const data = args[0];
843
823
  if (typeof data === "string") {
844
824
  updateScrollButtonFromInput(this, data);
825
+ // Capture the current viewport before Pi/Zentui applies the scroll input.
826
+ scheduleScrollButtonSync(this, data);
845
827
  if (isFixedEditorTui(this) && isScrollBottomInput(data) && jumpToBottomWithoutSubmit(this)) return;
846
828
  const packets = parseSgrMousePackets(data);
847
829
  if (packets) {
@@ -906,6 +888,10 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
906
888
  }
907
889
 
908
890
  function teardownToolMouseInteraction(): void {
891
+ if (resumeRenderTimer) {
892
+ clearTimeout(resumeRenderTimer);
893
+ resumeRenderTimer = null;
894
+ }
909
895
  toolMouseInputUnsubscribe?.();
910
896
  toolMouseInputUnsubscribe = null;
911
897
  try {
@@ -948,6 +934,19 @@ function installToolMouseInteraction(ctx: any): void {
948
934
  toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
949
935
  }
950
936
 
937
+ function scheduleResumeRender(): void {
938
+ const tui = toolMouseTui;
939
+ if (!tui || typeof tui.requestRender !== "function") return;
940
+ if (resumeRenderTimer) clearTimeout(resumeRenderTimer);
941
+ // /resume rebuilds the transcript while later session_start handlers may still
942
+ // replace UI components. Repaint after that turn so the restored messages are
943
+ // not left hidden until the next editor input event.
944
+ resumeRenderTimer = setTimeout(() => {
945
+ resumeRenderTimer = null;
946
+ if (toolMouseTui === tui) tui.requestRender(true);
947
+ }, 0);
948
+ }
949
+
951
950
  // Bright green for success icon (truecolor ANSI escape)
952
951
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
953
952
  const ANSI_RESET = "\x1b[0m";
@@ -1096,12 +1095,49 @@ function renderFileDiffPreview(_toolName: string, _args: any, theme: any, contex
1096
1095
  return renderDiffPreview(state.ccstyleDiffPreview, theme, isToolExpanded(undefined, context));
1097
1096
  }
1098
1097
 
1099
- function statusText() {
1100
- return config.enabled ? `CC ${config.mode}` : "CC off";
1098
+ function applyEnabledState(enabled: boolean, ctx: any): void {
1099
+ config.enabled = enabled;
1100
+ saveConfig();
1101
+ ctx.ui.notify(`Claude Code style: ${enabled ? "on" : "off"}`, "info");
1101
1102
  }
1102
1103
 
1103
- function updateStatus(ctx: any) {
1104
- ctx.ui.setStatus("ccstyle", statusText());
1104
+ async function showCcstylePanel(ctx: any): Promise<void> {
1105
+ if (ctx?.mode !== "tui" || !ctx?.hasUI || typeof ctx.ui?.custom !== "function") {
1106
+ ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
1107
+ return;
1108
+ }
1109
+
1110
+ await ctx.ui.custom((tui: any, theme: any, _keybindings: any, done: (value?: void) => void) => {
1111
+ const container = new Container();
1112
+ container.addChild(new Text(theme.fg("accent", theme.bold("Claude Code Style")), 1, 1));
1113
+ const settingsList = new SettingsList(
1114
+ [{
1115
+ id: "enabled",
1116
+ label: "Output style",
1117
+ description: "Use compact Claude Code-like tool rendering",
1118
+ currentValue: config.enabled ? "on" : "off",
1119
+ values: ["on", "off"],
1120
+ }],
1121
+ 3,
1122
+ getSettingsListTheme(),
1123
+ (id: string, newValue: string) => {
1124
+ if (id !== "enabled" || (newValue !== "on" && newValue !== "off")) return;
1125
+ applyEnabledState(newValue === "on", ctx);
1126
+ settingsList.updateValue(id, newValue);
1127
+ tui.requestRender();
1128
+ },
1129
+ () => done(),
1130
+ );
1131
+ container.addChild(settingsList);
1132
+ return {
1133
+ render: (width: number) => container.render(width),
1134
+ invalidate: () => container.invalidate(),
1135
+ handleInput(data: string) {
1136
+ settingsList.handleInput?.(data);
1137
+ tui.requestRender();
1138
+ },
1139
+ };
1140
+ });
1105
1141
  }
1106
1142
 
1107
1143
  function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
@@ -1207,14 +1243,7 @@ function createCcstyleTool(originalTool: any): any {
1207
1243
  const expanded = isToolExpanded(options, context);
1208
1244
 
1209
1245
  const text = textFromResult(result);
1210
- let rendered = "";
1211
- if (config.mode === "minimal" && !expanded) {
1212
- rendered = "";
1213
- } else if (!expanded) {
1214
- rendered = text ? oneLine(text, 96) : "Done";
1215
- } else {
1216
- rendered = text;
1217
- }
1246
+ const rendered = !expanded ? (text ? oneLine(text, 96) : "Done") : text;
1218
1247
 
1219
1248
  const hint = !expanded && hasExpandableResult(text) ? expandHint(theme) : "";
1220
1249
  if (expanded) return renderExpandedToolResult(rendered, theme, isError);
@@ -1509,35 +1538,25 @@ export default function (pi: ExtensionAPI) {
1509
1538
  const topLevel = [
1510
1539
  { value: "on", label: "on", description: "Enable Claude Code style" },
1511
1540
  { value: "off", label: "off", description: "Disable Claude Code style" },
1512
- { value: "status", label: "status", description: "Show current toggle and mode" },
1513
- { value: "minimal", label: "minimal", description: "Hide most tool output in collapsed view" },
1514
- { value: "compact", label: "compact", description: "Show short summaries in collapsed view" },
1541
+ { value: "status", label: "status", description: "Show current state" },
1515
1542
  ];
1516
1543
  return topLevel.filter((item) => item.value.startsWith(prefix));
1517
1544
  },
1518
1545
  handler: async (args, ctx) => {
1519
1546
  const arg = args.trim().toLowerCase();
1520
- if (arg === "on" || arg === "enable") config.enabled = true;
1521
- else if (arg === "off" || arg === "disable") config.enabled = false;
1522
- else if (arg === "minimal") {
1523
- config.enabled = true;
1524
- config.mode = "minimal";
1525
- } else if (arg === "compact") {
1526
- config.enabled = true;
1527
- config.mode = "compact";
1528
- } else if (arg === "status") {
1529
- updateStatus(ctx);
1530
- ctx.ui.notify(
1531
- `Claude Code style: ${config.enabled ? "on" : "off"}, mode=${config.mode}`,
1532
- "info",
1533
- );
1547
+ if (!arg) {
1548
+ await showCcstylePanel(ctx);
1534
1549
  return;
1535
- } else {
1536
- config.enabled = !config.enabled;
1537
1550
  }
1538
- saveConfig();
1539
- updateStatus(ctx);
1540
- ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}, mode=${config.mode}`, "info");
1551
+ if (arg === "on" || arg === "off") {
1552
+ applyEnabledState(arg === "on", ctx);
1553
+ return;
1554
+ }
1555
+ if (arg === "status") {
1556
+ ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
1557
+ return;
1558
+ }
1559
+ ctx.ui.notify("Usage: /ccstyle [on|off|status]", "warning");
1541
1560
  },
1542
1561
  });
1543
1562
 
@@ -1546,17 +1565,17 @@ export default function (pi: ExtensionAPI) {
1546
1565
  handler: async (ctx) => {
1547
1566
  config.enabled = !config.enabled;
1548
1567
  saveConfig();
1549
- updateStatus(ctx);
1550
1568
  ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
1551
1569
  },
1552
1570
  });
1553
1571
 
1554
- pi.on("session_start", async (_event, ctx) => {
1572
+ pi.on("session_start", async (event, ctx) => {
1555
1573
  diffPreviewByCall.clear();
1556
1574
  pendingScrollMessages = 0;
1557
1575
  assistantMessageActive = false;
1558
- updateStatus(ctx);
1576
+ ctx.ui.setStatus("ccstyle", undefined);
1559
1577
  installToolMouseInteraction(ctx);
1578
+ if (event?.reason === "resume") scheduleResumeRender();
1560
1579
  });
1561
1580
 
1562
1581
  pi.on("message_start", async (event) => {
@@ -1584,7 +1603,6 @@ export default function (pi: ExtensionAPI) {
1584
1603
  call: (args) => fileAction(args, "Read"),
1585
1604
  result: (result, { expanded }) => {
1586
1605
  const text = textFromResult(result);
1587
- if (config.mode === "minimal" && !expanded) return "";
1588
1606
  if (!expanded) return `${countLines(text)} lines read`;
1589
1607
  return text;
1590
1608
  },
@@ -1594,7 +1612,6 @@ export default function (pi: ExtensionAPI) {
1594
1612
  call: (args) => `Bash(${oneLine(args?.command, 86) || "..."})`,
1595
1613
  result: (result, { expanded }) => {
1596
1614
  const text = textFromResult(result).trim();
1597
- if (config.mode === "minimal" && !expanded) return "";
1598
1615
  if (!expanded) return summarizeBash(text);
1599
1616
  return text;
1600
1617
  },
@@ -1606,7 +1623,6 @@ export default function (pi: ExtensionAPI) {
1606
1623
  diffAfterResult: true,
1607
1624
  result: (result, { expanded }, _theme, context) => {
1608
1625
  const text = textFromResult(result).trim();
1609
- if (config.mode === "minimal" && !expanded) return "";
1610
1626
  return summarizeDiffStats(context?.state?.ccstyleDiffPreview) ?? summarizeEdit(text);
1611
1627
  },
1612
1628
  });
@@ -1621,7 +1637,6 @@ export default function (pi: ExtensionAPI) {
1621
1637
  diffAfterResult: true,
1622
1638
  result: (result, { expanded }, _theme, context) => {
1623
1639
  const text = textFromResult(result).trim();
1624
- if (config.mode === "minimal" && !expanded) return "";
1625
1640
  return summarizeDiffStats(context?.state?.ccstyleDiffPreview) ?? (text ? oneLine(text, 96) : "Written");
1626
1641
  },
1627
1642
  });
@@ -1630,7 +1645,6 @@ export default function (pi: ExtensionAPI) {
1630
1645
  call: (args) => `Find(${args?.pattern || "..."}${args?.path ? ` in ${shortenPath(args.path)}` : ""})`,
1631
1646
  result: (result, { expanded }) => {
1632
1647
  const text = textFromResult(result).trim();
1633
- if (config.mode === "minimal" && !expanded) return "";
1634
1648
  if (!expanded) return `${countLines(text)} files`;
1635
1649
  return text;
1636
1650
  },
@@ -1640,7 +1654,6 @@ export default function (pi: ExtensionAPI) {
1640
1654
  call: (args) => `Grep(${args?.pattern ? `/${oneLine(args.pattern, 48)}/` : "..."})`,
1641
1655
  result: (result, { expanded }) => {
1642
1656
  const text = textFromResult(result).trim();
1643
- if (config.mode === "minimal" && !expanded) return "";
1644
1657
  if (!expanded) {
1645
1658
  const matches = countLines(text);
1646
1659
  const files = parsePathCount(text);
@@ -1654,7 +1667,6 @@ export default function (pi: ExtensionAPI) {
1654
1667
  call: (args) => `List(${shortenPath(args?.path || ".")})`,
1655
1668
  result: (result, { expanded }) => {
1656
1669
  const text = textFromResult(result).trim();
1657
- if (config.mode === "minimal" && !expanded) return "";
1658
1670
  if (!expanded) return `${countLines(text)} entries`;
1659
1671
  return text;
1660
1672
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.5.6",
3
+ "version": "0.7.0",
4
4
  "description": "Personal extensions for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [