pi-cc-extensions 0.8.68 → 0.8.69

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
@@ -61,8 +61,6 @@ Run `/reload` after installation.
61
61
  "editDiffCollapsedLines": 24, // Edit collapse lines; beyond that shows the expand hint
62
62
  "writeDiffCollapsedLines": 0, // write collapse lines; 0 = creation summary only
63
63
  "diffWordWrap": true, // wrap long diff lines
64
- "expandedPreviewMaxLines": 40, // max lines for expanded bodies
65
- "inputClip": 100, // tool summary path/command clip length
66
64
 
67
65
  // thinking
68
66
  "useSummaryTitlesAsThinkingTitle": true, // use latest summary as thinking title
@@ -71,6 +69,10 @@ Run `/reload` after installation.
71
69
  "dimThinkingText": false, // dim thinking body text
72
70
 
73
71
  // ui
72
+ "expandedInputMaxLines": 5, // expanded tool Input lines; overflow shows a footer hint
73
+ "expandedOutputMaxLines": 10, // expanded tool Output lines; overflow shows a footer hint
74
+ "expandedPreviewMaxLines": 40, // max lines for expanded diff/TaskList bodies
75
+ "inputClip": 100, // tool summary path/command clip length
74
76
  "showStartupHeader": true, // startup header (logo + tips) toggle
75
77
  "scrollStepLines": 3, // fullscreen wheel scroll step
76
78
 
@@ -84,7 +86,7 @@ Run `/reload` after installation.
84
86
  }
85
87
  ```
86
88
 
87
- > **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. Double-click an expanded panel to collapse it.
89
+ > **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. When expanded Input/Output exceeds the line cap, the footer `… +N more lines • click to show more` opens a full preview. Double-click an expanded panel to collapse it.
88
90
  >
89
91
  > **Tip**: 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.
90
92
 
package/README.md CHANGED
@@ -61,8 +61,6 @@ pi install git:github.com/minuque/pi-cc-extensions
61
61
  "editDiffCollapsedLines": 24, // Edit 折叠行数,超出显示展开提示
62
62
  "writeDiffCollapsedLines": 0, // write 折叠行数,0 仅显示创建摘要
63
63
  "diffWordWrap": true, // 长 diff 行换行
64
- "expandedPreviewMaxLines": 40, // 展开正文最大行数
65
- "inputClip": 100, // 工具摘要 path/command 折叠字符数
66
64
 
67
65
  // thinking
68
66
  "useSummaryTitlesAsThinkingTitle": true, // 用最新摘要作思考标题
@@ -71,6 +69,10 @@ pi install git:github.com/minuque/pi-cc-extensions
71
69
  "dimThinkingText": false, // thinking 正文用 dim 色
72
70
 
73
71
  // ui
72
+ "expandedInputMaxLines": 5, // 展开工具卡 Input 可见行数,超出在末行显示展开提示
73
+ "expandedOutputMaxLines": 10, // 展开工具卡 Output 可见行数,超出在末行显示展开提示
74
+ "expandedPreviewMaxLines": 40, // 展开 diff/TaskList 正文最大行数
75
+ "inputClip": 100, // 工具摘要 path/command 折叠字符数
74
76
  "showStartupHeader": true, // 启动头(logo + tips)开关
75
77
  "scrollStepLines": 3, // fullscreen 滚轮步进
76
78
 
@@ -84,7 +86,7 @@ pi install git:github.com/minuque/pi-cc-extensions
84
86
  }
85
87
  ```
86
88
 
87
- > **Fullscreen**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要,双击展开面板收起。
89
+ > **Fullscreen**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;展开后 Input/Output 超行时,末行 `… +N more lines • click to show more` 打开全量预览。双击展开面板收起。
88
90
  >
