pi-cc-extensions 0.6.0 → 0.7.2

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
@@ -59,7 +59,7 @@
59
59
  常用命令:
60
60
 
61
61
  ```text
62
- /ccstyle # 切换开关
62
+ /ccstyle # 打开交互式配置面板
63
63
  /ccstyle on # 开启
64
64
  /ccstyle off # 关闭
65
65
  /ccstyle status # 查看状态
@@ -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,7 +27,7 @@ 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
@@ -325,6 +326,7 @@ let scrollButtonWidget: any = null;
325
326
  let pendingScrollMessages = 0;
326
327
  let assistantMessageActive = false;
327
328
  let scrollButtonSyncScheduled = false;
329
+ let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
328
330
 
329
331
  function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
330
332
  const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
@@ -586,15 +588,14 @@ function hideScrollButton(tui: any): void {
586
588
  }
587
589
 
588
590
  function scheduleScrollButtonSync(tui: any, data: string): void {
589
- if (!scrollButtonVisible || !isScrollNavigationInput(data) || scrollButtonSyncScheduled) return;
591
+ if (!isScrollNavigationInput(data) || scrollButtonSyncScheduled) return;
590
592
  scrollButtonSyncScheduled = true;
591
593
  const previousLines = tui.previousLines;
592
594
  const check = (attempt: number) => {
593
595
  scrollButtonSyncScheduled = false;
594
- if (toolMouseTui !== tui || !scrollButtonVisible) return;
595
- // Pi renders on its own frame timer. Do not inspect the old visible window
596
- // before Zentui has applied the new scroll offset, or PageUp at bottom would
597
- // 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.
598
599
  const rendered = tui.previousLines !== previousLines;
599
600
  if (!rendered && attempt < 4) {
600
601
  scrollButtonSyncScheduled = true;
@@ -604,35 +605,20 @@ function scheduleScrollButtonSync(tui: any, data: string): void {
604
605
  }
605
606
  return;
606
607
  }
607
- 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
+ }
608
614
  };
609
615
  process.nextTick(() => check(0));
610
616
  }
611
617
 
612
618
  function updateScrollButtonFromInput(tui: any, data: string): void {
613
619
  if (!isFixedEditorTui(tui)) return;
614
-
615
- let movedUp = matchesKey(data, "pageUp") || ZENTUI_PAGE_UP_INPUT.test(data);
616
- const packets = parseSgrMousePackets(data);
617
- if (packets) {
618
- movedUp = packets.some((packet) => {
619
- const baseButton = packet.code & ~(4 | 8 | 16 | 32);
620
- return packet.final === "M" && baseButton === 64;
621
- });
622
- }
623
-
624
- const jumpedBottom =
625
- matchesKey(data, "enter")
626
- || matchesKey(data, "return")
627
- || isScrollBottomInput(data);
628
- if (jumpedBottom) {
620
+ if (matchesKey(data, "enter") || matchesKey(data, "return") || isScrollBottomInput(data)) {
629
621
  hideScrollButton(tui);
630
- return;
631
- }
632
- const nextVisible = movedUp ? true : scrollButtonVisible;
633
- if (nextVisible !== scrollButtonVisible) {
634
- scrollButtonVisible = nextVisible;
635
- tui.requestRender?.();
636
622
  }
637
623
  }
638
624
 
@@ -836,6 +822,8 @@ function patchToolMouseInputCapture(tui: any): void {
836
822
  const data = args[0];
837
823
  if (typeof data === "string") {
838
824
  updateScrollButtonFromInput(this, data);
825
+ // Capture the current viewport before Pi/Zentui applies the scroll input.
826
+ scheduleScrollButtonSync(this, data);
839
827
  if (isFixedEditorTui(this) && isScrollBottomInput(data) && jumpToBottomWithoutSubmit(this)) return;
840
828
  const packets = parseSgrMousePackets(data);
841
829
  if (packets) {
@@ -900,6 +888,10 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
900
888
  }
901
889
 
902
890
  function teardownToolMouseInteraction(): void {
891
+ if (sessionRenderTimer) {
892
+ clearTimeout(sessionRenderTimer);
893
+ sessionRenderTimer = null;
894
+ }
903
895
  toolMouseInputUnsubscribe?.();
904
896
  toolMouseInputUnsubscribe = null;
905
897
  try {
@@ -942,6 +934,19 @@ function installToolMouseInteraction(ctx: any): void {
942
934
  toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
943
935
  }
944
936
 
937
+ function scheduleSessionRender(): void {
938
+ const tui = toolMouseTui;
939
+ if (!tui || typeof tui.requestRender !== "function") return;
940
+ if (sessionRenderTimer) clearTimeout(sessionRenderTimer);
941
+ // Restored transcripts are populated at different points for startup, reload,
942
+ // and session replacement. Repaint after session_start and the surrounding UI
943
+ // rebuild finish so messages are not left hidden until the next terminal input.
944
+ sessionRenderTimer = setTimeout(() => {
945
+ sessionRenderTimer = null;
946
+ if (toolMouseTui === tui) tui.requestRender(true);
947
+ }, 0);
948
+ }
949
+
945
950
  // Bright green for success icon (truecolor ANSI escape)
946
951
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
947
952
  const ANSI_RESET = "\x1b[0m";
@@ -1090,12 +1095,49 @@ function renderFileDiffPreview(_toolName: string, _args: any, theme: any, contex
1090
1095
  return renderDiffPreview(state.ccstyleDiffPreview, theme, isToolExpanded(undefined, context));
1091
1096
  }
1092
1097
 
1093
- function statusText() {
1094
- return config.enabled ? "CC on" : "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");
1095
1102
  }
1096
1103
 
1097
- function updateStatus(ctx: any) {
1098
- 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
+ });
1099
1141
  }
1100
1142
 
1101
1143
  function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
@@ -1502,21 +1544,19 @@ export default function (pi: ExtensionAPI) {
1502
1544
  },
1503
1545
  handler: async (args, ctx) => {
1504
1546
  const arg = args.trim().toLowerCase();
1505
- if (arg === "on") config.enabled = true;
1506
- else if (arg === "off") config.enabled = false;
1507
- else if (arg === "status") {
1508
- updateStatus(ctx);
1509
- ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
1547
+ if (!arg) {
1548
+ await showCcstylePanel(ctx);
1510
1549
  return;
1511
- } else if (!arg) {
1512
- config.enabled = !config.enabled;
1513
- } else {
1514
- ctx.ui.notify("Usage: /ccstyle [on|off|status]", "warning");
1550
+ }
1551
+ if (arg === "on" || arg === "off") {
1552
+ applyEnabledState(arg === "on", ctx);
1515
1553
  return;
1516
1554
  }
1517
- saveConfig();
1518
- updateStatus(ctx);
1519
- ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
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");
1520
1560
  },
1521
1561
  });
1522
1562
 
@@ -1525,7 +1565,6 @@ export default function (pi: ExtensionAPI) {
1525
1565
  handler: async (ctx) => {
1526
1566
  config.enabled = !config.enabled;
1527
1567
  saveConfig();
1528
- updateStatus(ctx);
1529
1568
  ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
1530
1569
  },
1531
1570
  });
@@ -1534,8 +1573,9 @@ export default function (pi: ExtensionAPI) {
1534
1573
  diffPreviewByCall.clear();
1535
1574
  pendingScrollMessages = 0;
1536
1575
  assistantMessageActive = false;
1537
- updateStatus(ctx);
1576
+ ctx.ui.setStatus("ccstyle", undefined);
1538
1577
  installToolMouseInteraction(ctx);
1578
+ scheduleSessionRender();
1539
1579
  });
1540
1580
 
1541
1581
  pi.on("message_start", async (event) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.6.0",
3
+ "version": "0.7.2",
4
4
  "description": "Personal extensions for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [