@herbertgao/pi-extensions 2026.8.1 → 2026.8.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.
Files changed (162) hide show
  1. package/README.md +5 -2
  2. package/node_modules/@czottmann/pi-automode/README.md +16 -2
  3. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +35 -0
  4. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +18 -9
  5. package/node_modules/@czottmann/pi-automode/docs/defaults.md +6 -2
  6. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +6 -0
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +11 -7
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +100 -0
  9. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +32 -0
  10. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +78 -3
  11. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +16 -4
  12. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +23 -1
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +22 -1
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +18 -2
  15. package/node_modules/@czottmann/pi-automode/package.json +4 -4
  16. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +5 -0
  17. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +5 -0
  18. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +5 -0
  19. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +5 -0
  20. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +5 -0
  21. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +5 -0
  22. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +23 -0
  23. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +99 -0
  24. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +30 -0
  25. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +16 -0
  26. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +32 -0
  27. package/node_modules/@dietrichgebert/ponytail/LICENSE +21 -0
  28. package/node_modules/@dietrichgebert/ponytail/README.es.md +298 -0
  29. package/node_modules/@dietrichgebert/ponytail/README.ko.md +315 -0
  30. package/node_modules/@dietrichgebert/ponytail/README.md +351 -0
  31. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +21 -0
  32. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +62 -0
  33. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  34. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +115 -0
  35. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +27 -0
  36. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +27 -0
  37. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  38. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  39. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  40. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  41. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
  42. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +44 -0
  43. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +21 -0
  44. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +96 -0
  45. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +169 -0
  46. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +98 -0
  47. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +130 -0
  48. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +85 -0
  49. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.ps1 +24 -0
  50. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.sh +18 -0
  51. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +77 -0
  52. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +26 -0
  53. package/node_modules/@dietrichgebert/ponytail/package.json +47 -0
  54. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +211 -0
  55. package/node_modules/@dietrichgebert/ponytail/pi-extension/package.json +8 -0
  56. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +60 -0
  57. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +120 -0
  58. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +41 -0
  59. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  60. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +50 -0
  61. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +71 -0
  62. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +57 -0
  63. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -3
  64. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -3
  65. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +10 -6
  66. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary.ts +6 -2
  67. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -83
  68. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +1 -1
  69. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/markdown-enhance.ts +119 -0
  70. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +73 -9
  71. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +2 -2
  72. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-hover.ts +130 -0
  73. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-interaction.ts +65 -542
  74. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-layout.ts +160 -0
  75. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-packets.ts +143 -0
  76. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-scroll.ts +184 -0
  77. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-diff/diff-renderer.ts +2 -2
  78. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-grouping.ts +9 -2
  79. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +91 -0
  80. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/agent-summary.ts +1 -1
  81. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  82. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -0
  83. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
  84. package/node_modules/@herbertgao/pi-stash/README.md +9 -9
  85. package/node_modules/@herbertgao/pi-stash/package.json +1 -1
  86. package/node_modules/@herbertgao/pi-stash/src/index.ts +2 -1
  87. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
  88. package/node_modules/@herbertgao/pi-subagents/README.md +6 -2
  89. package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
  90. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +85 -11
  91. package/node_modules/@herbertgao/pi-subagents/src/index.ts +28 -4
  92. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +12 -0
  93. package/node_modules/@herbertgao/pi-subagents/src/types.ts +2 -0
  94. package/node_modules/@narumitw/pi-btw/LICENSE +21 -0
  95. package/node_modules/@narumitw/pi-btw/README.md +210 -0
  96. package/node_modules/@narumitw/pi-btw/package.json +53 -0
  97. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +632 -0
  98. package/node_modules/@narumitw/pi-btw/src/btw.ts +996 -0
  99. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +339 -0
  100. package/node_modules/@narumitw/pi-btw/src/index.ts +1 -0
  101. package/node_modules/@narumitw/pi-btw/src/menu.ts +296 -0
  102. package/node_modules/@narumitw/pi-btw/src/settings.ts +237 -0
  103. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +238 -0
  104. package/node_modules/@narumitw/pi-btw/src/text.ts +10 -0
  105. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +620 -0
  106. package/node_modules/@pi-plugins/fast-mode/LICENSE +21 -0
  107. package/node_modules/@pi-plugins/fast-mode/README.md +92 -0
  108. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts +6 -0
  109. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -0
  110. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +2026 -0
  111. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -0
  112. package/node_modules/@pi-plugins/fast-mode/package.json +73 -0
  113. package/node_modules/pi-mcp-adapter/CHANGELOG.md +59 -0
  114. package/node_modules/pi-mcp-adapter/README.md +62 -13
  115. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +377 -0
  116. package/node_modules/pi-mcp-adapter/commands.ts +9 -7
  117. package/node_modules/pi-mcp-adapter/config.ts +110 -49
  118. package/node_modules/pi-mcp-adapter/direct-tools.ts +34 -19
  119. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +37 -36
  120. package/node_modules/pi-mcp-adapter/errors.ts +21 -17
  121. package/node_modules/pi-mcp-adapter/host-html-template.ts +13 -7
  122. package/node_modules/pi-mcp-adapter/index.ts +37 -14
  123. package/node_modules/pi-mcp-adapter/init.ts +9 -7
  124. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +2 -2
  125. package/node_modules/pi-mcp-adapter/lifecycle.ts +8 -8
  126. package/node_modules/pi-mcp-adapter/logger.ts +1 -1
  127. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +36 -22
  128. package/node_modules/pi-mcp-adapter/mcp-auth.ts +120 -2
  129. package/node_modules/pi-mcp-adapter/mcp-code.ts +13 -11
  130. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +19 -13
  131. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +15 -11
  132. package/node_modules/pi-mcp-adapter/mcp-panel.ts +34 -15
  133. package/node_modules/pi-mcp-adapter/mcp-probe.ts +131 -34
  134. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +2 -2
  135. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +7 -4
  136. package/node_modules/pi-mcp-adapter/mcp-trace.ts +59 -64
  137. package/node_modules/pi-mcp-adapter/metadata-cache.ts +34 -26
  138. package/node_modules/pi-mcp-adapter/npx-resolver.ts +6 -4
  139. package/node_modules/pi-mcp-adapter/oauth-handler.ts +1 -1
  140. package/node_modules/pi-mcp-adapter/onboarding-state.ts +19 -11
  141. package/node_modules/pi-mcp-adapter/package.json +13 -8
  142. package/node_modules/pi-mcp-adapter/panel-keys.ts +20 -4
  143. package/node_modules/pi-mcp-adapter/prompts.ts +2 -2
  144. package/node_modules/pi-mcp-adapter/proxy-modes.ts +48 -11
  145. package/node_modules/pi-mcp-adapter/sampling-handler.ts +22 -15
  146. package/node_modules/pi-mcp-adapter/search-ranking.ts +2 -1
  147. package/node_modules/pi-mcp-adapter/server-manager.ts +165 -113
  148. package/node_modules/pi-mcp-adapter/session-recovery.ts +11 -12
  149. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +3 -3
  150. package/node_modules/pi-mcp-adapter/state.ts +3 -1
  151. package/node_modules/pi-mcp-adapter/tool-approval.ts +67 -4
  152. package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -5
  153. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +14 -2
  154. package/node_modules/pi-mcp-adapter/ts-shape.ts +9 -2
  155. package/node_modules/pi-mcp-adapter/types.ts +109 -22
  156. package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +40 -0
  157. package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +28 -19
  158. package/node_modules/pi-mcp-adapter/ui-server.ts +90 -58
  159. package/node_modules/pi-mcp-adapter/ui-session.ts +16 -14
  160. package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +4 -4
  161. package/node_modules/pi-mcp-adapter/utils.ts +23 -9
  162. package/package.json +22 -7