89
91
  > **建议**:`markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
90
92
 
@@ -5,9 +5,6 @@ import { join } from "node:path";
5
5
 
6
6
  export type CompactStyleMode = "on" | "compact" | "off";
7
7
 
8
- // ── diff 显示配置:作为配置 schema 的一部分由 config 层拥有 ──
9
- // renderer(diff)从 config 导入并 re-export,避免 config → renderer 的反向依赖。
10
-
11
8
  export type DiffViewMode = "auto" | "split" | "unified";
12
9
  export type DiffIndicatorMode = "bars" | "classic" | "none";
13
10
 
@@ -52,6 +49,8 @@ export type Config = {
52
49
  writeDiffCollapsedLines: number;
53
50
  diffWordWrap: boolean;
54
51
  expandedPreviewMaxLines: number;
52
+ expandedInputMaxLines: number;
53
+ expandedOutputMaxLines: number;
55
54
  inputClip: number;
56
55
  useSummaryTitlesAsThinkingTitle: boolean;
57
56
  previewLines: number;
@@ -78,6 +77,10 @@ export const DIFF_COLLAPSED_LINES_VALUES = ["12", "24", "36", "48", "80", "120"]
78
77
  export const WRITE_DIFF_COLLAPSED_LINES_VALUES = ["0", "4", "8", "12", "24", "36"];
79
78
  /** Presets for expanded body height — keep low options first so cycling stays TUI-friendly. */
80
79
  export const EXPANDED_PREVIEW_MAX_LINES_VALUES = ["40", "60", "80", "120", "200", "500", "2000"];
80
+ /** 展开工具卡 Input 可见行数预设。 */
81
+ export const EXPANDED_INPUT_MAX_LINES_VALUES = ["5", "10", "20", "40", "80"];
82
+ /** 展开工具卡 Output 可见行数预设。 */
83
+ export const EXPANDED_OUTPUT_MAX_LINES_VALUES = ["10", "20", "40", "80", "120"];
81
84
  /** 工具摘要里 path/command 等输入的折叠字符数。 */
82
85
  export const INPUT_CLIP_VALUES = ["40", "60", "80", "100", "120", "160"];
83
86
  export const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
@@ -107,6 +110,8 @@ export const DEFAULT_CONFIG: Config = {
107
110
  writeDiffCollapsedLines: DEFAULT_TOOL_DISPLAY_CONFIG.writeDiffCollapsedLines,
108
111
  diffWordWrap: DEFAULT_TOOL_DISPLAY_CONFIG.diffWordWrap,
109
112
  expandedPreviewMaxLines: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
113
+ expandedInputMaxLines: 5,
114
+ expandedOutputMaxLines: 10,
110
115
  inputClip: 100,
111
116
  useSummaryTitlesAsThinkingTitle: true,
112
117
  previewLines: 3,
@@ -141,16 +146,7 @@ export function pickPositiveNumber(value: unknown, fallback: number, min = 1): n
141
146
 
142
147
  export function normalizeConfig(input: unknown): Config {
143
148
  const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
144
- const mode = source.mode;
145
- // 旧 `enabled: boolean` 配置迁移;compact 已恢复为受支持模式,不再回退 on。
146
- const migratedMode: CompactStyleMode =
147
- mode === "on" || mode === "compact" || mode === "off"
148
- ? mode
149
- : typeof source.enabled === "boolean"
150
- ? source.enabled
151
- ? "on"
152
- : "off"
153
- : "on";
149
+ const mode = pickEnum(source.mode, ["on", "compact", "off"], DEFAULT_CONFIG.mode);
154
150
  const excludeRenderers = Array.isArray(source.excludeRenderers)
155
151
  ? [
156
152
  ...new Set(
@@ -161,7 +157,7 @@ export function normalizeConfig(input: unknown): Config {
161
157
  ]
162
158
  : [];
163
159
  return {
164
- mode: migratedMode,
160
+ mode,
165
161
  excludeRenderers,
166
162
  diffViewMode: pickEnum(source.diffViewMode, DIFF_VIEW_MODES, DEFAULT_CONFIG.diffViewMode),
167
163
  diffIndicatorMode: pickEnum(
@@ -176,7 +172,7 @@ export function normalizeConfig(input: unknown): Config {
176
172
  300,
177
173
  ),
178
174
  editDiffCollapsedLines: pickPositiveInt(
179
- source.editDiffCollapsedLines ?? source.diffCollapsedLines,
175
+ source.editDiffCollapsedLines,
180
176
  DEFAULT_CONFIG.editDiffCollapsedLines,
181
177
  1,
182
178
  500,
@@ -194,12 +190,19 @@ export function normalizeConfig(input: unknown): Config {
194
190
  10,
195
191
  50_000,
196
192
  ),
197
- inputClip: pickPositiveInt(
198
- source.inputClip ?? source.toolInputNameLength,
199
- DEFAULT_CONFIG.inputClip,
200
- 8,
201
- 500,
193
+ expandedInputMaxLines: pickPositiveInt(
194
+ source.expandedInputMaxLines,
195
+ DEFAULT_CONFIG.expandedInputMaxLines,
196
+ 1,
197
+ 5_000,
198
+ ),
199
+ expandedOutputMaxLines: pickPositiveInt(
200
+ source.expandedOutputMaxLines,
201
+ DEFAULT_CONFIG.expandedOutputMaxLines,
202
+ 1,
203
+ 5_000,
202
204
  ),
205
+ inputClip: pickPositiveInt(source.inputClip, DEFAULT_CONFIG.inputClip, 8, 500),
203
206
  useSummaryTitlesAsThinkingTitle: source.useSummaryTitlesAsThinkingTitle !== false,
204
207
  previewLines: pickPositiveInt(
205
208
  source.previewLines,
@@ -258,6 +261,8 @@ export function formatConfigStatus(source: Config = config): string {
258
261
  `writeCollapsed=${source.writeDiffCollapsedLines}`,
259
262
  `diffWordWrap=${source.diffWordWrap ? "on" : "off"}`,
260
263
  `expandedMax=${source.expandedPreviewMaxLines}`,
264
+ `expandedInput=${source.expandedInputMaxLines}`,
265
+ `expandedOutput=${source.expandedOutputMaxLines}`,
261
266
  `inputClip=${source.inputClip}`,
262
267
  `thinkingTitle=${source.useSummaryTitlesAsThinkingTitle ? "summary" : "default"}`,
263
268
  `thinkingPreview=${source.previewLines}`,
@@ -282,20 +287,7 @@ function loadConfig(): Config {
282
287
  const source = existsSync(CONFIG_PATH)
283
288
  ? (JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Record<string, unknown>)
284
289
  : {};
285
- const normalized = normalizeConfig(source);
286
- if (
287
- typeof source.enabled === "boolean" &&
288
- source.mode !== "on" &&
289
- source.mode !== "compact" &&
290
- source.mode !== "off"
291
- ) {
292
- try {
293
- writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2));
294
- } catch {
295
- // A read-only config still uses the migrated in-memory value.
296
- }
297
- }
298
- return normalized;
290
+ return normalizeConfig(source);
299
291
  } catch {
300
292
  // Ignore bad config and fall back to defaults.
301
293
  }
@@ -17,6 +17,8 @@ import {
17
17
  DIFF_SPLIT_MIN_WIDTH_VALUES,
18
18
  DIFF_VIEW_MODES,
19
19
  EXCLUDE_RENDERER_CANDIDATES,
20
+ EXPANDED_INPUT_MAX_LINES_VALUES,
21
+ EXPANDED_OUTPUT_MAX_LINES_VALUES,
20
22
  EXPANDED_PREVIEW_MAX_LINES_VALUES,
21
23
  formatExcludeRenderers,
22
24
  getCompactThinkingConfig,
@@ -327,11 +329,31 @@ export async function showCcstylePanel(
327
329
  currentValue: config.diffWordWrap ? "on" : "off",
328
330
  values: ["on", "off"],
329
331
  };
332
+ const expandedInputSetting = {
333
+ id: "expandedInputMaxLines",
334
+ label: "Expanded input lines",
335
+ description:
336
+ "Max Input section lines in an expanded tool card. Overflow shows click to show more. Default 5.",
337
+ currentValue: String(config.expandedInputMaxLines),
338
+ values: [...EXPANDED_INPUT_MAX_LINES_VALUES],
339
+ submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
340
+ buildNumberInputSubmenu(theme, expandedInputSetting, closeSubmenu),
341
+ };
342
+ const expandedOutputSetting = {
343
+ id: "expandedOutputMaxLines",
344
+ label: "Expanded output lines",
345
+ description:
346
+ "Max Output section lines in an expanded tool card. Overflow shows click to show more. Default 10.",
347
+ currentValue: String(config.expandedOutputMaxLines),
348
+ values: [...EXPANDED_OUTPUT_MAX_LINES_VALUES],
349
+ submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
350
+ buildNumberInputSubmenu(theme, expandedOutputSetting, closeSubmenu),
351
+ };
330
352
  const expandedMaxSetting = {
331
353
  id: "expandedPreviewMaxLines",
332
354
  label: "Expanded max lines",
333
355
  description:
334
- "Max Output/diff body lines when expanded. Default 40 keeps the TUI compact; raise for large dumps.",
356
+ "Max diff/TaskList body lines when expanded. Tool Input/Output use the two settings above.",
335
357
  currentValue: String(config.expandedPreviewMaxLines),
336
358
  values: [...EXPANDED_PREVIEW_MAX_LINES_VALUES],
337
359
  submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
@@ -512,6 +534,28 @@ export async function showCcstylePanel(
512
534
  ? "Long diff lines wrap within the panel width."
513
535
  : "Long diff lines are truncated to the panel width.";
514
536
  break;
537
+ case "expandedInputMaxLines":
538
+ updateConfig({
539
+ expandedInputMaxLines: pickPositiveInt(
540
+ value,
541
+ DEFAULT_CONFIG.expandedInputMaxLines,
542
+ 1,
543
+ 5_000,
544
+ ),
545
+ });
546
+ expandedInputSetting.currentValue = String(config.expandedInputMaxLines);
547
+ break;
548
+ case "expandedOutputMaxLines":
549
+ updateConfig({
550
+ expandedOutputMaxLines: pickPositiveInt(
551
+ value,
552
+ DEFAULT_CONFIG.expandedOutputMaxLines,
553
+ 1,
554
+ 5_000,
555
+ ),
556
+ });
557
+ expandedOutputSetting.currentValue = String(config.expandedOutputMaxLines);
558
+ break;
515
559
  case "expandedPreviewMaxLines":
516
560
  updateConfig({
517
561
  expandedPreviewMaxLines: pickPositiveInt(
@@ -580,8 +624,6 @@ export async function showCcstylePanel(
580
624
  diffCollapsedSetting,
581
625
  writeDiffCollapsedSetting,
582
626
  diffWordWrapSetting,
583
- expandedMaxSetting,
584
- inputClipSetting,
585
627
  ],
586
628
  },
587
629
  {
@@ -597,7 +639,14 @@ export async function showCcstylePanel(
597
639
  {
598
640
  id: "ui",
599
641
  label: "UI",
600
- items: [startupHeaderSetting, scrollStepSetting],
642
+ items: [
643
+ expandedInputSetting,
644
+ expandedOutputSetting,
645
+ expandedMaxSetting,
646
+ inputClipSetting,
647
+ startupHeaderSetting,
648
+ scrollStepSetting,
649
+ ],
601
650
  },
602
651
  {
603
652
  id: "feature",
@@ -10,7 +10,6 @@
10
10
 
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { formatDuration } from "../../utils/format.ts";
13
- export { formatDuration };
14
13
 
15
14
  /** 工具分类。 */
16
15
  export type AgentToolCategory = "bash" | "read" | "edit" | "write" | "other";
@@ -114,24 +113,8 @@ function summaryParts(data: AgentSummaryData): string[] {
114
113
  return parts;
115
114
  }
116
115
 
117
- /** 纯文本摘要行:句首大写 + 可选耗时。 */
118
- export function summaryLine(data: AgentSummaryData): string {
119
- const parts = summaryParts(data);
120
- if (parts.length === 0) return "";
121
- const text = parts.join(", ");
122
- const capitalized = text[0].toUpperCase() + text.slice(1);
123
- const duration = formatDuration(data.durationMs);
124
- return duration ? `${capitalized} · ${duration}` : capitalized;
125
- }
126
-
127
- /**
128
- * Markdown 摘要行。
129
- * `box` 为 true:引用块 `> *斜体*`;false:整体加粗。
130
- * `colors`:仅数字染色(success / failed)。
131
- */
132
116
  export function summaryMarkdown(
133
117
  data: AgentSummaryData,
134
- box = false,
135
118
  colors: { success: string; failed: string } = { success: "", failed: "" },
136
119
  ): string {
137
120
  const parts = summaryParts(data);
@@ -148,7 +131,7 @@ export function summaryMarkdown(
148
131
  .join(", ");
149
132
  const duration = formatDuration(data.durationMs);
150
133
  const line = duration ? `${text} · ${duration}` : text;
151
- return box ? `> *${line}*` : `**${line}**`;
134
+ return `> *${line}*`;
152
135
  }
153
136
 
154
137
  /**
@@ -160,8 +143,7 @@ export function summaryMarkdown(
160
143
  export function bindAgentSummary(
161
144
  pi: ExtensionAPI,
162
145
  onSummary: (data: AgentSummaryData) => void,
163
- minToolCount = 2,
164
- ): () => void {
146
+ ): void {
165
147
  let summary = new AgentRunSummary();
166
148
  pi.on("agent_start", async () => {
167
149
  summary = new AgentRunSummary();
@@ -173,9 +155,6 @@ export function bindAgentSummary(
173
155
  summary.recordToolResult(event.isError === true);
174
156
  });
175
157
  pi.on("agent_end", async () => {
176
- if (summary.toolCount >= minToolCount) onSummary(summary.snapshot());
158
+ if (summary.toolCount >= 2) onSummary(summary.snapshot());
177
159
  });
178
- return () => {
179
- summary = new AgentRunSummary();
180
- };
181
160
  }
@@ -16,7 +16,7 @@ export const AGENT_SUMMARY_ENTRY_TYPE = "agent-summary";
16
16
 
17
17
  export default function (pi: ExtensionAPI): void {
18
18
  pi.registerEntryRenderer(AGENT_SUMMARY_ENTRY_TYPE, (entry, _options, theme) => {
19
- const line = summaryMarkdown(entry.data as AgentSummaryData, true, {
19
+ const line = summaryMarkdown(entry.data as AgentSummaryData, {
20
20
  success: theme.getFgAnsi("success"),
21
21
  failed: theme.getFgAnsi("error"),
22
22
  });
@@ -31,9 +31,6 @@ import {
31
31
  patchRegistry,
32
32
  PROTOTYPE_ORIGINAL_KEY,
33
33
  } from "../utils/patch-keys.ts";
34
- // 保持导出兼容:渲染函数已并入 renderer/compact-mode.ts,这里 re-export。
35
- export { animateCompactThinkingText, formatThoughtDuration, styleCompactThinkingText };
36
-
37
34
  // pi-tui 类型声明中 TUI 的 re-export 解析失败,本地用最小结构化类型(只用到 requestRender)。
38
35
  type RenderTui = { requestRender(force?: boolean): void };
39
36
 
@@ -184,6 +184,7 @@ export async function showTextPreview(
184
184
  else if (button === 65) scrollTo(scrollOffset + 3);
185
185
  }
186
186
  },
187
+
187
188
  render(width: number) {
188
189
  const inner = Math.max(1, width - 2);
189
190
  const escWidth = visibleWidth("[esc]");
@@ -26,8 +26,6 @@ const MAX_SESSION_SUGGESTIONS = 3;
26
26
  const MAX_FILE_SUGGESTIONS = 7;
27
27
  const MAX_REFERENCED_SESSIONS = 5;
28
28
  const MENTION_PATTERN = /(?:^|[\t ])@([^\s@]*)$/;
29
- const SUBAGENT_MANAGER_KEY = Symbol.for("pi-subagents:manager");
30
-
31
29
  // ── In-process subagent record tracker ──────────────────────────────
32
30
  // pi-subagents does not expose a global manager; we track records
33
31
  // ourselves by listening to the events it emits.
@@ -53,17 +51,6 @@ type SessionReference = {
53
51
  messages?: unknown[];
54
52
  };
55
53
 
56
- type SubagentRecord = {
57
- id: string;
58
- description?: string;
59
- startedAt?: number;
60
- completedAt?: number;
61
- };
62
-
63
- type SubagentManager = {
64
- getRecord(id: string): SubagentRecord | undefined;
65
- };
66
-
67
54
  // Local subagent record tracking (pi-subagents does not expose a global manager).
68
55
  const liveSubagentRecords = new Map<string, SubagentLiveRecord>();
69
56
  /** Cap retained completed/live records so long-lived pi processes cannot grow unbounded. */
@@ -120,28 +107,6 @@ function trackSubagentFromEvent(data: unknown): void {
120
107
  pruneLiveSubagentRecords();
121
108
  }
122
109
 
123
- function getSubagentManager(): SubagentManager | undefined {
124
- // Try the global manager first (future-proof), fall back to local records.
125
- const manager = (globalThis as Record<PropertyKey, unknown>)[SUBAGENT_MANAGER_KEY] as
126
- | SubagentManager
127
- | undefined;
128
- if (manager && typeof manager.getRecord === "function") return manager;
129
- // If no global manager, use our local tracking.
130
- if (liveSubagentRecords.size === 0) return undefined;
131
- return {
132
- getRecord(id: string): SubagentRecord | undefined {
133
- const record = liveSubagentRecords.get(id);
134
- if (!record) return undefined;
135
- return {
136
- id: record.runId,
137
- description: record.agent || undefined,
138
- startedAt: record.startedAt,
139
- completedAt: record.completedAt,
140
- };
141
- },
142
- };
143
- }
144
-
145
110
  function extractMentionQuery(textBeforeCursor: string): string | undefined {
146
111
  return textBeforeCursor.match(MENTION_PATTERN)?.[1];
147
112
  }
@@ -281,31 +246,20 @@ function liveSubagentReferences(
281
246
  agentIds: Set<string>,
282
247
  currentSessionId: string,
283
248
  ): SessionReference[] {
284
- const manager = getSubagentManager();
285
- if (!manager) return [];
286
-
287
249
  const references: SessionReference[] = [];
288
250
  for (const agentId of agentIds) {
289
- const record = manager.getRecord(agentId);
290
- if (!record) continue;
291
-
292
- // Try the global manager's live session first (future-proof).
293
- const liveRecord = liveSubagentRecords.get(agentId);
294
- const sessionId = liveRecord?.sessionId ?? record.id;
295
- if (!sessionId || sessionId === currentSessionId) continue;
296
-
297
- const name = record.description?.trim() || liveRecord?.agent || undefined;
298
- const modifiedAt = record.completedAt ?? record.startedAt ?? Date.now();
251
+ const record = liveSubagentRecords.get(agentId);
252
+ if (!record || record.sessionId === currentSessionId) continue;
299
253
  references.push({
300
254
  kind: "subagent",
301
- referenceIds: [sessionId, agentId],
255
+ referenceIds: [record.sessionId, agentId],
302
256
  info: {
303
- id: sessionId,
304
- name,
305
- cwd: liveRecord?.cwd ?? "",
257
+ id: record.sessionId,
258
+ name: record.agent || undefined,
259
+ cwd: record.cwd,
306
260
  firstMessage: "",
307
261
  messageCount: 0,
308
- modified: new Date(modifiedAt),
262
+ modified: new Date(record.completedAt ?? record.startedAt),
309
263
  },
310
264
  });
311
265
  }
@@ -1,8 +1,7 @@
1
1
  export const SESSION_REFERENCE_CUSTOM_TYPE = "session-reference";
2
2
  export const SESSION_REFERENCE_PREFIX = "@session:";
3
3
 
4
- const SESSION_REFERENCE_PATTERN =
5
- /(?:^|\s)@session:(?:\[([^\]]+)\]|([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?))(?![\w.\]-])/g;
4
+ const SESSION_REFERENCE_PATTERN = /(?:^|\s)@session:\[([^\]]+)](?![\w-])/g;
6
5
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
7
6
 
8
7
  export interface ReferenceSessionInfo {
@@ -31,13 +30,12 @@ export const DEFAULT_REFERENCE_LIMITS: ReferenceLimits = {
31
30
  maxTotalBytes: 48_000,
32
31
  };
33
32
 
34
- /** 提取 @session:[name](组 1)或旧格式 @session:id(组 2),返回 name 或 id。 */
35
33
  export function extractSessionReferenceIds(text: string): string[] {
36
34
  const ids: string[] = [];
37
35
  const seen = new Set<string>();
38
36
 
39
37
  for (const match of text.matchAll(SESSION_REFERENCE_PATTERN)) {
40
- const id = (match[1] ?? match[2])?.trim();
38
+ const id = match[1]?.trim();
41
39
  if (id && !seen.has(id)) {
42
40
  seen.add(id);
43
41
  ids.push(id);
@@ -250,13 +248,3 @@ export function buildReferenceContentFromSections(
250
248
  ].join("\n");
251
249
  return truncateUtf8([header, ...sections].join("\n\n"), maxTotalBytes);
252
250
  }
253
-
254
- export function buildReferenceContent(
255
- sources: ReferenceSource[],
256
- limits: ReferenceLimits = DEFAULT_REFERENCE_LIMITS,
257
- ): string {
258
- return buildReferenceContentFromSections(
259
- sources.map((source) => formatReferenceSession(source, limits)),
260
- limits.maxTotalBytes,
261
- );
262
- }
@@ -3,6 +3,7 @@ import { getKeybindings } from "@earendil-works/pi-tui";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { config } from "../../config/config.ts";
5
5
  import { ansi16ToRgb, ansi256ToRgb } from "../../utils/ansi-color.ts";
6
+ import { stripAnsi } from "../../utils/ansi-text.ts";
6
7
  type Rgb = [number, number, number];
7
8
  type StyledPart = {
8
9
  raw: string;
@@ -10,8 +11,6 @@ type StyledPart = {
10
11
  };
11
12
 
12
13
  const ANSI_RESET = "\x1b[0m";
13
- const ANSI_PATTERN =
14
- /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
15
14
 
16
15
  // 官方 install.sh 静态 logo(4 行原样,短行补尾随空格统一到 8 列)+ 底部空行补到 5 行,
17
16
  // 与右侧 tips 行数等高;着色保持现状(accent 渐变)
@@ -40,10 +39,6 @@ const PALETTE_SPAN = 0.25;
40
39
  // 行间相位偏移:小步累加 → 整体左上暗→右下亮的对角渐变
41
40
  const LOGO_ROW_PHASE_STEP = 0.08;
42
41
 
43
- function stripAnsi(text: string): string {
44
- return text.replace(ANSI_PATTERN, "");
45
- }
46
-
47
42
  function getVisibleLength(text: string): number {
48
43
  return [...stripAnsi(text)].length;
49
44
  }
@@ -41,11 +41,8 @@ import { paddedBackgroundRow } from "./tool/grouping.ts";
41
41
  import { hasVisibleText, stripBackgroundAnsi } from "../utils/ansi-text.ts";
42
42
  import { walkComponentTree } from "../utils/component-tree.ts";
43
43
  import {
44
- ASSISTANT_REENTRY_DESCRIPTION,
45
44
  ASSISTANT_REENTRY_KEY,
46
- ASSISTANT_SET_EXPANDED_DESCRIPTION,
47
45
  ASSISTANT_SET_EXPANDED_KEY,
48
- ASSISTANT_TOGGLE_ROUND_DESCRIPTION,
49
46
  ASSISTANT_TOGGLE_ROUND_KEY,
50
47
  COMPACT_MODE_PATCH_KEY,
51
48
  COMPACT_THINKING_PATCH_KEY,
@@ -433,28 +430,16 @@ function isToolComponent(value: any): boolean {
433
430
  }
434
431
 
435
432
  function detachAssistantExpansion(component: any): void {
436
- let owned =
437
- typeof component?.[ASSISTANT_SET_EXPANDED_KEY] === "function" &&
438
- component.setExpanded === component[ASSISTANT_SET_EXPANDED_KEY];
439
- if (!owned && component && typeof component === "object") {
440
- // 首次升级时清理旧模块使用 Symbol() 留下的实例方法。
441
- owned = Object.getOwnPropertySymbols(component).some(
442
- (symbol) =>
443
- symbol.description === ASSISTANT_SET_EXPANDED_DESCRIPTION && component[symbol] === true,
444
- );
433
+ if (
434
+ typeof component?.[ASSISTANT_SET_EXPANDED_KEY] !== "function" ||
435
+ component.setExpanded !== component[ASSISTANT_SET_EXPANDED_KEY]
436
+ ) {
437
+ return;
445
438
  }
446
- if (!owned) return;
447
-
448
439
  delete component.setExpanded;
449
- for (const symbol of Object.getOwnPropertySymbols(component)) {
450
- if (
451
- symbol.description === ASSISTANT_SET_EXPANDED_DESCRIPTION ||
452
- symbol.description === ASSISTANT_TOGGLE_ROUND_DESCRIPTION ||
453
- symbol.description === ASSISTANT_REENTRY_DESCRIPTION
454
- ) {
455
- delete component[symbol];
456
- }
457
- }
440
+ delete component[ASSISTANT_SET_EXPANDED_KEY];
441
+ delete component[ASSISTANT_TOGGLE_ROUND_KEY];
442
+ delete component[ASSISTANT_REENTRY_KEY];
458
443
  }
459
444
 
460
445
  /** 供 mouse-interaction 识别可点击的 compact assistant 行(仅 compact 模式下生效)。 */
@@ -62,20 +62,13 @@ type ToolRenderMethods = {
62
62
  type GlobalToolRenderPatch = {
63
63
  version: 2;
64
64
  prototype: any;
65
- owner: object;
66
65
  active: boolean;
67
- enabled: () => boolean;
68
66
  mode: () => CompactStyleMode;
69
67
  wrap: (tool: any) => any;
70
68
  byDefinition: WeakMap<object, any>;
71
69
  byName: Map<string, any>;
72
70
  downstream: ToolRenderMethods;
73
71
  installed: ToolRenderMethods;
74
- // 兼容旧扩展读取 Symbol 上的 legacy 字段。
75
- originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
76
- originalGetRenderShell: ToolRenderMethods["getRenderShell"];
77
- originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
78
- originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
79
72
  };
80
73
 
81
74
  type ToolExpandedBackgroundPatch = {
@@ -450,63 +443,39 @@ function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch {
450
443
  );
451
444
  }
452
445
 
453
- function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods {
446
+ function downstreamForGlobalToolInstall(
447
+ prototype: any,
448
+ previous: GlobalToolRenderPatch | undefined,
449
+ ): ToolRenderMethods {
454
450
  const current = prototypeToolRenderMethods(prototype);
455
- if (!previous || previous.prototype !== prototype) return current;
456
- if (isOwnershipAwarePatch(previous)) {
457
- return {
458
- hasRendererDefinition:
459
- current.hasRendererDefinition === previous.installed.hasRendererDefinition
460
- ? previous.downstream.hasRendererDefinition
461
- : current.hasRendererDefinition,
462
- getRenderShell:
463
- current.getRenderShell === previous.installed.getRenderShell
464
- ? previous.downstream.getRenderShell
465
- : current.getRenderShell,
466
- getCallRenderer:
467
- current.getCallRenderer === previous.installed.getCallRenderer
468
- ? previous.downstream.getCallRenderer
469
- : current.getCallRenderer,
470
- getResultRenderer:
471
- current.getResultRenderer === previous.installed.getResultRenderer
472
- ? previous.downstream.getResultRenderer
473
- : current.getResultRenderer,
474
- };
451
+ if (!previous || previous.prototype !== prototype || !isOwnershipAwarePatch(previous)) {
452
+ return current;
475
453
  }
476
-
477
- // pre-v2 Symbol:originalX 显式记录真实下游(native 原型方法),无需源码嗅探。
478
- // 缺失字段回退到当前原型方法(视作 external)。
479
- const originalOrCurrent = (field: string, method: Function): Function =>
480
- typeof previous[field] === "function" ? previous[field] : method;
481
454
  return {
482
- hasRendererDefinition: originalOrCurrent(
483
- "originalHasRendererDefinition",
484
- current.hasRendererDefinition,
485
- ) as ToolRenderMethods["hasRendererDefinition"],
486
- getRenderShell: originalOrCurrent(
487
- "originalGetRenderShell",
488
- current.getRenderShell,
489
- ) as ToolRenderMethods["getRenderShell"],
490
- getCallRenderer: originalOrCurrent(
491
- "originalGetCallRenderer",
492
- current.getCallRenderer,
493
- ) as ToolRenderMethods["getCallRenderer"],
494
- getResultRenderer: originalOrCurrent(
495
- "originalGetResultRenderer",
496
- current.getResultRenderer,
497
- ) as ToolRenderMethods["getResultRenderer"],
455
+ hasRendererDefinition:
456
+ current.hasRendererDefinition === previous.installed.hasRendererDefinition
457
+ ? previous.downstream.hasRendererDefinition
458
+ : current.hasRendererDefinition,
459
+ getRenderShell:
460
+ current.getRenderShell === previous.installed.getRenderShell
461
+ ? previous.downstream.getRenderShell
462
+ : current.getRenderShell,
463
+ getCallRenderer:
464
+ current.getCallRenderer === previous.installed.getCallRenderer
465
+ ? previous.downstream.getCallRenderer
466
+ : current.getCallRenderer,
467
+ getResultRenderer:
468
+ current.getResultRenderer === previous.installed.getResultRenderer
469
+ ? previous.downstream.getResultRenderer
470
+ : current.getResultRenderer,
498
471
  };
499
472
  }
500
473
 
501
- function disconnectGlobalToolRenderPatch(patch: any): void {
502
- if (!patch || typeof patch !== "object") return;
474
+ function disconnectGlobalToolRenderPatch(patch: GlobalToolRenderPatch | undefined): void {
475
+ if (!patch) return;
503
476
  patch.active = false;
504
- patch.enabled = () => false;
505
- patch.mode = () => "off";
506
- patch.wrap = (tool: any) => tool;
507
477
  patch.byDefinition = new WeakMap();
508
- if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear();
509
- else patch.byName = new Map();
478
+ patch.byName.clear();
510
479
  }
511
480
 
512
481
  function installGlobalToolRendering(
@@ -515,25 +484,18 @@ function installGlobalToolRendering(
515
484
  const prototype = (ToolExecutionComponent as any).prototype;
516
485
  const previous = patchRegistry.get<GlobalToolRenderPatch>(GLOBAL_TOOL_RENDER_PATCH);
517
486
  const downstream = downstreamForGlobalToolInstall(prototype, previous);
518
- // 外部仍持有的旧 wrapper 先变为无回调 pass-through,再挂新安装。
519
- disconnectGlobalToolRenderPatch(previous);
487
+ if (isOwnershipAwarePatch(previous)) disconnectGlobalToolRenderPatch(previous);
520
488
 
521
489
  const patch: GlobalToolRenderPatch = {
522
490
  version: 2,
523
491
  prototype,
524
- owner: {},
525
492
  active: true,
526
- enabled: () => config.mode === "on",
527
493
  mode: () => config.mode,
528
494
  wrap: (tool: any) => createCcstyleTool(tool, writeExecutionMetadata),
529
495
  byDefinition: new WeakMap(),
530
496
  byName: new Map(),
531
497
  downstream,
532
498
  installed: undefined as any,
533
- originalHasRendererDefinition: downstream.hasRendererDefinition,
534
- originalGetRenderShell: downstream.getRenderShell,
535
- originalGetCallRenderer: downstream.getCallRenderer,
536
- originalGetResultRenderer: downstream.getResultRenderer,
537
499
  };
538
500
 
539
501
  patch.installed = {
@@ -14,7 +14,6 @@ import {
14
14
  } from "./default-mode.ts";
15
15
  import { isLazyProxyTui } from "../utils/fullscreen-detect.ts";
16
16
  import { showCcstylePanel } from "../config/panel.ts";
17
- import { GLOBAL_COMPACTION_RENDER_PATCH, patchRegistry } from "../utils/patch-keys.ts";
18
17
  import {
19
18
  config,
20
19
  formatConfigStatus,
@@ -93,16 +92,6 @@ function applyStyleMode(mode: CompactStyleMode, ctx: any, toolGrouping?: ToolGro
93
92
  ctx.ui.notify(`Claude Code style: ${mode}`, "info");
94
93
  }
95
94
 
96
- type LegacyCompactionRenderPatch = {
97
- enabled?: () => boolean;
98
- };
99
-
100
- /** Disable the pre-native compaction monkey patch left alive by /reload. */
101
- function deactivateLegacyCompactionRendering() {
102
- const patch = patchRegistry.get<LegacyCompactionRenderPatch>(GLOBAL_COMPACTION_RENDER_PATCH);
103
- if (patch) patch.enabled = () => false;
104
- }
105
-
106
95
  export default function (
107
96
  pi: ExtensionAPI,
108
97
  configOverride?: Partial<Config>,
@@ -137,7 +126,6 @@ export default function (
137
126
  const disposeMessageDisplay = installMessageDisplayRendering();
138
127
  // 展开背景必须在 compact-mode 之后装,shutdown 时先于 compact 释放。
139
128
  const disposeToolExpandedBackground = installToolExpandedBackground();
140
- deactivateLegacyCompactionRendering();
141
129
  installation = {
142
130
  defaultMode,
143
131
  toolGrouping,
@@ -270,7 +258,6 @@ export default function (
270
258
  current.compactMode.shutdown();
271
259
  compactModeHooks = undefined;
272
260
  current.disposeMessageDisplay();
273
- deactivateLegacyCompactionRendering();
274
261
  clearAllAnimations();
275
262
  installation = undefined;
276
263
  });
@@ -21,13 +21,8 @@ export function sharedToolHoverState(): SharedToolHoverState {
21
21
  return patchRegistry.ensure(TOOL_HOVER_STATE_KEY, () => ({ toolCallId: null }));
22
22
  }
23
23
 
24
- // 状态单源在 globalThis 槽;hoveredToolCallId 变量仅为兼容旧 deep import 保留
25
- // (外部扩展可能直接从 interaction.ts re-export 读取),内部读取一律走槽。
26
- export let hoveredToolCallId: string | null = sharedToolHoverState().toolCallId;
27
-
28
24
  export function setHoveredToolCallId(toolCallId: string | null): void {
29
25
  sharedToolHoverState().toolCallId = toolCallId;
30
- hoveredToolCallId = toolCallId;
31
26
  }
32
27
 
33
28
  export function isToolCallHovered(toolCallId: string | null | undefined): boolean {
@@ -19,6 +19,7 @@ import {
19
19
  collectToolComponents,
20
20
  extractToolFramePlacements,
21
21
  isSgrLeftPress,
22
+ isSgrLeftRelease,
22
23
  isToolExecutionComponent,
23
24
  parseSgrMousePackets,
24
25
  stripTerminalSequences,
@@ -36,6 +37,7 @@ import {
36
37
  isScrollbarColumnAt,
37
38
  } from "./layout.ts";
38
39
  import {
40
+ disableOfficialScrollToEnd,
39
41
  fullscreenLazyTui,
40
42
  hideScrollButton,
41
43
  isScrollBottomInput,
@@ -156,7 +158,11 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
156
158
  }
157
159
 
158
160
  const EXPAND_PANEL_DOUBLE_CLICK_MS = 400;
159
- let lastExpandPanelClick: { id: unknown; at: number } | null = null;
161
+ /** 松开后短时间内的重复按下视为同一次单击(终端会在 mouseup 后再打一次 press)。 */
162
+ const EXPAND_PANEL_CLICK_ARM_MS = 50;
163
+ let pendingExpandPress: { id: unknown } | null = null;
164
+ let lastExpandClick: { id: unknown; at: number } | null = null;
165
+ let armExpandClickTimer: ReturnType<typeof setTimeout> | null = null;
160
166
 
161
167
  function expandPanelIdentity(card: any): unknown {
162
168
  return card instanceof ThinkingPreviewBlock
@@ -167,17 +173,48 @@ function expandPanelIdentity(card: any): unknown {
167
173
  function isExpandPanelDoubleClick(card: any): boolean {
168
174
  const now = Date.now();
169
175
  const id = expandPanelIdentity(card);
170
- const prev = lastExpandPanelClick;
171
- lastExpandPanelClick = { id, at: now };
172
- return Boolean(prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS);
176
+ const prev = lastExpandClick;
177
+ pendingExpandPress = { id };
178
+ if (prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS) {
179
+ pendingExpandPress = null;
180
+ lastExpandClick = null;
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+
186
+ function completeExpandPanelClick(): void {
187
+ if (!pendingExpandPress) return;
188
+ const click = { id: pendingExpandPress.id, at: Date.now() };
189
+ pendingExpandPress = null;
190
+ if (armExpandClickTimer) clearTimeout(armExpandClickTimer);
191
+ armExpandClickTimer = setTimeout(() => {
192
+ armExpandClickTimer = null;
193
+ // 50ms 内又按下:仍是同一次单击,不能武装成双击。
194
+ if (pendingExpandPress) return;
195
+ lastExpandClick = click;
196
+ }, EXPAND_PANEL_CLICK_ARM_MS);
197
+ if (
198
+ typeof armExpandClickTimer === "object" &&
199
+ armExpandClickTimer &&
200
+ "unref" in armExpandClickTimer
201
+ ) {
202
+ armExpandClickTimer.unref();
203
+ }
173
204
  }
174
205
 
175
206
  function clearExpandPanelDoubleClick(): void {
176
- lastExpandPanelClick = null;
207
+ pendingExpandPress = null;
208
+ lastExpandClick = null;
209
+ if (armExpandClickTimer) {
210
+ clearTimeout(armExpandClickTimer);
211
+ armExpandClickTimer = null;
212
+ }
177
213
  }
178
214
 
179
215
  function collapseExpandedCard(tui: any, card: any): boolean {
180
- if (!isExpandPanelDoubleClick(card)) return false;
216
+ // 单击也 consume,避免官方链再合成一次 press 把单击当成双击。
217
+ if (!isExpandPanelDoubleClick(card)) return true;
181
218
  card.setExpanded(false);
182
219
  setHoveredToolCallId(null);
183
220
  setHoveredToolGroup(null);
@@ -333,18 +370,17 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
333
370
  const plain = stripTerminalSequencesPreservingLayout(line);
334
371
  const section = view.matchShowMoreLine(plain);
335
372
  if (section) {
373
+ clearExpandPanelDoubleClick();
336
374
  const box = view.showMoreHitbox(plain);
337
- if (box && x + 1 >= box.startCol && x + 1 <= box.endCol) {
338
- return tryOpenToolIoShowMore({
339
- kind: "show-more",
340
- row: 0,
341
- startCol: box.startCol,
342
- endCol: box.endCol,
343
- component,
344
- view,
345
- section,
346
- });
347
- }
375
+ return tryOpenToolIoShowMore({
376
+ kind: "show-more",
377
+ row: 0,
378
+ startCol: box?.startCol ?? 1,
379
+ endCol: box?.endCol ?? 1,
380
+ component,
381
+ view,
382
+ section,
383
+ });
348
384
  }
349
385
  }
350
386
  // 双击收起:内部工具仍归所属 group。单击放行官方(选择/链接)。
@@ -461,6 +497,7 @@ function patchFullscreenViewportInput(tui: any): void {
461
497
  if (packets && tui.hasOverlay?.() && hasActiveTextPreview()) return undefined;
462
498
  if (packets && !tui.hasOverlay?.()) {
463
499
  for (const packet of packets) {
500
+ if (isSgrLeftRelease(packet)) completeExpandPanelClick();
464
501
  if (isSgrLeftPress(packet) && handleFullscreenToolClick(tui, packet)) {
465
502
  return { consume: true };
466
503
  }
@@ -797,6 +834,7 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
797
834
  let consumed = false;
798
835
  for (const packet of packets) {
799
836
  updateToolSummaryHover(getToolMouseTui(), packet);
837
+ if (isSgrLeftRelease(packet)) completeExpandPanelClick();
800
838
  if (!isSgrLeftPress(packet)) continue;
801
839
  if (toggleToolAtMouseClick(getToolMouseTui(), packet)) {
802
840
  consumed = true;
@@ -879,10 +917,12 @@ export function installToolMouseInteraction(
879
917
  if (isLazyProxyTui(tui)) {
880
918
  patchFullscreenViewportInput(tui);
881
919
  ensureFullscreenToolMouseMotion(tui);
920
+ disableOfficialScrollToEnd(tui);
882
921
  setScrollButtonWidget({
883
922
  render: (width: number) => {
884
923
  patchFullscreenViewportInput(tui);
885
924
  ensureFullscreenToolMouseMotion(tui);
925
+ disableOfficialScrollToEnd(tui);
886
926
  return renderScrollButton(width, theme);
887
927
  },
888
928
  invalidate() {},
@@ -924,13 +964,3 @@ export function scheduleSessionRender(refresh?: () => void): void {
924
964
  tui.requestRender(true);
925
965
  }, 0);
926
966
  }
927
-
928
- // toolMouseTui 跨模块读取一律走 getToolMouseTui()(jiti 转译下 let 绑定可能是快照);
929
- // 此 re-export 仅保留兼容旧导入,新代码请用 getToolMouseTui。
930
- export { toolMouseTui } from "./scroll.ts";
931
- export {
932
- hoveredToolCallId,
933
- isToolCallHovered,
934
- setHoveredToolGroup,
935
- setHoveredToolIo,
936
- } from "./hover.ts";
@@ -7,6 +7,7 @@ export {
7
7
  export {
8
8
  parseSgrMousePackets,
9
9
  isSgrLeftPress,
10
+ isSgrLeftRelease,
10
11
  type SgrMousePacket,
11
12
  } from "../../utils/sgr-mouse.ts";
12
13
 
@@ -18,26 +18,14 @@ const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|
18
18
  const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
19
19
  const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
20
20
 
21
- /**
22
- * 当前安装的 tui 宿主。宿主放本模块(滚动按钮/调度依赖它),
23
- * 由 mouse-interaction 经 setToolMouseTui 维护;跨模块一律经绑定/setter 访问。
24
- *
25
- * 状态镜像到 globalThis(Symbol 槽):jiti 转译下经 re-export 链读取的
26
- * 模块级 let 绑定是初始值快照(死绑定,实测恒 null),函数调用才是活引用。
27
- * 跨模块读取一律用 getToolMouseTui(),避免拿到加载时的快照。
28
- */
29
21
  patchRegistry.ensure(TOOL_MOUSE_TUI_SLOT, () => null);
30
- export let toolMouseTui: any = null;
31
22
  export function getToolMouseTui(): any {
32
23
  return patchRegistry.get(TOOL_MOUSE_TUI_SLOT);
33
24
  }
34
25
  export function setToolMouseTui(tui: any): void {
35
- toolMouseTui = tui;
36
26
  patchRegistry.install(TOOL_MOUSE_TUI_SLOT, tui);
37
27
  }
38
28
 
39
- // 滚动按钮状态同 toolMouseTui:镜像到 globalThis(Symbol 槽),跨模块读取
40
- // 一律用 getter(jiti 转译下模块级 let 绑定是初始值快照)。
41
29
  type ScrollButtonState = { visible: boolean; hovered: boolean; widget: any };
42
30
  function scrollButtonState(): ScrollButtonState {
43
31
  return patchRegistry.ensure(SCROLL_BUTTON_STATE_SLOT, () => ({
@@ -91,6 +79,11 @@ export function fullscreenLazyTui(tui: any): boolean {
91
79
  return isLazyProxyTui(tui) && tui.mode === "fullscreen";
92
80
  }
93
81
 
82
+ /** 关掉 pi 0.85 Jump to latest overlay,避免和本仓库 dock 按钮叠两层。 */
83
+ export function disableOfficialScrollToEnd(tui: any): void {
84
+ if (typeof tui?.scrollToEndIndicator === "function") tui.scrollToEndIndicator = undefined;
85
+ }
86
+
94
87
  /** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
95
88
  export function isFullscreenAtBottom(tui: any): boolean {
96
89
  const following = tui.isFollowingOutput ?? tui.getPrimaryScrollView?.()?.isFollowingEnd ?? true;
@@ -164,7 +157,7 @@ export function scheduleScrollButtonSync(tui: any, data: string): void {
164
157
  const previousLines = tui.previousLines;
165
158
  const check = (attempt: number) => {
166
159
  scrollButtonSyncScheduled = false;
167
- if (toolMouseTui !== tui) return;
160
+ if (getToolMouseTui() !== tui) return;
168
161
  // Pi renders on its own frame timer. Inspect the resulting viewport before
169
162
  // showing the button so empty or non-scrollable transcripts never flash it.
170
163
  const rendered = tui.previousLines !== previousLines;
@@ -192,7 +185,7 @@ export function updateScrollButtonFromInput(tui: any, data: string): void {
192
185
  }
193
186
 
194
187
  export function renderScrollButton(width: number, theme: any): string[] {
195
- if (!getScrollButtonVisible() || !fullscreenLazyTui(toolMouseTui)) return [];
188
+ if (!getScrollButtonVisible() || !fullscreenLazyTui(getToolMouseTui())) return [];
196
189
  const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
197
190
  const label = theme.fg(
198
191
  getScrollButtonHovered() ? "text" : "accent",
@@ -45,18 +45,10 @@ export function resolveDiffDisplayLimit(
45
45
  return expanded ? Math.max(0, expandedLimit) : Math.max(1, collapsedLimit);
46
46
  }
47
47
 
48
- /** Write collapsed body limit. Missing/NaN falls back to edit's `editDiffCollapsedLines`. */
49
48
  export function resolveWriteCollapsedLimit(
50
- config: Pick<ToolDisplayConfig, "writeDiffCollapsedLines" | "editDiffCollapsedLines">,
49
+ config: Pick<ToolDisplayConfig, "writeDiffCollapsedLines">,
51
50
  ): number {
52
- const value = config.writeDiffCollapsedLines;
53
- if (Number.isFinite(value)) {
54
- return Math.max(0, value);
55
- }
56
- const fallback = Number.isFinite(config.editDiffCollapsedLines)
57
- ? config.editDiffCollapsedLines
58
- : DEFAULT_TOOL_DISPLAY_CONFIG.writeDiffCollapsedLines;
59
- return Math.max(0, fallback);
51
+ return Math.max(0, config.writeDiffCollapsedLines);
60
52
  }
61
53
 
62
54
  /**
@@ -40,7 +40,6 @@ export interface DiffStats {
40
40
 
41
41
  const CANONICAL_LINE_PATTERN = /^([+\- ])(\s*\d+)\|(.*)$/;
42
42
  const HASHLINE_ANCHOR_LINE_PATTERN = /^([+\- ])(\s*\d+)#([A-Za-z0-9]+| {2}):(.*)$/;
43
- const LEGACY_LINE_PATTERN = /^([+\- ])(\s*\d+)\s(.*)$/;
44
43
  const HUNK_HEADER_PATTERN = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
45
44
  const MIN_LINE_NUMBER_WIDTH = 2;
46
45
 
@@ -81,14 +80,8 @@ function parseCanonicalDiffLine(line: string): {
81
80
  };
82
81
  }
83
82
 
84
- const canonicalMatch = line.match(CANONICAL_LINE_PATTERN);
85
- const legacyMatch = canonicalMatch ? null : line.match(LEGACY_LINE_PATTERN);
86
- const matched = canonicalMatch ?? legacyMatch;
87
- if (!matched) {
88
- return null;
89
- }
90
-
91
- return toParsedDiffLine(matched[1] ?? " ", matched[2] ?? "", matched[3] ?? "");
83
+ const match = line.match(CANONICAL_LINE_PATTERN);
84
+ return match ? toParsedDiffLine(match[1] ?? " ", match[2] ?? "", match[3] ?? "") : null;
92
85
  }
93
86
 
94
87
  function toNumber(value: string | undefined): number | null {
@@ -204,7 +204,7 @@ export class ExpandedToolResultText {
204
204
  }
205
205
  }
206
206
 
207
- /** Affordance next to truncated Input/Output headers — click opens full preview. */
207
+ /** 截断体末行 `… +N more lines` 旁的展开提示,点击打开全量预览。 */
208
208
  export const SHOW_MORE_LABEL = "• click to show more";
209
209
 
210
210
  export type ToolIoSection = "input" | "output";
@@ -251,8 +251,8 @@ export class ExpandedToolIoView {
251
251
  inputBody: string,
252
252
  outputBody: string,
253
253
  isError: boolean,
254
- maxOutputLines = config.expandedPreviewMaxLines,
255
- maxInputLines = config.expandedPreviewMaxLines,
254
+ maxOutputLines = config.expandedOutputMaxLines,
255
+ maxInputLines = config.expandedInputMaxLines,
256
256
  flushLeft = false,
257
257
  ) {
258
258
  this.theme = theme;
@@ -309,12 +309,12 @@ export class ExpandedToolIoView {
309
309
  this.invalidate();
310
310
  }
311
311
 
312
- /** True when the plain header line is a truncated section with show-more. */
312
+ /** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
313
313
  matchShowMoreLine(plainLine: string): ToolIoSection | null {
314
314
  const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
315
- if (!line.includes(` • ${showMoreHintText()}`)) return null;
316
- if (/\bInput\b/.test(line) && this.truncated.input) return "input";
317
- if (/\bOutput\b/.test(line) && this.truncated.output) return "output";
315
+ if (!line.includes(` • ${showMoreHintText()}`) || !/\+\d+ more lines/.test(line)) return null;
316
+ if (this.truncated.input && line.includes("│")) return "input";
317
+ if (this.truncated.output) return "output";
318
318
  return null;
319
319
  }
320
320
 
@@ -360,24 +360,13 @@ export class ExpandedToolIoView {
360
360
  this.truncated = { input: false, output: false };
361
361
  this.showMoreHeaderRows = {};
362
362
 
363
- const pushHeader = (
364
- corner: "├" | "└",
365
- label: string,
366
- section: ToolIoSection,
367
- showMore: boolean,
368
- ) => {
363
+ const pushHeader = (corner: "├" | "└", label: string) => {
369
364
  const mark = theme.fg("dim", `${lead}${corner} `);
370
365
  const title = theme.fg(
371
366
  "accent",
372
367
  typeof theme.bold === "function" ? theme.bold(label) : label,
373
368
  );
374
- // hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
375
- const more = showMore
376
- ? theme.fg("dim", " •") +
377
- theme.fg(this.hoveredSection === section ? "text" : "dim", ` ${showMoreHintText()}`)
378
- : "";
379
- if (showMore) this.showMoreHeaderRows[section] = lines.length;
380
- lines.push(truncateToWidth(mark + title + more, safeWidth, ""));
369
+ lines.push(truncateToWidth(mark + title, safeWidth, ""));
381
370
  };
382
371
 
383
372
  const pushRailLine = (styledContent: string, continued = true) => {
@@ -400,7 +389,7 @@ export class ExpandedToolIoView {
400
389
 
401
390
  const pushBody = (
402
391
  body: string,
403
- opts: { input?: boolean; limit: number; continued?: boolean },
392
+ opts: { input?: boolean; limit: number; continued?: boolean; section: ToolIoSection },
404
393
  ): boolean /* truncated */ => {
405
394
  const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
406
395
  if (!raw.trim()) {
@@ -423,7 +412,15 @@ export class ExpandedToolIoView {
423
412
  if (truncated) {
424
413
  const hidden = Math.max(0, wrapped.length - visible.length);
425
414
  if (hidden > 0) {
426
- pushRailLine(theme.fg("dim", `… +${hidden} more lines`), opts.continued);
415
+ // hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
416
+ const more =
417
+ theme.fg("dim", " •") +
418
+ theme.fg(
419
+ this.hoveredSection === opts.section ? "text" : "dim",
420
+ ` ${showMoreHintText()}`,
421
+ );
422
+ pushRailLine(theme.fg("dim", `… +${hidden} more lines`) + more, opts.continued);
423
+ this.showMoreHeaderRows[opts.section] = lines.length - 1;
427
424
  }
428
425
  }
429
426
  return truncated;
@@ -447,20 +444,29 @@ export class ExpandedToolIoView {
447
444
 
448
445
  if (hasInput) {
449
446
  this.truncated.input = inputWouldTruncate;
450
- pushHeader("├", "Input", "input", inputWouldTruncate);
447
+ pushHeader("├", "Input");
451
448
  pushBody(this.inputBody, {
452
449
  input: true,
453
450
  limit: this.maxInputLines,
454
451
  continued: true,
452
+ section: "input",
455
453
  });
456
454
  pushBlankRail();
457
455
  this.truncated.output = outputWouldTruncate;
458
- pushHeader("└", "Output", "output", outputWouldTruncate);
459
- pushBody(outputText, { limit: this.maxOutputLines, continued: false });
456
+ pushHeader("└", "Output");
457
+ pushBody(outputText, {
458
+ limit: this.maxOutputLines,
459
+ continued: false,
460
+ section: "output",
461
+ });
460
462
  } else {
461
463
  this.truncated.output = outputWouldTruncate;
462
- pushHeader("└", "Output", "output", outputWouldTruncate);
463
- pushBody(outputText, { limit: this.maxOutputLines, continued: false });
464
+ pushHeader("└", "Output");
465
+ pushBody(outputText, {
466
+ limit: this.maxOutputLines,
467
+ continued: false,
468
+ section: "output",
469
+ });
464
470
  }
465
471
 
466
472
  this.cachedWidth = width;
@@ -663,13 +669,21 @@ export function renderExpandedToolResult(
663
669
  ): ExpandedToolIoView | ExpandedToolResultText | Text {
664
670
  const inputBody = formatToolInputArgs(args);
665
671
  const outputBody = body;
666
- const maxLines = config.expandedPreviewMaxLines;
672
+ const maxOutputLines = config.expandedOutputMaxLines;
673
+ const maxInputLines = config.expandedInputMaxLines;
667
674
 
668
675
  // Prefer structured Input/Output when we have args or non-empty output.
669
676
  if (inputBody.trim() || outputBody.trim()) {
670
677
  let view: ExpandedToolIoView;
671
678
  if (isExpandedToolIoView(lastComponent)) {
672
- lastComponent.setContent(inputBody, outputBody, isError, maxLines, maxLines, flushLeft);
679
+ lastComponent.setContent(
680
+ inputBody,
681
+ outputBody,
682
+ isError,
683
+ maxOutputLines,
684
+ maxInputLines,
685
+ flushLeft,
686
+ );
673
687
  view = lastComponent;
674
688
  } else {
675
689
  view = new ExpandedToolIoView(
@@ -677,8 +691,8 @@ export function renderExpandedToolResult(
677
691
  inputBody,
678
692
  outputBody,
679
693
  isError,
680
- maxLines,
681
- maxLines,
694
+ maxOutputLines,
695
+ maxInputLines,
682
696
  flushLeft,
683
697
  );
684
698
  }
@@ -18,13 +18,10 @@ export const COMPACT_THINKING_PATCH_KEY = Symbol.for("pi.ccstyle.compact-thinkin
18
18
  export const PROTOTYPE_ORIGINAL_KEY = Symbol.for("pi.ccstyle.prototype-original");
19
19
  export const COMPACT_THINKING_OWNER = Symbol.for("pi.ccstyle.compact-thinking-owner");
20
20
 
21
- // ── assistant 展开状态(实例标记;description 供旧模块按 symbol.description 清理) ──
22
- export const ASSISTANT_SET_EXPANDED_DESCRIPTION = "pi.ccstyle.compact-assistant-set-expanded";
23
- export const ASSISTANT_TOGGLE_ROUND_DESCRIPTION = "pi.ccstyle.compact-assistant-toggle-round";
24
- export const ASSISTANT_REENTRY_DESCRIPTION = "pi.ccstyle.compact-assistant-reentry";
25
- export const ASSISTANT_SET_EXPANDED_KEY = Symbol.for(ASSISTANT_SET_EXPANDED_DESCRIPTION);
26
- export const ASSISTANT_TOGGLE_ROUND_KEY = Symbol.for(ASSISTANT_TOGGLE_ROUND_DESCRIPTION);
27
- export const ASSISTANT_REENTRY_KEY = Symbol.for(ASSISTANT_REENTRY_DESCRIPTION);
21
+ // ── assistant 展开状态(实例标记) ──
22
+ export const ASSISTANT_SET_EXPANDED_KEY = Symbol.for("pi.ccstyle.compact-assistant-set-expanded");
23
+ export const ASSISTANT_TOGGLE_ROUND_KEY = Symbol.for("pi.ccstyle.compact-assistant-toggle-round");
24
+ export const ASSISTANT_REENTRY_KEY = Symbol.for("pi.ccstyle.compact-assistant-reentry");
28
25
 
29
26
  // ── 工具渲染补丁 ──
30
27
  export const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
@@ -36,9 +33,6 @@ export const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for(
36
33
  // ── 消息组件补丁 ──
37
34
  export const MESSAGE_DISPLAY_PATCH = Symbol.for("pi.ccstyle.message-display-patch");
38
35
 
39
- // ── 压缩渲染补丁(/reload 残留 deactivate 用) ──
40
- export const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
41
-
42
36
  // ── 工具分组 ──
43
37
  export const TOOL_GROUPING_PATCH_KEY = Symbol.for("pi.ccstyle.tool-grouping-patch");
44
38
  export const TOOL_GROUPING_PARENT_KEY = Symbol.for("pi.ccstyle.tool-grouping-parent");
@@ -53,6 +53,12 @@ export function isSgrLeftPress(packet: SgrMousePacket): boolean {
53
53
  return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
54
54
  }
55
55
 
56
+ /** 是否为左键松开。 */
57
+ export function isSgrLeftRelease(packet: SgrMousePacket): boolean {
58
+ const baseButton = mouseBaseButton(packet.code);
59
+ return packet.final === "m" && baseButton === 0 && (packet.code & 32) === 0;
60
+ }
61
+
56
62
  /** 剥离修饰键位(4/8/16/32:shift/meta/ctrl/motion),得到基础按键码。 */
57
63
  export function mouseBaseButton(code: number): number {
58
64
  return code & ~(4 | 8 | 16 | 32);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.68",
3
+ "version": "0.8.69",
4
4
  "license": "MIT",
5
5
  "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",
@@ -39,10 +39,10 @@
39
39
  },
40
40
  "scripts": {
41
41
  "test": "node --test tests/**/*.test.ts",
42
- "typecheck": "node scripts/typecheck.mjs",
42
+ "typecheck": "tsc --noEmit",
43
43
  "format": "biome format --write .",
44
44
  "format:check": "biome format .",
45
- "docs:tool-render": "node scripts/generate-tool-render-examples.mjs",
45
+ "docs:tool-render": "node --experimental-strip-types scripts/generate-tool-render-examples.ts",
46
46
  "lint": "biome check .",
47
47
  "preversion": "npm run format && npm run lint && npm run typecheck && npm test && npm run docs:tool-render",
48
48
  "release": "npm version patch && npm publish",