pi-cc-extensions 0.8.56 → 0.8.58

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 (45) hide show
  1. package/extensions/config/config.ts +40 -9
  2. package/extensions/config/panel.ts +32 -36
  3. package/extensions/feature/compact-thinking.ts +12 -12
  4. package/extensions/feature/context.ts +10 -56
  5. package/extensions/feature/reference/index.ts +4 -2
  6. package/extensions/feature/shell/startup-header.ts +1 -1
  7. package/extensions/feature/shell/working-message.ts +28 -21
  8. package/extensions/renderer/compact-mode.ts +23 -50
  9. package/extensions/renderer/default-mode.ts +36 -152
  10. package/extensions/renderer/index.ts +5 -9
  11. package/extensions/renderer/markdown-enhance.ts +1 -1
  12. package/extensions/renderer/mouse/hover.ts +2 -4
  13. package/extensions/renderer/mouse/interaction.ts +6 -6
  14. package/extensions/renderer/mouse/packets.ts +16 -66
  15. package/extensions/renderer/mouse/scroll.ts +13 -7
  16. package/extensions/renderer/tool/diff/diff-component.ts +107 -0
  17. package/extensions/renderer/tool/diff/diff-edit-render.ts +198 -0
  18. package/extensions/renderer/tool/diff/diff-header.ts +136 -0
  19. package/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  20. package/extensions/renderer/tool/diff/diff-inline.ts +312 -0
  21. package/extensions/renderer/tool/diff/diff-layout.ts +730 -0
  22. package/extensions/renderer/tool/diff/diff-limits.ts +145 -0
  23. package/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  24. package/extensions/renderer/tool/diff/diff-parse.ts +512 -0
  25. package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  26. package/extensions/renderer/tool/diff/diff-renderer.ts +21 -3046
  27. package/extensions/renderer/tool/diff/diff-text.ts +45 -0
  28. package/extensions/renderer/tool/diff/diff-write-render.ts +498 -0
  29. package/extensions/renderer/tool/diff/index.ts +2 -2
  30. package/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  31. package/extensions/renderer/tool/grouping.ts +19 -143
  32. package/extensions/renderer/{message-display.ts → tool/message-display.ts} +9 -30
  33. package/extensions/renderer/tool/names.ts +168 -0
  34. package/extensions/renderer/tool/result.ts +3 -6
  35. package/extensions/renderer/transcript-refresh.ts +8 -26
  36. package/extensions/utils/ansi-text.ts +47 -0
  37. package/extensions/utils/component-tree.ts +34 -0
  38. package/extensions/utils/format.ts +7 -0
  39. package/extensions/utils/patch-keys.ts +121 -0
  40. package/extensions/utils/sgr-mouse.ts +59 -0
  41. package/package.json +1 -1
  42. package/extensions/renderer/tool/diff/render-utils.ts +0 -5
  43. package/extensions/renderer/tool/diff/types.ts +0 -26
  44. package/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  45. /package/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +0 -0
@@ -1,16 +1,40 @@
1
1
  import type { CompactThinkingConfig } from "../feature/compact-thinking.ts";
2
- import {
3
- DEFAULT_TOOL_DISPLAY_CONFIG,
4
- type DiffIndicatorMode,
5
- type DiffViewMode,
6
- type ToolDisplayConfig,
7
- } from "../renderer/tool/diff/index.ts";
8
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
3
  import { homedir } from "node:os";
10
4
  import { join } from "node:path";
11
5
 
12
6
  export type CompactStyleMode = "on" | "compact" | "off";
13
7
 
8
+ // ── diff 显示配置:作为配置 schema 的一部分由 config 层拥有 ──
9
+ // renderer(diff)从 config 导入并 re-export,避免 config → renderer 的反向依赖。
10
+
11
+ export type DiffViewMode = "auto" | "split" | "unified";
12
+ export type DiffIndicatorMode = "bars" | "classic" | "none";
13
+
14
+ export interface ToolDisplayConfig {
15
+ diffViewMode: DiffViewMode;
16
+ diffIndicatorMode: DiffIndicatorMode;
17
+ diffSplitMinWidth: number;
18
+ diffCollapsedLines: number;
19
+ diffWordWrap: boolean;
20
+ expandedPreviewMaxLines: number;
21
+ }
22
+
23
+ export const DEFAULT_TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
24
+ diffViewMode: "auto",
25
+ diffIndicatorMode: "bars",
26
+ diffSplitMinWidth: 120,
27
+ /** Collapsed tool/diff body: ~half a typical terminal after chrome. */
28
+ diffCollapsedLines: 24,
29
+ diffWordWrap: true,
30
+ /**
31
+ * Expanded tool/diff body cap. 40 ≈ one screen of content after title,
32
+ * Input section, editor, and status — keeps the TUI compact.
33
+ * Raise via /ccstyle → Diff → Expanded max lines when reviewing large dumps.
34
+ */
35
+ expandedPreviewMaxLines: 40,
36
+ };
37
+
14
38
  export type Config = {
15
39
  mode: CompactStyleMode;
16
40
  excludeRenderers: string[];
@@ -216,7 +240,8 @@ export function formatConfigStatus(source: Config = config): string {
216
240
  ].join(" · ");
217
241
  }
218
242
 
219
- export let config: Config = loadConfig();
243
+ /** 进程内唯一的活动配置对象。读取直接用 `config.xxx`;写入必须走 `updateConfig`。 */
244
+ export const config: Config = loadConfig();
220
245
 
221
246
  function loadConfig(): Config {
222
247
  try {
@@ -247,7 +272,13 @@ export function saveConfig() {
247
272
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
248
273
  }
249
274
 
250
- /** 整体替换配置(default export 的 configOverride 注入路径;导入绑定不可重新赋值)。 */
275
+ /** 运行时配置写入的唯一入口:合并 + 规范化 + 持久化。 */
276
+ export function updateConfig(partial: Partial<Config>): void {
277
+ Object.assign(config, normalizeConfig({ ...config, ...partial }));
278
+ saveConfig();
279
+ }
280
+
281
+ /** 整体替换配置(default export 的 configOverride 注入路径;就地覆盖,不持久化)。 */
251
282
  export function setConfig(next: Config): void {
252
- config = next;
283
+ Object.assign(config, next);
253
284
  }
@@ -9,7 +9,6 @@ import { Input, SettingsList, matchesKey, truncateToWidth } from "@earendil-work
9
9
  import type { CompactThinkingController } from "../feature/compact-thinking.ts";
10
10
  import { applyStartupHeader } from "../feature/shell/startup-header.ts";
11
11
  import type { ToolGroupingHooks } from "../renderer/tool/grouping.ts";
12
- import type { DiffIndicatorMode, DiffViewMode } from "../renderer/tool/diff/index.ts";
13
12
  import {
14
13
  config,
15
14
  DEFAULT_CONFIG,
@@ -23,11 +22,14 @@ import {
23
22
  getCompactThinkingConfig,
24
23
  pickPositiveInt,
25
24
  pickPositiveNumber,
26
- saveConfig,
27
25
  SCROLL_STEP_LINES_VALUES,
28
26
  THINKING_ANIMATION_INTERVAL_VALUES,
29
27
  THINKING_PREVIEW_LINES_VALUES,
28
+ updateConfig,
30
29
  type CompactStyleMode,
30
+ type Config,
31
+ type DiffIndicatorMode,
32
+ type DiffViewMode,
31
33
  } from "./config.ts";
32
34
 
33
35
  /** renderer 注入的渲染副作用,面板自身不触碰渲染状态。 */
@@ -117,8 +119,7 @@ function buildExcludeRenderersSubmenu(
117
119
  const excluded = new Set(config.excludeRenderers);
118
120
  if (value === "exclude") excluded.add(id);
119
121
  else excluded.delete(id);
120
- config.excludeRenderers = [...excluded].sort((a, b) => a.localeCompare(b));
121
- saveConfig();
122
+ updateConfig({ excludeRenderers: [...excluded].sort((a, b) => a.localeCompare(b)) });
122
123
  onLiveChange();
123
124
  },
124
125
  () => onClose(),
@@ -414,9 +415,8 @@ export async function showCcstylePanel(
414
415
  // 额外功能开关:字段名与配置布尔字段一一对应,切换后重启生效。
415
416
  const featureToggle = featureToggles[id];
416
417
  if (featureToggle) {
417
- (config as unknown as Record<string, boolean>)[id] = value === "on";
418
+ updateConfig({ [id]: value === "on" } as Partial<Config>);
418
419
  featureToggle.apply(value === "on");
419
- saveConfig();
420
420
  ctx.ui.notify(`Updated ${id}: ${value} (next restart)`, "info");
421
421
  return;
422
422
  }
@@ -434,62 +434,57 @@ export async function showCcstylePanel(
434
434
  excludeSetting.description = excludeRenderersDescription(config.excludeRenderers);
435
435
  return;
436
436
  case "diffViewMode":
437
- config.diffViewMode = value as DiffViewMode;
437
+ updateConfig({ diffViewMode: value as DiffViewMode });
438
438
  diffViewSetting.description = diffViewModeDescription(config.diffViewMode);
439
439
  break;
440
440
  case "diffIndicatorMode":
441
- config.diffIndicatorMode = value as DiffIndicatorMode;
441
+ updateConfig({ diffIndicatorMode: value as DiffIndicatorMode });
442
442
  diffIndicatorSetting.description = diffIndicatorDescription(config.diffIndicatorMode);
443
443
  break;
444
444
  case "diffSplitMinWidth":
445
- config.diffSplitMinWidth = pickPositiveInt(
446
- value,
447
- DEFAULT_CONFIG.diffSplitMinWidth,
448
- 40,
449
- 300,
450
- );
445
+ updateConfig({
446
+ diffSplitMinWidth: pickPositiveInt(value, DEFAULT_CONFIG.diffSplitMinWidth, 40, 300),
447
+ });
451
448
  diffSplitSetting.currentValue = String(config.diffSplitMinWidth);
452
449
  break;
453
450
  case "diffCollapsedLines":
454
- config.diffCollapsedLines = pickPositiveInt(
455
- value,
456
- DEFAULT_CONFIG.diffCollapsedLines,
457
- 1,
458
- 500,
459
- );
451
+ updateConfig({
452
+ diffCollapsedLines: pickPositiveInt(value, DEFAULT_CONFIG.diffCollapsedLines, 1, 500),
453
+ });
460
454
  diffCollapsedSetting.currentValue = String(config.diffCollapsedLines);
461
455
  break;
462
456
  case "diffWordWrap":
463
- config.diffWordWrap = value === "on";
457
+ updateConfig({ diffWordWrap: value === "on" });
464
458
  diffWordWrapSetting.description = config.diffWordWrap
465
459
  ? "Long diff lines wrap within the panel width."
466
460
  : "Long diff lines are truncated to the panel width.";
467
461
  break;
468
462
  case "expandedPreviewMaxLines":
469
- config.expandedPreviewMaxLines = pickPositiveInt(
470
- value,
471
- DEFAULT_CONFIG.expandedPreviewMaxLines,
472
- 10,
473
- 50_000,
474
- );
463
+ updateConfig({
464
+ expandedPreviewMaxLines: pickPositiveInt(
465
+ value,
466
+ DEFAULT_CONFIG.expandedPreviewMaxLines,
467
+ 10,
468
+ 50_000,
469
+ ),
470
+ });
475
471
  expandedMaxSetting.currentValue = String(config.expandedPreviewMaxLines);
476
472
  break;
477
473
  case "useSummaryTitlesAsThinkingTitle":
478
- config.useSummaryTitlesAsThinkingTitle = value === "on";
474
+ updateConfig({ useSummaryTitlesAsThinkingTitle: value === "on" });
479
475
  break;
480
476
  case "previewLines":
481
- config.previewLines = pickPositiveInt(value, DEFAULT_CONFIG.previewLines, 0);
477
+ updateConfig({ previewLines: pickPositiveInt(value, DEFAULT_CONFIG.previewLines, 0) });
482
478
  thinkingPreviewSetting.currentValue = String(config.previewLines);
483
479
  break;
484
480
  case "animationIntervalMs":
485
- config.animationIntervalMs = pickPositiveNumber(
486
- value,
487
- DEFAULT_CONFIG.animationIntervalMs,
488
- );
481
+ updateConfig({
482
+ animationIntervalMs: pickPositiveNumber(value, DEFAULT_CONFIG.animationIntervalMs),
483
+ });
489
484
  thinkingAnimationSetting.currentValue = String(config.animationIntervalMs);
490
485
  break;
491
486
  case "showStartupHeader":
492
- config.showStartupHeader = value === "on";
487
+ updateConfig({ showStartupHeader: value === "on" });
493
488
  startupHeaderSetting.description = config.showStartupHeader
494
489
  ? "Show the custom startup header (logo + tips) on new sessions."
495
490
  : "Use Pi's native startup header instead.";
@@ -497,13 +492,14 @@ export async function showCcstylePanel(
497
492
  applyStartupHeader(ctx);
498
493
  break;
499
494
  case "scrollStepLines":
500
- config.scrollStepLines = pickPositiveInt(value, DEFAULT_CONFIG.scrollStepLines, 1, 50);
495
+ updateConfig({
496
+ scrollStepLines: pickPositiveInt(value, DEFAULT_CONFIG.scrollStepLines, 1, 50),
497
+ });
501
498
  scrollStepSetting.currentValue = String(config.scrollStepLines);
502
499
  break;
503
500
  default:
504
501
  return;
505
502
  }
506
- saveConfig();
507
503
  compactThinking?.updateConfig(getCompactThinkingConfig());
508
504
  hooks.refreshCurrentTranscript(ctx);
509
505
  ctx.ui.notify(`Updated ${id}: ${value}`, "info");
@@ -19,6 +19,12 @@ import {
19
19
  styleCompactThinkingText,
20
20
  } from "../renderer/compact-mode.ts";
21
21
  import { refreshMountedTranscript } from "../renderer/transcript-refresh.ts";
22
+ import {
23
+ COMPACT_THINKING_OWNER,
24
+ COMPACT_THINKING_PATCH_KEY,
25
+ patchRegistry,
26
+ PROTOTYPE_ORIGINAL_KEY,
27
+ } from "../utils/patch-keys.ts";
22
28
  // 保持导出兼容:渲染函数已并入 renderer/compact-mode.ts,这里 re-export。
23
29
  export { animateCompactThinkingText, formatThoughtDuration, styleCompactThinkingText };
24
30
 
@@ -106,8 +112,6 @@ const config: CompactThinkingConfig = {
106
112
  };
107
113
 
108
114
  const DURATION_ENTRY_TYPE = "compact-thinking-duration";
109
- const COMPACT_THINKING_PATCH_KEY = Symbol.for("pi.ccstyle.compact-thinking-update");
110
- const PROTOTYPE_ORIGINAL_KEY = Symbol.for("pi.ccstyle.prototype-original");
111
115
 
112
116
  /** 当前激活 session 的只读查询委托(compact 渲染层使用)。 */
113
117
  type ThinkingDurationQuery = (messageTimestamp: number) => number | undefined;
@@ -747,8 +751,6 @@ type CompactThinkingOwner = {
747
751
  stop(event?: any, ctx?: any): void;
748
752
  };
749
753
 
750
- const COMPACT_THINKING_OWNER = Symbol.for("pi.ccstyle.compact-thinking-owner");
751
-
752
754
  type UpstreamHandler = (event: any, ctx: any) => void;
753
755
 
754
756
  export function installCompactThinking(
@@ -756,10 +758,6 @@ export function installCompactThinking(
756
758
  initialConfig: CompactThinkingConfig,
757
759
  ): CompactThinkingController {
758
760
  const owner = {};
759
- const host = globalThis as typeof globalThis & {
760
- [COMPACT_THINKING_OWNER]?: CompactThinkingOwner;
761
- };
762
-
763
761
  let session: { event: any; ctx: any } | undefined;
764
762
  let active = false;
765
763
  // Stable pi.on wrappers delegate here so activate/reload never double-binds.
@@ -795,7 +793,8 @@ export function installCompactThinking(
795
793
  const shutdown = delegates.get("session_shutdown");
796
794
  delegates.clear();
797
795
  shutdown?.(event ?? session?.event ?? {}, ctx ?? session?.ctx ?? { mode: "rpc", ui: {} });
798
- if (host[COMPACT_THINKING_OWNER]?.owner === owner) delete host[COMPACT_THINKING_OWNER];
796
+ if (patchRegistry.get<CompactThinkingOwner>(COMPACT_THINKING_OWNER)?.owner === owner)
797
+ patchRegistry.delete(COMPACT_THINKING_OWNER);
799
798
  };
800
799
 
801
800
  const activate = (event: any, ctx: any) => {
@@ -803,7 +802,7 @@ export function installCompactThinking(
803
802
  // TUI prototype patch or kill its thinking ticker.
804
803
  if (ctx?.mode !== "tui") return;
805
804
 
806
- host[COMPACT_THINKING_OWNER]?.stop(event, ctx);
805
+ patchRegistry.get<CompactThinkingOwner>(COMPACT_THINKING_OWNER)?.stop(event, ctx);
807
806
  session = { event, ctx };
808
807
 
809
808
  // 配置统一由 claude-code-style 管控,加载时覆盖库默认值。
@@ -828,7 +827,7 @@ export function installCompactThinking(
828
827
  } as unknown as ExtensionAPI);
829
828
 
830
829
  active = true;
831
- host[COMPACT_THINKING_OWNER] = { owner, stop };
830
+ patchRegistry.install(COMPACT_THINKING_OWNER, { owner, stop });
832
831
  };
833
832
 
834
833
  pi.on("session_start", (event, ctx) => {
@@ -836,7 +835,8 @@ export function installCompactThinking(
836
835
  activate(event, ctx);
837
836
  });
838
837
  pi.on("session_shutdown", (event, ctx) => {
839
- if (host[COMPACT_THINKING_OWNER]?.owner === owner) stop(event, ctx);
838
+ if (patchRegistry.get<CompactThinkingOwner>(COMPACT_THINKING_OWNER)?.owner === owner)
839
+ stop(event, ctx);
840
840
  session = undefined;
841
841
  });
842
842
 
@@ -1,10 +1,14 @@
1
1
  import {
2
+ type BuildSystemPromptOptions,
2
3
  type ExtensionAPI,
3
4
  type ExtensionCommandContext,
5
+ type ToolInfo,
4
6
  estimateTokens,
5
7
  getMarkdownTheme,
6
8
  } from "@earendil-works/pi-coding-agent";
7
- import { Key, Markdown, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { Key, Markdown, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { mouseBaseButton, parseSgrMousePacket } from "../utils/sgr-mouse.ts";
11
+ import { padLine } from "../utils/format.ts";
8
12
 
9
13
  export type ContextPart = {
10
14
  label: string;
@@ -189,10 +193,6 @@ export async function showTextPreview(
189
193
  escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
190
194
  const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
191
195
  const border = (text: string) => theme.fg("border", text);
192
- const padLine = (text: string, lineWidth = inner): string => {
193
- const truncated = truncateToWidth(text, lineWidth, "…");
194
- return truncated + " ".repeat(Math.max(0, lineWidth - visibleWidth(truncated)));
195
- };
196
196
  const scrollable = totalLines > pageSize;
197
197
  const thumbSize = scrollable
198
198
  ? Math.max(1, Math.floor((pageSize * pageSize) / totalLines))
@@ -231,7 +231,7 @@ export async function showTextPreview(
231
231
  `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
232
232
  ...bodyRows,
233
233
  `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
234
- `${border("│")}${padLine(theme.fg("dim", ` ${status}`))}${border("│")}`,
234
+ `${border("│")}${padLine(theme.fg("dim", ` ${status}`), inner)}${border("│")}`,
235
235
  border(`╰${"─".repeat(inner)}╯`),
236
236
  ];
237
237
  },
@@ -253,40 +253,12 @@ export async function showTextPreview(
253
253
  }
254
254
  }
255
255
 
256
- type SgrMousePacket = {
257
- code: number;
258
- col: number;
259
- row: number;
260
- final: "M" | "m";
261
- };
262
-
263
- function parseSgrMousePacket(data: string): SgrMousePacket | null {
264
- const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
265
- if (!match) return null;
266
- return {
267
- code: Number(match[1]),
268
- col: Number(match[2]),
269
- row: Number(match[3]),
270
- final: match[4] as "M" | "m",
271
- };
272
- }
273
-
274
- function mouseBaseButton(code: number): number {
275
- return code & ~(4 | 8 | 16 | 32);
276
- }
277
-
278
256
  const tokenEstimate = (value: unknown): number => {
279
257
  if (!value) return 0;
280
258
  const text = typeof value === "string" ? value : JSON.stringify(value);
281
259
  return Math.max(0, Math.ceil(text.length / 4));
282
260
  };
283
261
 
284
- type ToolPreviewInfo = {
285
- name: string;
286
- description?: string;
287
- promptGuidelines?: string[];
288
- };
289
-
290
262
  export function scaleParts(parts: ContextPart[], target: number): ContextPart[] {
291
263
  const estimated = parts.reduce((sum, part) => sum + part.tokens, 0);
292
264
  if (estimated === 0 || target <= 0) return parts;
@@ -309,22 +281,9 @@ export function formatTokens(tokens: number): string {
309
281
  return `${Math.round(tokens / 1_000)}k`;
310
282
  }
311
283
 
312
- type SystemPromptOptions = {
313
- contextFiles?: Array<{ path: string; content: string }>;
314
- skills?: Array<{
315
- name: string;
316
- description?: string;
317
- filePath?: string;
318
- disableModelInvocation?: boolean;
319
- }>;
320
- selectedTools?: string[];
321
- toolSnippets?: Record<string, string>;
322
- promptGuidelines?: string[];
323
- };
324
-
325
284
  type ContextBreakdown = {
326
285
  parts: ContextPart[];
327
- options: SystemPromptOptions;
286
+ options: BuildSystemPromptOptions;
328
287
  systemPrompt: string;
329
288
  };
330
289
 
@@ -333,7 +292,7 @@ type ContextBreakdown = {
333
292
  * so the /context UI does not re-fetch or re-stringify the same sources.
334
293
  */
335
294
  function collectContextBreakdown(ctx: ExtensionCommandContext): ContextBreakdown {
336
- const options = (ctx.getSystemPromptOptions?.() ?? {}) as SystemPromptOptions;
295
+ const options = (ctx.getSystemPromptOptions?.() ?? {}) as BuildSystemPromptOptions;
337
296
  const systemPrompt = typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "";
338
297
 
339
298
  const contextFileTokens = (options.contextFiles ?? []).reduce(
@@ -410,8 +369,8 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
410
369
  }
411
370
 
412
371
  const options = breakdown.options;
413
- const toolByName = new Map<string, ToolPreviewInfo>(
414
- (pi.getAllTools() as ToolPreviewInfo[]).map((tool) => [tool.name, tool] as const),
372
+ const toolByName = new Map<string, ToolInfo>(
373
+ pi.getAllTools().map((tool) => [tool.name, tool] as const),
415
374
  );
416
375
  const toolContent = (options.selectedTools ?? []).map((name) => {
417
376
  const tool = toolByName.get(name);
@@ -490,11 +449,6 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
490
449
  }> = [];
491
450
  let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
492
451
 
493
- const padLine = (text: string, width: number): string => {
494
- const truncated = truncateToWidth(text, width, "…");
495
- return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
496
- };
497
-
498
452
  return {
499
453
  invalidate() {},
500
454
  handleInput(data: string) {
@@ -109,7 +109,7 @@ function trackSubagentFromEvent(data: unknown): void {
109
109
  // Started event — create new record
110
110
  const agent = typeof event.agent === "string" ? event.agent : "";
111
111
  const cwd = typeof event.cwd === "string" ? event.cwd : "";
112
- const startedAt = typeof event.startedAt === "number" ? (event.startedAt as number) : Date.now();
112
+ const startedAt = typeof event.startedAt === "number" ? event.startedAt : Date.now();
113
113
  liveSubagentRecords.set(runId, {
114
114
  runId,
115
115
  sessionId,
@@ -122,7 +122,9 @@ function trackSubagentFromEvent(data: unknown): void {
122
122
 
123
123
  function getSubagentManager(): SubagentManager | undefined {
124
124
  // Try the global manager first (future-proof), fall back to local records.
125
- const manager = (globalThis as any)[SUBAGENT_MANAGER_KEY] as SubagentManager | undefined;
125
+ const manager = (globalThis as Record<PropertyKey, unknown>)[SUBAGENT_MANAGER_KEY] as
126
+ | SubagentManager
127
+ | undefined;
126
128
  if (manager && typeof manager.getRecord === "function") return manager;
127
129
  // If no global manager, use our local tracking.
128
130
  if (liveSubagentRecords.size === 0) return undefined;
@@ -295,7 +295,7 @@ export function applyStartupHeader(ctx: any): void {
295
295
  ctx.ui.setHeader(undefined);
296
296
  return;
297
297
  }
298
- ctx.ui.setHeader((_tui, theme) => ({
298
+ ctx.ui.setHeader((_tui: unknown, theme: any) => ({
299
299
  render(width: number): string[] {
300
300
  return renderHeaderLines(width, theme);
301
301
  },
@@ -9,21 +9,24 @@ function formatCount(value: number): string {
9
9
  return new Intl.NumberFormat("en-US").format(value);
10
10
  }
11
11
 
12
- function estimateTextLength(message: any): number {
13
- if (!Array.isArray(message?.content)) return 0;
14
- return message.content.reduce((sum: number, block: any) => {
15
- if (block?.type === "text" && typeof block.text === "string") return sum + block.text.length;
16
- if (block?.type === "thinking" && block.thinkingSignature?.body)
17
- return sum + (block.thinkingSignature.body as string).length;
18
- return sum;
19
- }, 0);
20
- }
21
-
22
- function textBlockLengths(message: any): number[] {
23
- if (!Array.isArray(message?.content)) return [];
12
+ type ContentBlock = {
13
+ type?: unknown;
14
+ text?: unknown;
15
+ thinkingSignature?: { body?: unknown };
16
+ };
17
+
18
+ type StreamMessage = {
19
+ content?: unknown;
20
+ usage?: { output?: unknown };
21
+ };
22
+
23
+ /** 每个 content index 的可见文本/思考长度;无对应块的 index 保持稀疏洞。 */
24
+ function textBlockLengths(message: StreamMessage): number[] {
25
+ const content = message.content;
26
+ if (!Array.isArray(content)) return [];
24
27
  const lengths: number[] = [];
25
- for (let index = 0; index < message.content.length; index++) {
26
- const block = message.content[index];
28
+ for (let index = 0; index < content.length; index++) {
29
+ const block = content[index] as ContentBlock;
27
30
  if (block?.type === "text" && typeof block.text === "string") {
28
31
  lengths[index] = block.text.length;
29
32
  } else if (block?.type === "thinking" && block.thinkingSignature?.body) {
@@ -33,11 +36,15 @@ function textBlockLengths(message: any): number[] {
33
36
  return lengths;
34
37
  }
35
38
 
36
- function outputUsage(message: any): number {
39
+ function outputUsage(message: StreamMessage): number {
37
40
  const value = Number(message?.usage?.output);
38
41
  return Number.isFinite(value) && value > 0 ? Math.round(value) : 0;
39
42
  }
40
43
 
44
+ type WorkingUi = {
45
+ setWorkingMessage(message?: string): void;
46
+ };
47
+
41
48
  /**
42
49
  * Extend Pi's footer working row while preserving its spinner and "Working...":
43
50
  * `⠋ Working... (↓ 1,234 tokens · 12s)`
@@ -54,7 +61,7 @@ export default function (pi: ExtensionAPI): void {
54
61
  let providerOutputTokens = 0;
55
62
  let refreshTimer: ReturnType<typeof setTimeout> | null = null;
56
63
  let lastMessage: string | null = null;
57
- let activeCtx: { ui: any; hasUI: boolean } | null = null;
64
+ let activeCtx: { ui: WorkingUi | undefined; hasUI: boolean } | null = null;
58
65
 
59
66
  function tokenCount(): number {
60
67
  return providerOutputTokens || Math.max(0, Math.round(responseLength / 4));
@@ -66,13 +73,13 @@ export default function (pi: ExtensionAPI): void {
66
73
  responseLength = Math.max(0, responseLength + responseTextBlockLengths[index] - previous);
67
74
  }
68
75
 
69
- function resetResponseTracking(message?: any): void {
76
+ function resetResponseTracking(message?: StreamMessage): void {
70
77
  responseTextBlockLengths = message ? textBlockLengths(message) : [];
71
- responseLength = message ? estimateTextLength(message) : 0;
78
+ responseLength = responseTextBlockLengths.reduce((sum, length) => sum + length, 0);
72
79
  providerOutputTokens = message ? outputUsage(message) : 0;
73
80
  }
74
81
 
75
- function updateProviderUsage(message: any): void {
82
+ function updateProviderUsage(message: StreamMessage): void {
76
83
  const output = outputUsage(message);
77
84
  if (output > 0) providerOutputTokens = output;
78
85
  }
@@ -93,7 +100,7 @@ export default function (pi: ExtensionAPI): void {
93
100
  lastMessage = null;
94
101
  if (!activeCtx?.hasUI) return;
95
102
  try {
96
- activeCtx.ui.setWorkingMessage();
103
+ activeCtx.ui?.setWorkingMessage();
97
104
  } catch {
98
105
  // Noop when the TUI is unavailable.
99
106
  }
@@ -109,7 +116,7 @@ export default function (pi: ExtensionAPI): void {
109
116
  if (!force && next === lastMessage) return;
110
117
  lastMessage = next;
111
118
  try {
112
- activeCtx.ui.setWorkingMessage(next);
119
+ activeCtx.ui?.setWorkingMessage(next);
113
120
  } catch {
114
121
  // Noop when the TUI is unavailable.
115
122
  }