@@ -1,4 +1,3 @@
1
- import { getKeybindings, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
1
  import { hasActiveTextPreview, showTextPreview } from "../feature/context.ts";
3
2
  import { ToolGroupComponent } from "./tool-grouping.ts";
4
3
  import { isCompactAssistantComponent, setHoveredCompactAssistant } from "./compact-mode.ts";
@@ -8,19 +7,57 @@ import { setToolTuiFullscreen } from "./show-more-hint.ts";
8
7
  import {
9
8
  type ExpandedToolIoView,
10
9
  getActiveIoViewFrame,
11
- invalidateIoView,
12
10
  isExpandedToolIoView,
13
11
  type IoViewFrameState,
14
12
  setActiveIoViewFrame,
15
13
  type ToolIoSection,
16
14
  } from "./tool-result.ts";
17
-
18
- type SgrMousePacket = {
19
- code: number;
20
- col: number;
21
- row: number;
22
- final: "M" | "m";
23
- };
15
+ import {
16
+ collectToolComponents,
17
+ extractToolFramePlacements,
18
+ isSgrLeftPress,
19
+ isToolExecutionComponent,
20
+ parseSgrMousePackets,
21
+ stripTerminalSequences,
22
+ stripTerminalSequencesPreservingLayout,
23
+ toolFrameMarker,
24
+ type FrameToolPlacement,
25
+ type SgrMousePacket,
26
+ } from "./mouse-packets.ts";
27
+ import {
28
+ collectFullscreenToolCards,
29
+ componentAtLocalRow,
30
+ collapsedHintHitbox,
31
+ fullscreenContentWidth,
32
+ fullscreenLeafAt,
33
+ isScrollbarColumnAt,
34
+ } from "./mouse-layout.ts";
35
+ import {
36
+ fullscreenLazyTui,
37
+ hideScrollButton,
38
+ isScrollBottomInput,
39
+ renderScrollButton,
40
+ resetScrollButtonState,
41
+ scheduleScrollButtonSync,
42
+ setScrollButtonHovered,
43
+ setScrollButtonVisible,
44
+ setScrollButtonWidget,
45
+ setToolMouseTui,
46
+ scrollButtonVisible,
47
+ scrollButtonWidget,
48
+ toolMouseInteractionActive,
49
+ toolMouseTui,
50
+ updateScrollButtonFromInput,
51
+ } from "./mouse-scroll.ts";
52
+ import {
53
+ applyFullscreenHover,
54
+ cachedFullscreenComponentAtRow,
55
+ sharedToolHoverState,
56
+ setHoveredToolCallId,
57
+ setHoveredToolGroup,
58
+ setHoveredToolIo,
59
+ type FullscreenHoverTarget,
60
+ } from "./mouse-hover.ts";
24
61
 
25
62
  type FrameToolRender = {
26
63
  component: any;
@@ -28,17 +65,6 @@ type FrameToolRender = {
28
65
  contentBoxLines: number;
29
66
  };
30
67
 
31
- /** Final painted placement of one outermost tool/group row after parent layout. */
32
- type FrameToolPlacement = {
33
- component: any;
34
- componentRow: number;
35
- lineIndex: number;
36
- /** Marker-stripped final line text as painted after parent layout. */
37
- finalLine: string;
38
- view?: ExpandedToolIoView;
39
- section?: ToolIoSection;
40
- };
41
-
42
68
  type InteractionRegion = {
43
69
  kind: "collapsed-hint" | "expanded-card" | "show-more" | "scroll-bottom";
44
70
  row: number;
@@ -51,23 +77,14 @@ type InteractionRegion = {
51
77
 
52
78
  type InteractionFrame = { regions: InteractionRegion[] };
53
79
 
54
- /** Zero-width APC row marker (like pi CURSOR_MARKER); stripped before terminal output. */
55
- const TOOL_FRAME_MARKER_RE = /_cc:t(\d+):(\d+)/g;
56
- const TOOL_VIEW_MARKER_RE = /_cc:v(\d+):([io])/g;
57
- const toolFrameMarker = (id: number, row: number) => `_cc:t${id}:${row}`;
58
-
59
80
  const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
60
81
  const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
61
82
  const TOOL_MOUSE_MOTION_DISABLE = "\x1b[?1003l";
62
83
  const FULLSCREEN_MOTION_ENABLED = Symbol("ccstyle.fullscreen-motion-enabled");
63
- const TOOL_HOVER_STATE_KEY = Symbol.for("pi.ccstyle.tool-hover-state");
64
84
  const TOOL_MOUSE_OWNER_KEY = Symbol.for("pi.ccstyle.tool-mouse-owner");
65
85
  const DEFAULT_TOOL_MOUSE_OWNER = {};
66
86
  export const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
67
- const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
68
- const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
69
- const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
70
- export let toolMouseTui: any = null;
87
+
71
88
  let toolMouseUi: any = null;
72
89
  let toolMouseInputUnsubscribe: (() => void) | null = null;
73
90
  let toolMouseRenderPatchTui: any = null;
@@ -78,268 +95,12 @@ let toolMouseRawWrite: ((data: string) => unknown) | null = null;
78
95
  let toolMouseInstallationOwner: object | null = null;
79
96
  let fullscreenMotionTerminal: any = null;
80
97
  let ownsFullscreenMotion = false;
81
- let scrollButtonVisible = false;
82
- let scrollButtonHovered = false;
83
- let scrollButtonWidget: any = null;
84
- let scrollButtonSyncScheduled = false;
85
98
  let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
86
- type SharedToolHoverState = { toolCallId: string | null };
87
- function sharedToolHoverState(): SharedToolHoverState {
88
- const host = globalThis as any;
89
- return (host[TOOL_HOVER_STATE_KEY] ??= { toolCallId: null });
90
- }
91
- export let hoveredToolCallId: string | null = sharedToolHoverState().toolCallId;
92
- function setHoveredToolCallId(toolCallId: string | null): void {
93
- hoveredToolCallId = toolCallId;
94
- sharedToolHoverState().toolCallId = toolCallId;
95
- }
96
- export function isToolCallHovered(toolCallId: string | null | undefined): boolean {
97
- return Boolean(toolCallId && sharedToolHoverState().toolCallId === toolCallId);
98
- }
99
- let hoveredToolGroup: ToolGroupComponent | null = null;
100
99
  let latestInteractionFrame: InteractionFrame = { regions: [] };
101
100
 
102
- function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
103
- const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
104
- const packets: SgrMousePacket[] = [];
105
- let offset = 0;
106
-
107
- for (const match of data.matchAll(pattern)) {
108
- if (match.index !== offset) return null;
109
- offset = match.index + match[0].length;
110
- packets.push({
111
- code: Number(match[1]),
112
- col: Number(match[2]),
113
- row: Number(match[3]),
114
- final: match[4] as "M" | "m",
115
- });
116
- }
117
-
118
- return packets.length > 0 && offset === data.length ? packets : null;
119
- }
120
-
121
- function isSgrLeftPress(packet: SgrMousePacket): boolean {
122
- const baseButton = packet.code & ~(4 | 8 | 16 | 32);
123
- return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
124
- }
125
-
126
- function stripTerminalSequencesPreservingLayout(value: string): string {
127
- return value
128
- .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
129
- .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
130
- }
131
-
132
- function stripTerminalSequences(value: string): string {
133
- return stripTerminalSequencesPreservingLayout(value).replace(/\s+/g, " ").trim();
134
- }
135
-
136
- function isToolExecutionComponent(value: any): boolean {
137
- return Boolean(
138
- value &&
139
- typeof value === "object" &&
140
- typeof value.toolCallId === "string" &&
141
- typeof value.setExpanded === "function" &&
142
- typeof value.render === "function",
143
- );
144
- }
145
-
146
- function collectToolComponents(component: any, tools: any[], seen = new Set<any>()): void {
147
- if (!component || typeof component !== "object" || seen.has(component)) return;
148
- seen.add(component);
149
- if (isToolExecutionComponent(component)) {
150
- tools.push(component);
151
- return;
152
- }
153
- if (Array.isArray(component.children)) {
154
- for (const child of component.children) collectToolComponents(child, tools, seen);
155
- }
156
- try {
157
- const mounted = component.getMountedRoots?.();
158
- if (Array.isArray(mounted)) {
159
- for (const root of mounted) collectToolComponents(root, tools, seen);
160
- }
161
- } catch {
162
- // renderer 切换中的惰性 Proxy 可能暂时没有 mounted roots。
163
- }
164
- }
165
-
166
- function stripToolFrameMarkers(line: string): string {
167
- return line.replace(TOOL_FRAME_MARKER_RE, "").replace(TOOL_VIEW_MARKER_RE, "");
168
- }
169
-
170
- function extractToolFramePlacements(
171
- lines: string[],
172
- idToComponent: Map<number, any>,
173
- idToView: Map<number, ExpandedToolIoView>,
174
- ): { lines: string[]; placements: FrameToolPlacement[] } {
175
- const placements: FrameToolPlacement[] = [];
176
- const cleaned = lines.map((line, lineIndex) => {
177
- const toolMatches = [...line.matchAll(TOOL_FRAME_MARKER_RE)];
178
- const viewMatches = [...line.matchAll(TOOL_VIEW_MARKER_RE)];
179
- const finalLine = stripToolFrameMarkers(line);
180
- let view: ExpandedToolIoView | undefined;
181
- let section: ToolIoSection | undefined;
182
- for (const match of viewMatches) {
183
- const candidate = idToView.get(Number(match[1]));
184
- if (!candidate) continue;
185
- view = candidate;
186
- section = match[2] === "i" ? "input" : "output";
187
- break;
188
- }
189
- for (const match of toolMatches) {
190
- const component = idToComponent.get(Number(match[1]));
191
- if (!component) continue;
192
- placements.push({
193
- component,
194
- componentRow: Number(match[2]),
195
- lineIndex,
196
- finalLine,
197
- view,
198
- section,
199
- });
200
- }
201
- return finalLine;
202
- });
203
- return { lines: cleaned, placements };
204
- }
205
-
206
101
  /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
207
102
  const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
208
103
 
209
- function toolMouseInteractionActive(): boolean {
210
- if (config.mode === "off") return false;
211
- if (isLazyProxyTui(toolMouseTui)) return true;
212
- return true;
213
- }
214
-
215
- function formatShortcut(shortcut: string): string {
216
- return shortcut
217
- .split("+")
218
- .map((part) =>
219
- part.length <= 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`,
220
- )
221
- .join("+");
222
- }
223
-
224
- function isScrollBottomInput(data: string): boolean {
225
- return matchesKey(data, SCROLL_BOTTOM_SHORTCUT);
226
- }
227
-
228
- function wheelDirection(data: string): "up" | "down" | null {
229
- const packets = parseSgrMousePackets(data);
230
- for (const packet of packets ?? []) {
231
- if (packet.final !== "M") continue;
232
- const baseButton = packet.code & ~(4 | 8 | 16 | 32);
233
- if (baseButton === 64) return "up";
234
- if (baseButton === 65) return "down";
235
- }
236
- return null;
237
- }
238
-
239
- function isScrollNavigationInput(data: string): boolean {
240
- if (
241
- matchesKey(data, "pageUp") ||
242
- matchesKey(data, "pageDown") ||
243
- ZENTUI_PAGE_UP_INPUT.test(data) ||
244
- ZENTUI_PAGE_DOWN_INPUT.test(data) ||
245
- // 官方 fullscreen viewport 的可滚动键(half-page/prompt/top/bottom)。
246
- [
247
- "tui.altScreen.pageUp",
248
- "tui.altScreen.pageDown",
249
- "tui.altScreen.halfPageUp",
250
- "tui.altScreen.halfPageDown",
251
- "tui.altScreen.previousPrompt",
252
- "tui.altScreen.nextPrompt",
253
- "tui.altScreen.top",
254
- "tui.altScreen.bottom",
255
- ].some((key) => getKeybindings().matches(data, key))
256
- ) {
257
- return true;
258
- }
259
- const packets = parseSgrMousePackets(data);
260
- return Boolean(
261
- packets?.some((packet) => {
262
- const baseButton = packet.code & ~(4 | 8 | 16 | 32);
263
- return packet.final === "M" && (baseButton === 64 || baseButton === 65);
264
- }),
265
- );
266
- }
267
-
268
- function isAtTranscriptBottom(tui: any): boolean {
269
- // 惰性 Proxy fullscreen:官方 viewport 以 isFollowingOutput 判定是否在底部。
270
- if (fullscreenLazyTui(tui)) return isFullscreenAtBottom(tui);
271
- return true;
272
- }
273
-
274
- function hideScrollButton(tui: any): void {
275
- const changed = scrollButtonVisible || scrollButtonHovered;
276
- scrollButtonVisible = false;
277
- scrollButtonHovered = false;
278
- if (changed) tui.requestRender?.();
279
- }
280
-
281
- function scheduleScrollButtonSync(tui: any, data: string): void {
282
- if (
283
- !fullscreenLazyTui(tui) ||
284
- !toolMouseInteractionActive() ||
285
- !isScrollNavigationInput(data) ||
286
- scrollButtonSyncScheduled
287
- )
288
- return;
289
- scrollButtonSyncScheduled = true;
290
- const previousLines = tui.previousLines;
291
- const check = (attempt: number) => {
292
- scrollButtonSyncScheduled = false;
293
- if (toolMouseTui !== tui) return;
294
- // Pi renders on its own frame timer. Inspect the resulting viewport before
295
- // showing the button so empty or non-scrollable transcripts never flash it.
296
- const rendered = tui.previousLines !== previousLines;
297
- // fullscreen 下 isFollowingOutput 是即时状态,无需等待官方帧渲染。
298
- if (!rendered && attempt < 4 && !fullscreenLazyTui(tui)) {
299
- scrollButtonSyncScheduled = true;
300
- const timer = setTimeout(() => check(attempt + 1), 16);
301
- if (typeof timer === "object" && timer !== null && "unref" in timer) {
302
- (timer as { unref: () => void }).unref();
303
- }
304
- return;
305
- }
306
- const nextVisible = !isAtTranscriptBottom(tui);
307
- if (nextVisible !== scrollButtonVisible) {
308
- scrollButtonVisible = nextVisible;
309
- tui.requestRender?.();
310
- }
311
- };
312
- process.nextTick(() => check(0));
313
- }
314
-
315
- function updateScrollButtonFromInput(tui: any, data: string): void {
316
- if (!fullscreenLazyTui(tui) || !toolMouseInteractionActive()) return;
317
- if (matchesKey(data, "enter") || matchesKey(data, "return")) hideScrollButton(tui);
318
- }
319
-
320
- function renderComponentTree(component: any, width: number): string[] {
321
- if (!component || typeof component !== "object") return [];
322
- try {
323
- const lines = component.render?.(width);
324
- if (Array.isArray(lines) && lines.length > 0) return lines;
325
- } catch {
326
- // Fall through to children for hidden container renderers.
327
- }
328
- if (!Array.isArray(component.children)) return [];
329
- return component.children.flatMap((child: any) => renderComponentTree(child, width));
330
- }
331
-
332
- let hoveredToolIoView: ExpandedToolIoView | null = null;
333
- let hoveredToolIoSection: ToolIoSection | null = null;
334
-
335
- function collapsedHintHitbox(line: string): { startCol: number; endCol: number } | null {
336
- const plain = stripTerminalSequencesPreservingLayout(line);
337
- const match = /(\([^()\n]* \/ click\)|click to show more|to show more)(?=\)?\s*$)/.exec(plain);
338
- if (!match?.[1]) return null;
339
- const startCol = visibleWidth(plain.slice(0, match.index)) + 1;
340
- return { startCol, endCol: startCol + visibleWidth(match[1]) - 1 };
341
- }
342
-
343
104
  function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
344
105
  const matches = latestInteractionFrame.regions.filter(
345
106
  (region) =>
@@ -369,41 +130,11 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
369
130
  return true;
370
131
  }
371
132
 
372
- export function setHoveredToolIo(
373
- view: ExpandedToolIoView | null,
374
- section: ToolIoSection | null,
375
- ): boolean {
376
- // resultRendererComponent 可能是 Text/第三方 renderer;reload 后也可能残留旧实例。
377
- const nextView = isExpandedToolIoView(view) ? view : null;
378
- const nextSection = nextView ? section : null;
379
- if (nextView === hoveredToolIoView && nextSection === hoveredToolIoSection) return false;
380
- if (isExpandedToolIoView(hoveredToolIoView)) {
381
- hoveredToolIoView.setHoveredSection(null);
382
- invalidateIoView(hoveredToolIoView);
383
- }
384
- hoveredToolIoView = nextView;
385
- hoveredToolIoSection = nextSection;
386
- if (nextView) {
387
- nextView.setHoveredSection(nextSection);
388
- invalidateIoView(nextView);
389
- }
390
- return true;
391
- }
392
-
393
- export function setHoveredToolGroup(group: ToolGroupComponent | null): boolean {
394
- if (group === hoveredToolGroup) return false;
395
- hoveredToolGroup?.setHintHovered(false);
396
- hoveredToolGroup = group;
397
- group?.setHintHovered(true);
398
- return true;
399
- }
400
-
401
133
  function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
402
134
  if ((packet.code & 32) === 0 || packet.final !== "M") return;
403
135
  const region = interactionRegionAt(packet);
404
136
  const nextScrollButtonHovered = region?.kind === "scroll-bottom";
405
- const scrollButtonChanged = nextScrollButtonHovered !== scrollButtonHovered;
406
- scrollButtonHovered = nextScrollButtonHovered;
137
+ const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
407
138
  const component = region?.component;
408
139
  const nextToolCallId = region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
409
140
  const nextGroup = component instanceof ToolGroupComponent ? component : null;
@@ -446,22 +177,6 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
446
177
  return true;
447
178
  }
448
179
 
449
- function renderScrollButton(width: number, theme: any): string[] {
450
- if (!scrollButtonVisible || !fullscreenLazyTui(toolMouseTui)) return [];
451
- const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
452
- const label = theme.fg(
453
- scrollButtonHovered ? "text" : "accent",
454
- `[ ↓ Back to bottom · ${shortcut} ]`,
455
- );
456
- const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
457
- return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
458
- }
459
-
460
- /** 惰性 Proxy 官方 fullscreen(TuiAltScreen)判定。 */
461
- function fullscreenLazyTui(tui: any): boolean {
462
- return isLazyProxyTui(tui) && tui.mode === "fullscreen";
463
- }
464
-
465
180
  function officialFullscreenHasAllMotion(): boolean {
466
181
  const term = process.env.TERM?.toLowerCase() ?? "";
467
182
  return !(
@@ -522,141 +237,9 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
522
237
  }
523
238
  }
524
239
 
525
- /** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
526
- function isFullscreenAtBottom(tui: any): boolean {
527
- const following = tui.isFollowingOutput ?? tui.getPrimaryScrollView?.()?.isFollowingEnd ?? true;
528
- return Boolean(following);
529
- }
530
-
531
240
  const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
532
241
  const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol("ccstyle.fullscreen-wheel-scroll-original");
533
242
 
534
- /** 布局树点查询:返回 (x,y) 处最深含行 leaf box(屏幕行 → 组件局部行)。 */
535
- function fullscreenLeafAt(
536
- layout: any,
537
- x: number,
538
- y: number,
539
- ): { box: any; localRow: number } | null {
540
- const root = layout?.root;
541
- if (!root) return null;
542
- let best: { box: any; localRow: number } | null = null;
543
- let bestDepth = -1;
544
- const visit = (box: any, depth: number) => {
545
- if (!box) return;
546
- const clip = box.clip;
547
- if (!clip || x < clip.x || x >= clip.x + clip.width || y < clip.y || y >= clip.y + clip.height)
548
- return;
549
- const isLeaf = !Array.isArray(box.children) || box.children.length === 0;
550
- if (
551
- isLeaf &&
552
- y >= box.rect.y &&
553
- y < box.rect.y + Math.max(1, box.rect.height) &&
554
- depth > bestDepth
555
- ) {
556
- best = { box, localRow: Math.max(0, y - box.rect.y) };
557
- bestDepth = depth;
558
- }
559
- for (const child of box.children ?? []) visit(child, depth + 1);
560
- };
561
- visit(root, 0);
562
- return best;
563
- }
564
-
565
- /** leaf 自身不带 scrollView;内容宽度由最近的 scroll 祖先决定。 */
566
- function fullscreenContentWidth(box: any, terminalWidth: number): number {
567
- for (let current = box; current; current = current.parent) {
568
- if (typeof current.scrollView?.getContentWidth === "function") {
569
- return Math.max(1, current.scrollView.getContentWidth(terminalWidth));
570
- }
571
- }
572
- return terminalWidth;
573
- }
574
-
575
- /** 点击列是否为官方滚动条列(放行官方拖动)。 */
576
- function isScrollbarColumnAt(layout: any, x: number): boolean {
577
- let hit = false;
578
- const visit = (box: any) => {
579
- if (hit || !box) return;
580
- if (box.scrollView?.isScrollbarVisible && x === box.rect.x + box.rect.width - 1) {
581
- hit = true;
582
- return;
583
- }
584
- for (const child of box.children ?? []) visit(child);
585
- };
586
- visit(layout?.root);
587
- return hit;
588
- }
589
-
590
- type ComponentRowHit = {
591
- component: any;
592
- row: number;
593
- /** 内部工具命中时所属的展开 group;普通卡点击仍折叠整个 group。 */
594
- group?: ToolGroupComponent;
595
- };
596
-
597
- /**
598
- * 布局 leaf box 的组件通常是容器(documentContainer/dock 容器等),工具卡与
599
- * widget 在其 children 内。按局部行遍历组件树,定位实际命中的子组件。
600
- */
601
- function componentAtLocalRow(
602
- component: any,
603
- localRow: number,
604
- width: number,
605
- ): ComponentRowHit | null {
606
- if (component instanceof ToolGroupComponent) {
607
- // 展开的 group:头两行(空行 + 头行)归 group,其余行映射到内部工具。
608
- const child = component.childAtRow(localRow, width);
609
- return child ? { ...child, group: component } : { component, row: localRow };
610
- }
611
- if (isToolExecutionComponent(component)) {
612
- return { component, row: localRow };
613
- }
614
- if (isCompactAssistantComponent(component)) {
615
- // compact 摘要行整体作为可展开卡片(折叠摘要 / 展开内容)。
616
- return { component, row: localRow };
617
- }
618
- if (component === scrollButtonWidget) {
619
- return { component, row: localRow };
620
- }
621
- if (!Array.isArray(component.children)) return null;
622
- let offset = 0;
623
- for (const child of component.children) {
624
- let lines: string[] = [];
625
- try {
626
- const rendered = child.render?.(width);
627
- if (Array.isArray(rendered)) lines = rendered.map((line) => String(line));
628
- } catch {
629
- lines = [];
630
- }
631
- if (localRow < offset + lines.length) {
632
- return (
633
- componentAtLocalRow(child, localRow - offset, width) ?? {
634
- component: child,
635
- row: localRow - offset,
636
- }
637
- );
638
- }
639
- offset += lines.length;
640
- }
641
- return null;
642
- }
643
-
644
- /** fullscreen single-expand:group 作为整体,不继续递归其内部工具。 */
645
- function collectFullscreenToolCards(component: any, out: any[], seen = new Set<any>()): void {
646
- if (!component || typeof component !== "object" || seen.has(component)) return;
647
- seen.add(component);
648
- if (
649
- isToolExecutionComponent(component) ||
650
- component instanceof ToolGroupComponent ||
651
- isCompactAssistantComponent(component)
652
- ) {
653
- out.push(component);
654
- return;
655
- }
656
- if (!Array.isArray(component.children)) return;
657
- for (const child of component.children) collectFullscreenToolCards(child, out, seen);
658
- }
659
-
660
243
  /**
661
244
  * 官方 fullscreen 工具卡点击:collapsed hint 点击展开
662
245
  * (有且仅保持一个展开:展开前收起其他工具卡),expanded 整卡二次点击收起,
@@ -740,47 +323,8 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
740
323
  return true;
741
324
  }
742
325
 
743
- /** hover 与点击共用组件定位;同一布局下按容器/宽度/行缓存,避免 motion 重复渲染。 */
744
- let fullscreenHoverCacheLayout: unknown = null;
745
- let fullscreenHoverComponentCache = new WeakMap<object, Map<string, ComponentRowHit | null>>();
746
-
747
- function cachedFullscreenComponentAtRow(
748
- layout: any,
749
- container: any,
750
- row: number,
751
- width: number,
752
- ): ComponentRowHit | null {
753
- if (!container || typeof container !== "object") return null;
754
- if (fullscreenHoverCacheLayout !== layout) {
755
- fullscreenHoverCacheLayout = layout;
756
- fullscreenHoverComponentCache = new WeakMap();
757
- }
758
- let rows = fullscreenHoverComponentCache.get(container);
759
- if (!rows) {
760
- rows = new Map();
761
- fullscreenHoverComponentCache.set(container, rows);
762
- }
763
- const key = `${width}:${row}`;
764
- if (rows.has(key)) return rows.get(key) ?? null;
765
- const hit = componentAtLocalRow(container, row, width);
766
- rows.set(key, hit);
767
- return hit;
768
- }
769
-
770
- /** fullscreen 鼠标悬停目标。 */
771
- type FullscreenHoverTarget =
772
- | { kind: "button" }
773
- | { kind: "group"; component: ToolGroupComponent }
774
- | { kind: "assistant"; component: any }
775
- | {
776
- kind: "tool";
777
- component: any;
778
- view: ExpandedToolIoView | null;
779
- section: ToolIoSection | null;
780
- };
781
-
782
326
  /**
783
- * fullscreen 悬停高亮:collapsed 卡 [click to show more] hint、
327
+ * fullscreen 鼠标悬停:collapsed 卡 [click to show more] hint、
784
328
  * expanded 卡截断头 show-more、回到底部按钮。motion 不 consume,官方链照常。
785
329
  */
786
330
  function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
@@ -846,32 +390,6 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
846
390
  applyFullscreenHover(tui, target);
847
391
  }
848
392
 
849
- /** 悬停状态变化才触发渲染(motion 事件密集,状态不变跳过)。 */
850
- function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | null): void {
851
- let changed = false;
852
- const nextCallId =
853
- target?.kind === "tool" && !target.component.expanded
854
- ? (target.component.toolCallId ?? null)
855
- : null;
856
- if (nextCallId !== sharedToolHoverState().toolCallId) {
857
- setHoveredToolCallId(nextCallId);
858
- changed = true;
859
- }
860
- const nextGroup = target?.kind === "group" ? target.component : null;
861
- if (setHoveredToolGroup(nextGroup)) changed = true;
862
- const nextAssistant = target?.kind === "assistant" ? target.component : null;
863
- if (setHoveredCompactAssistant(nextAssistant)) changed = true;
864
- const nextView = target?.kind === "tool" ? target.view : null;
865
- const nextSection = target?.kind === "tool" ? target.section : null;
866
- if (setHoveredToolIo(nextView, nextSection)) changed = true;
867
- const nextButton = target?.kind === "button";
868
- if (nextButton !== scrollButtonHovered) {
869
- scrollButtonHovered = nextButton;
870
- changed = true;
871
- }
872
- if (changed) tui.requestRender?.();
873
- }
874
-
875
393
  /**
876
394
  * 实例级包装 TuiAltScreen.handleViewportInput(惰性 Proxy 安全):
877
395
  * 原型方法取 original(绕开 proxy 函数包装),实例 own property 装 wrapper
@@ -1276,11 +794,8 @@ export function teardownToolMouseInteraction(
1276
794
  }
1277
795
  restoreToolMouseRenderPatch();
1278
796
  restoreFullscreenViewportInput(toolMouseTui);
1279
- scrollButtonVisible = false;
1280
- scrollButtonHovered = false;
1281
- scrollButtonWidget = null;
1282
- scrollButtonSyncScheduled = false;
1283
- toolMouseTui = null;
797
+ resetScrollButtonState();
798
+ setToolMouseTui(null);
1284
799
  toolMouseUi = null;
1285
800
  if (host[TOOL_MOUSE_OWNER_KEY] === owner) delete host[TOOL_MOUSE_OWNER_KEY];
1286
801
  if (toolMouseInstallationOwner === owner) toolMouseInstallationOwner = null;
@@ -1290,8 +805,8 @@ export function teardownToolMouseInteraction(
1290
805
  export function resetToolHoverState(): void {
1291
806
  setHoveredToolCallId(null);
1292
807
  setHoveredCompactAssistant(null);
1293
- scrollButtonVisible = false;
1294
- scrollButtonHovered = false;
808
+ setScrollButtonVisible(false);
809
+ setScrollButtonHovered(false);
1295
810
  releaseFullscreenToolMouseMotion(toolMouseTui);
1296
811
  }
1297
812
 
@@ -1311,19 +826,19 @@ export function installToolMouseInteraction(
1311
826
  // 0.84+ 的 tui 是惰性 Proxy:regular 保留原生 scrollback;fullscreen
1312
827
  // 由官方 LayoutFrame 命中,并由扩展补齐 hover 所需的 all-motion 上报。
1313
828
  ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
1314
- toolMouseTui = tui;
829
+ setToolMouseTui(tui);
1315
830
  setToolTuiFullscreen(fullscreenLazyTui(tui));
1316
831
  if (isLazyProxyTui(tui)) {
1317
832
  patchFullscreenViewportInput(tui);
1318
833
  ensureFullscreenToolMouseMotion(tui);
1319
- scrollButtonWidget = {
834
+ setScrollButtonWidget({
1320
835
  render: (width: number) => {
1321
836
  patchFullscreenViewportInput(tui);
1322
837
  ensureFullscreenToolMouseMotion(tui);
1323
838
  return renderScrollButton(width, theme);
1324
839
  },
1325
840
  invalidate() {},
1326
- };
841
+ });
1327
842
  return scrollButtonWidget;
1328
843
  }
1329
844
  // Wrap doRender to capture the live frame for tool click/hover mapping.
@@ -1333,7 +848,7 @@ export function installToolMouseInteraction(
1333
848
  render: (width: number) => renderScrollButton(width, theme),
1334
849
  invalidate() {},
1335
850
  };
1336
- scrollButtonWidget = widget;
851
+ setScrollButtonWidget(widget);
1337
852
  return widget;
1338
853
  });
1339
854
  toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
@@ -1361,3 +876,11 @@ export function scheduleSessionRender(refresh?: () => void): void {
1361
876
  tui.requestRender(true);
1362
877
  }, 0);
1363
878
  }
879
+
880
+ export { toolMouseTui } from "./mouse-scroll.ts";
881
+ export {
882
+ hoveredToolCallId,
883
+ isToolCallHovered,
884
+ setHoveredToolGroup,
885
+ setHoveredToolIo,
886
+ } from "./mouse-hover.ts";