pi-cc-extensions 0.8.50 → 0.8.52

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.
@@ -3,6 +3,11 @@ import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
3
3
  import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
4
  import type { CompactThinkingController } from "../feature/compact-thinking.ts";
5
5
  import { installToolGrouping, type ToolGroupingHooks } from "./tool-grouping.ts";
6
+ import {
7
+ installCompactMode,
8
+ refreshCompactModeComponents,
9
+ type CompactModeHooks,
10
+ } from "./compact-mode.ts";
6
11
  import { isLazyProxyTui } from "../utils/fullscreen-detect.ts";
7
12
  import { showCcstylePanel } from "../config/panel.ts";
8
13
  import {
@@ -33,6 +38,7 @@ import {
33
38
  countLines,
34
39
  hasExpandableDetail,
35
40
  headTruncateToWidth,
41
+ insetComponent,
36
42
  isToolExpanded,
37
43
  middleTruncateToWidth,
38
44
  oneLine,
@@ -49,6 +55,7 @@ import {
49
55
  toolViewportWidth,
50
56
  } from "./tool-result.ts";
51
57
  import { showMoreHintText } from "./show-more-hint.ts";
58
+ import type { CompactThinkingQuery } from "./compact-mode.ts";
52
59
  import {
53
60
  installWriteOverride,
54
61
  renderRichToolResult,
@@ -74,10 +81,19 @@ const ANSI_FG_RESET = "\x1b[39m";
74
81
  function refreshCurrentTranscript(ctx?: any, toolGrouping?: ToolGroupingHooks): void {
75
82
  toolGrouping?.refresh(toolMouseTui);
76
83
  refreshMessageDisplays(toolMouseTui);
84
+ refreshCompactModeComponents(toolMouseTui);
85
+ compactModeHooks?.refresh();
77
86
  toolMouseTui?.requestRender?.(true);
78
87
  ctx?.ui?.requestRender?.(true);
79
88
  }
80
89
 
90
+ let compactModeHooks: CompactModeHooks | undefined;
91
+
92
+ function syncCompactMode(ctx: any): void {
93
+ refreshCompactModeComponents(toolMouseTui);
94
+ compactModeHooks?.sync(ctx);
95
+ }
96
+
81
97
  function applyStyleMode(mode: CompactStyleMode, ctx: any, toolGrouping?: ToolGroupingHooks): void {
82
98
  config.mode = mode;
83
99
  saveConfig();
@@ -92,6 +108,9 @@ function applyStyleMode(mode: CompactStyleMode, ctx: any, toolGrouping?: ToolGro
92
108
  if (toolMouseTui && !isLazyProxyTui(toolMouseTui)) {
93
109
  toolMouseTui.terminal?.write?.(TOOL_MOUSE_DISABLE);
94
110
  }
111
+ } else if (mode === "compact") {
112
+ // 切入 compact:先收集当前 transcript,再同步全局展开状态和补丁所有权。
113
+ syncCompactMode(ctx);
95
114
  }
96
115
  refreshCurrentTranscript(ctx, toolGrouping);
97
116
  ctx.ui.notify(`Claude Code style: ${mode}`, "info");
@@ -113,17 +132,6 @@ function singleLine(text: string) {
113
132
  };
114
133
  }
115
134
 
116
- function insetComponent(component: any): any {
117
- return {
118
- render: (width: number) =>
119
- component.render(Math.max(1, width - 1)).map((line: string) => {
120
- const nestedMarker = line.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)↳/, "$1 ↳");
121
- return ` ${nestedMarker}`;
122
- }),
123
- invalidate: () => component.invalidate?.(),
124
- };
125
- }
126
-
127
135
  function humanizeToolLabel(label: string): string {
128
136
  return label
129
137
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
@@ -763,7 +771,7 @@ function installToolExpandedBackground(): () => void {
763
771
  const host = globalThis as any;
764
772
  const previous = host[TOOL_EXPANDED_BACKGROUND_PATCH] as ToolExpandedBackgroundPatch | undefined;
765
773
  if (previous) previous.dispose();
766
- const prototype = ToolExecutionComponent.prototype;
774
+ const prototype = ToolExecutionComponent.prototype as unknown as { updateDisplay: () => void };
767
775
  const original = prototype.updateDisplay;
768
776
  const patch: ToolExpandedBackgroundPatch = {
769
777
  active: true,
@@ -817,6 +825,7 @@ export default function (
817
825
  | {
818
826
  globalToolRendering: GlobalToolRenderPatch;
819
827
  toolGrouping: ToolGroupingHooks;
828
+ compactMode: CompactModeHooks;
820
829
  disposeMessageDisplay: () => void;
821
830
  disposeToolExpandedBackground: () => void;
822
831
  }
@@ -828,12 +837,18 @@ export default function (
828
837
  if (installation) return installation;
829
838
  const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
830
839
  const toolGrouping = installToolGrouping(() => config.mode === "on");
840
+ const compactMode = installCompactMode({
841
+ query: compactThinking as CompactThinkingQuery | undefined,
842
+ writeMetadata: writeExecutionMetadata,
843
+ });
844
+ compactModeHooks = compactMode;
831
845
  const disposeMessageDisplay = installMessageDisplayRendering();
832
846
  const disposeToolExpandedBackground = installToolExpandedBackground();
833
847
  deactivateLegacyCompactionRendering();
834
848
  installation = {
835
849
  globalToolRendering,
836
850
  toolGrouping,
851
+ compactMode,
837
852
  disposeMessageDisplay,
838
853
  disposeToolExpandedBackground,
839
854
  };
@@ -845,6 +860,11 @@ export default function (
845
860
  getArgumentCompletions: (prefix) => {
846
861
  const topLevel = [
847
862
  { value: "on", label: "on", description: "Enable Claude Code style" },
863
+ {
864
+ value: "compact",
865
+ label: "compact",
866
+ description: "One summary line per assistant message",
867
+ },
848
868
  { value: "off", label: "off", description: "Use Pi's native renderer" },
849
869
  { value: "status", label: "status", description: "Show full configuration" },
850
870
  { value: "panel", label: "panel", description: "Open interactive settings panel" },
@@ -853,7 +873,7 @@ export default function (
853
873
  },
854
874
  handler: async (args, ctx) => {
855
875
  const arg = args.trim().toLowerCase();
856
- if (!arg || arg === "panel" || arg === "on" || arg === "off") {
876
+ if (!arg || arg === "panel" || arg === "on" || arg === "compact" || arg === "off") {
857
877
  if (ctx?.mode !== "tui" || !ctx?.hasUI) {
858
878
  ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
859
879
  return;
@@ -868,7 +888,7 @@ export default function (
868
888
  compactThinking,
869
889
  );
870
890
  } else {
871
- applyStyleMode(arg, ctx, hooks.toolGrouping);
891
+ applyStyleMode(arg as CompactStyleMode, ctx, hooks.toolGrouping);
872
892
  }
873
893
  return;
874
894
  }
@@ -876,10 +896,28 @@ export default function (
876
896
  ctx.ui.notify(`Claude Code style: ${formatConfigStatus(config)}`, "info");
877
897
  return;
878
898
  }
879
- ctx.ui.notify("Usage: /ccstyle [on|off|status|panel]", "warning");
899
+ ctx.ui.notify("Usage: /ccstyle [on|compact|off|status|panel]", "warning");
880
900
  },
881
901
  });
882
902
 
903
+ pi.on("message_update", async (event) => {
904
+ // compact-thinking 在 session_start 时于 compact 补丁之上再装一层;
905
+ // 扩展事件先于 interactive-mode 的 updateContent 派发,此处先重新认领。
906
+ if (config.mode === "compact" && event.message?.role === "assistant") {
907
+ compactModeHooks?.assertOwnership();
908
+ }
909
+ });
910
+
911
+ pi.on("tool_execution_end", async (event) => {
912
+ if (config.mode !== "compact") return;
913
+ // Agent 工具在 tool_execution_end 收尾思考:延迟到所有监听器之后刷新,
914
+ // 让 compact-thinking 先写入最终时长,再重绘摘要行。
915
+ const toolCallId: string | undefined = event?.toolCallId;
916
+ setTimeout(() => {
917
+ compactModeHooks?.refreshToolCallMessage(toolCallId);
918
+ }, 0);
919
+ });
920
+
883
921
  pi.on("session_start", async (event, ctx) => {
884
922
  // 延迟到 session_start 注册 write override:加载阶段 getAllTools 不可用且其他扩展
885
923
  // 尚未注册工具,无法检测外部 write 所有者(如 pi-spark),直接注册会与对方撞名。
@@ -893,6 +931,11 @@ export default function (
893
931
  hooks.toolGrouping.setTheme(ctx.ui.theme);
894
932
  setMessageDisplayTheme(ctx.ui.theme);
895
933
  ctx.ui.setStatus("ccstyle", undefined);
934
+ // 先收集 resume transcript,再同步 compact 补丁与全局展开状态。
935
+ syncCompactMode(ctx);
936
+ // compact-thinking 的 session_start 处理在本 handler 之后执行(在其之上再装
937
+ // 一层 updateContent);延迟再同步一次,保证 compact 补丁最终位于外层。
938
+ setTimeout(() => syncCompactMode(ctx), 0);
896
939
  scheduleSessionRender(() => hooks.toolGrouping.refresh(toolMouseTui));
897
940
  });
898
941
 
@@ -904,7 +947,18 @@ export default function (
904
947
  if (!hooks) return;
905
948
  hooks.toolGrouping.setTheme(ctx.ui.theme);
906
949
  setMessageDisplayTheme(ctx.ui.theme);
907
- scheduleSessionRender(() => hooks.toolGrouping.refresh(toolMouseTui));
950
+ syncCompactMode(ctx);
951
+ scheduleSessionRender(() => {
952
+ syncCompactMode(ctx);
953
+ hooks.toolGrouping.refresh(toolMouseTui);
954
+ });
955
+ });
956
+
957
+ pi.on("session_tree", async (event, ctx) => {
958
+ // 会话树重建后在当前帧和下一帧各同步一次,替换旧组件引用。
959
+ if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
960
+ syncCompactMode(ctx);
961
+ scheduleSessionRender(() => syncCompactMode(ctx));
908
962
  });
909
963
 
910
964
  pi.on("tool_execution_start", async (event, ctx) => {
@@ -925,8 +979,10 @@ export default function (
925
979
  return;
926
980
  deactivateGlobalToolRendering(current.globalToolRendering);
927
981
  current.toolGrouping.shutdown();
928
- current.disposeMessageDisplay();
929
982
  current.disposeToolExpandedBackground();
983
+ current.compactMode.shutdown();
984
+ compactModeHooks = undefined;
985
+ current.disposeMessageDisplay();
930
986
  deactivateLegacyCompactionRendering();
931
987
  clearAllAnimations();
932
988
  installation = undefined;
@@ -45,7 +45,8 @@ const SKILL_KIND: DisplayKind = {
45
45
  };
46
46
 
47
47
  const COMPACTION_KIND: DisplayKind = {
48
- title: (component) => `Compacted from ${Number(component.message?.tokensBefore ?? 0).toLocaleString()} tokens`,
48
+ title: (component) =>
49
+ `Compacted from ${Number(component.message?.tokensBefore ?? 0).toLocaleString()} tokens`,
49
50
  body: (component) => String(component.message?.summary ?? ""),
50
51
  };
51
52
 
@@ -58,6 +59,10 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
58
59
  const theme = displayTheme;
59
60
  if (!theme) return; // 主题未就绪时保留原生渲染
60
61
  if (component.bgFn) component.setBgFn?.(undefined); // 与工具调用一致,去掉灰底
62
+ if (component.paddingY !== 0) {
63
+ component._ccstyleOriginalPaddingY = component.paddingY;
64
+ component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
65
+ }
61
66
  component.clear();
62
67
  const icon = `${BRIGHT_GREEN}✓${ANSI_FG_RESET}`; // 已完成消息,等同工具成功态
63
68
  const title = theme.fg("toolTitle", kind.title(component));
@@ -75,7 +80,11 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
75
80
  );
76
81
  }
77
82
 
78
- type PatchEntry = { prototype: any; installed: (...args: any[]) => void; original: (...args: any[]) => void };
83
+ type PatchEntry = {
84
+ prototype: any;
85
+ installed: (...args: any[]) => void;
86
+ original: (...args: any[]) => void;
87
+ };
79
88
 
80
89
  /** patch 三个消息组件的 updateDisplay,返回统一 dispose(/reload 链安全)。 */
81
90
  export function installMessageDisplayRendering(): () => void {
@@ -91,7 +100,7 @@ export function installMessageDisplayRendering(): () => void {
91
100
  const prototype = ComponentClass.prototype;
92
101
  const original = prototype.updateDisplay;
93
102
  const installed = function (this: any) {
94
- if (patch.active && config.mode === "on") {
103
+ if (patch.active && config.mode !== "off") {
95
104
  try {
96
105
  renderCcstyle(this, kind);
97
106
  return;
@@ -99,6 +108,11 @@ export function installMessageDisplayRendering(): () => void {
99
108
  // 渲染失败回退原生
100
109
  }
101
110
  }
111
+ // 回退原生前恢复 paddingY,避免原生渲染丢失上下内边距
112
+ if (this._ccstyleOriginalPaddingY !== undefined) {
113
+ this.paddingY = this._ccstyleOriginalPaddingY;
114
+ delete this._ccstyleOriginalPaddingY;
115
+ }
102
116
  original.call(this);
103
117
  };
104
118
  prototype.updateDisplay = installed;
@@ -0,0 +1,130 @@
1
+ import { setHoveredCompactAssistant } from "./compact-mode.ts";
2
+ import {
3
+ isExpandedToolIoView,
4
+ invalidateIoView,
5
+ type ExpandedToolIoView,
6
+ type ToolIoSection,
7
+ } from "./tool-result.ts";
8
+ import type { ToolGroupComponent } from "./tool-grouping.ts";
9
+ import { componentAtLocalRow, type ComponentRowHit } from "./mouse-layout.ts";
10
+ import { setScrollButtonHovered } from "./mouse-scroll.ts";
11
+
12
+ const TOOL_HOVER_STATE_KEY = Symbol.for("pi.ccstyle.tool-hover-state");
13
+
14
+ type SharedToolHoverState = { toolCallId: string | null };
15
+
16
+ /**
17
+ * hover 状态宿主(Symbol.for + globalThis):模块 reload 后新旧实例共享,
18
+ * 测试依赖该跨实例语义。
19
+ */
20
+ export function sharedToolHoverState(): SharedToolHoverState {
21
+ const host = globalThis as any;
22
+ return (host[TOOL_HOVER_STATE_KEY] ??= { toolCallId: null });
23
+ }
24
+
25
+ export let hoveredToolCallId: string | null = sharedToolHoverState().toolCallId;
26
+
27
+ export function setHoveredToolCallId(toolCallId: string | null): void {
28
+ hoveredToolCallId = toolCallId;
29
+ sharedToolHoverState().toolCallId = toolCallId;
30
+ }
31
+
32
+ export function isToolCallHovered(toolCallId: string | null | undefined): boolean {
33
+ return Boolean(toolCallId && sharedToolHoverState().toolCallId === toolCallId);
34
+ }
35
+
36
+ let hoveredToolGroup: ToolGroupComponent | null = null;
37
+
38
+ export function setHoveredToolGroup(group: ToolGroupComponent | null): boolean {
39
+ if (group === hoveredToolGroup) return false;
40
+ hoveredToolGroup?.setHintHovered(false);
41
+ hoveredToolGroup = group;
42
+ group?.setHintHovered(true);
43
+ return true;
44
+ }
45
+
46
+ let hoveredToolIoView: ExpandedToolIoView | null = null;
47
+ let hoveredToolIoSection: ToolIoSection | null = null;
48
+
49
+ export function setHoveredToolIo(
50
+ view: ExpandedToolIoView | null,
51
+ section: ToolIoSection | null,
52
+ ): boolean {
53
+ // resultRendererComponent 可能是 Text/第三方 renderer;reload 后也可能残留旧实例。
54
+ const nextView = isExpandedToolIoView(view) ? view : null;
55
+ const nextSection = nextView ? section : null;
56
+ if (nextView === hoveredToolIoView && nextSection === hoveredToolIoSection) return false;
57
+ if (isExpandedToolIoView(hoveredToolIoView)) {
58
+ hoveredToolIoView.setHoveredSection(null);
59
+ invalidateIoView(hoveredToolIoView);
60
+ }
61
+ hoveredToolIoView = nextView;
62
+ hoveredToolIoSection = nextSection;
63
+ if (nextView) {
64
+ nextView.setHoveredSection(nextSection);
65
+ invalidateIoView(nextView);
66
+ }
67
+ return true;
68
+ }
69
+
70
+ /** hover 与点击共用组件定位;同一布局下按容器/宽度/行缓存,避免 motion 重复渲染。 */
71
+ let fullscreenHoverCacheLayout: unknown = null;
72
+ let fullscreenHoverComponentCache = new WeakMap<object, Map<string, ComponentRowHit | null>>();
73
+
74
+ export function cachedFullscreenComponentAtRow(
75
+ layout: any,
76
+ container: any,
77
+ row: number,
78
+ width: number,
79
+ ): ComponentRowHit | null {
80
+ if (!container || typeof container !== "object") return null;
81
+ if (fullscreenHoverCacheLayout !== layout) {
82
+ fullscreenHoverCacheLayout = layout;
83
+ fullscreenHoverComponentCache = new WeakMap();
84
+ }
85
+ let rows = fullscreenHoverComponentCache.get(container);
86
+ if (!rows) {
87
+ rows = new Map();
88
+ fullscreenHoverComponentCache.set(container, rows);
89
+ }
90
+ const key = `${width}:${row}`;
91
+ if (rows.has(key)) return rows.get(key) ?? null;
92
+ const hit = componentAtLocalRow(container, row, width);
93
+ rows.set(key, hit);
94
+ return hit;
95
+ }
96
+
97
+ /** fullscreen 鼠标悬停目标。 */
98
+ export type FullscreenHoverTarget =
99
+ | { kind: "button" }
100
+ | { kind: "group"; component: ToolGroupComponent }
101
+ | { kind: "assistant"; component: any }
102
+ | {
103
+ kind: "tool";
104
+ component: any;
105
+ view: ExpandedToolIoView | null;
106
+ section: ToolIoSection | null;
107
+ };
108
+
109
+ /** 悬停状态变化才触发渲染(motion 事件密集,状态不变跳过)。 */
110
+ export function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | null): void {
111
+ let changed = false;
112
+ const nextCallId =
113
+ target?.kind === "tool" && !target.component.expanded
114
+ ? (target.component.toolCallId ?? null)
115
+ : null;
116
+ if (nextCallId !== sharedToolHoverState().toolCallId) {
117
+ setHoveredToolCallId(nextCallId);
118
+ changed = true;
119
+ }
120
+ const nextGroup = target?.kind === "group" ? target.component : null;
121
+ if (setHoveredToolGroup(nextGroup)) changed = true;
122
+ const nextAssistant = target?.kind === "assistant" ? target.component : null;
123
+ if (setHoveredCompactAssistant(nextAssistant)) changed = true;
124
+ const nextView = target?.kind === "tool" ? target.view : null;
125
+ const nextSection = target?.kind === "tool" ? target.section : null;
126
+ if (setHoveredToolIo(nextView, nextSection)) changed = true;
127
+ const nextButton = target?.kind === "button";
128
+ if (setScrollButtonHovered(nextButton)) changed = true;
129
+ if (changed) tui.requestRender?.();
130
+ }