pi-cc-extensions 0.9.8 → 0.9.9

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
@@ -60,7 +60,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
60
60
  "expandedInputMaxLines": 5, // expanded tool Input lines; overflow shows a footer hint
61
61
  "expandedOutputMaxLines": 10, // expanded tool Output lines; overflow shows a footer hint
62
62
  "expandedPreviewMaxLines": 40, // max lines for expanded TaskList bodies (expanded diffs always show all)
63
- "inputClip": 100, // tool summary path/command clip length
63
+ "inputClip": 0, // tool summary path/command clip length; 0 = fit width
64
64
  "showStartupHeader": true, // startup header (logo + tips) toggle
65
65
  "scrollStepLines": 3, // fullscreen wheel scroll step
66
66
 
package/README.md CHANGED
@@ -60,7 +60,7 @@ pi install git:github.com/minuque/pi-cc-extensions
60
60
  "expandedInputMaxLines": 5, // 展开工具卡 Input 可见行数,超出在末行显示展开提示
61
61
  "expandedOutputMaxLines": 10, // 展开工具卡 Output 可见行数,超出在末行显示展开提示
62
62
  "expandedPreviewMaxLines": 40, // 展开 TaskList 正文最大行数(展开 diff 始终全量)
63
- "inputClip": 100, // 工具摘要 path/command 折叠字符数
63
+ "inputClip": 0, // 工具摘要 path/command 折叠字符数;0 = 按可用宽度
64
64
  "showStartupHeader": true, // 启动头(logo + tips)开关
65
65
  "scrollStepLines": 3, // fullscreen 滚轮步进
66
66
 
@@ -92,8 +92,8 @@ export const EXPANDED_PREVIEW_MAX_LINES_VALUES = ["40", "60", "80", "120", "200"
92
92
  export const EXPANDED_INPUT_MAX_LINES_VALUES = ["5", "10", "20", "40", "80"];
93
93
  /** 展开工具卡 Output 可见行数预设。 */
94
94
  export const EXPANDED_OUTPUT_MAX_LINES_VALUES = ["10", "20", "40", "80", "120"];
95
- /** 工具摘要里 path/command 等输入的折叠字符数。 */
96
- export const INPUT_CLIP_VALUES = ["40", "60", "80", "100", "120", "160"];
95
+ /** 工具摘要里 path/command 等输入的折叠字符数;0 = 只按可用宽度截断。 */
96
+ export const INPUT_CLIP_VALUES = ["0", "40", "60", "80", "100", "120", "160"];
97
97
  export const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
98
98
  export const THINKING_ANIMATION_INTERVAL_VALUES = ["40", "60", "90", "120", "180"];
99
99
  /** fullscreen 滚轮步进行数预设。 */
@@ -124,7 +124,7 @@ export const DEFAULT_CONFIG: Config = {
124
124
  expandedPreviewMaxLines: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
125
125
  expandedInputMaxLines: 5,
126
126
  expandedOutputMaxLines: 10,
127
- inputClip: 100,
127
+ inputClip: 0,
128
128
  useSummaryTitlesAsThinkingTitle: true,
129
129
  previewLines: 3,
130
130
  animationIntervalMs: 90,
@@ -154,6 +154,12 @@ export function pickPositiveInt(value: unknown, fallback: number, min = 1, max =
154
154
  return Math.min(max, Math.max(min, Math.floor(n)));
155
155
  }
156
156
 
157
+ /** inputClip:0 表示按宽度自适应,其余夹到 8..500。 */
158
+ export function pickInputClip(value: unknown): number {
159
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
160
+ return n === 0 ? 0 : pickPositiveInt(value, DEFAULT_CONFIG.inputClip, 8, 500);
161
+ }
162
+
157
163
  export function pickPositiveNumber(value: unknown, fallback: number, min = 1): number {
158
164
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
159
165
  return Number.isFinite(n) ? Math.max(min, n) : fallback;
@@ -217,7 +223,7 @@ export function normalizeConfig(input: unknown): Config {
217
223
  1,
218
224
  5_000,
219
225
  ),
220
- inputClip: pickPositiveInt(source.inputClip, DEFAULT_CONFIG.inputClip, 8, 500),
226
+ inputClip: pickInputClip(source.inputClip),
221
227
  useSummaryTitlesAsThinkingTitle: source.useSummaryTitlesAsThinkingTitle !== false,
222
228
  previewLines: pickPositiveInt(
223
229
  source.previewLines,
@@ -38,6 +38,7 @@ import {
38
38
  EXPANDED_PREVIEW_MAX_LINES_VALUES,
39
39
  formatExcludeRenderers,
40
40
  getCompactThinkingConfig,
41
+ pickInputClip,
41
42
  pickPositiveInt,
42
43
  pickPositiveNumber,
43
44
  SCROLL_STEP_LINES_VALUES,
@@ -403,7 +404,7 @@ export async function showCcstylePanel(
403
404
  id: "inputClip",
404
405
  label: "Input clip",
405
406
  description:
406
- "Max characters for path/command/name in single and grouped tool summaries. Enter to type a custom value.",
407
+ "Max characters for path/command/name in single and grouped tool summaries; 0 fits available width. Enter to type a custom value.",
407
408
  currentValue: String(config.inputClip),
408
409
  values: [...INPUT_CLIP_VALUES],
409
410
  submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
@@ -667,7 +668,7 @@ export async function showCcstylePanel(
667
668
  switch (id) {
668
669
  case "inputClip":
669
670
  updateConfig({
670
- inputClip: pickPositiveInt(value, DEFAULT_CONFIG.inputClip, 8, 500),
671
+ inputClip: pickInputClip(value),
671
672
  });
672
673
  inputClipSetting.currentValue = String(config.inputClip);
673
674
  break;
@@ -1,9 +1,14 @@
1
- import { VERSION, type AppKeybinding } from "@earendil-works/pi-coding-agent";
1
+ import { InteractiveMode, VERSION, type AppKeybinding } from "@earendil-works/pi-coding-agent";
2
2
  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
6
  import { stripAnsi } from "../../utils/ansi-text.ts";
7
+ import {
8
+ EARLY_STARTUP_HEADER_PATCH,
9
+ EARLY_STARTUP_HEADER_SWAP_KEY,
10
+ patchRegistry,
11
+ } from "../../utils/patch-keys.ts";
7
12
  type Rgb = [number, number, number];
8
13
  type StyledPart = {
9
14
  raw: string;
@@ -279,17 +284,88 @@ export function renderHeaderLines(
279
284
  );
280
285
  }
281
286
 
287
+ /** 官方 setHeader 的组件工厂:theme 由宿主注入。 */
288
+ function headerFactory(): (
289
+ tui: unknown,
290
+ theme: any,
291
+ ) => {
292
+ render(width: number): string[];
293
+ invalidate(): void;
294
+ } {
295
+ return (_tui: unknown, theme: any) => ({
296
+ render(width: number): string[] {
297
+ return renderHeaderLines(width, theme);
298
+ },
299
+ invalidate() {},
300
+ });
301
+ }
302
+
282
303
  /** 按配置安装启动头。禁用时不碰槽位,避免清掉其他扩展的 header。 */
283
304
  export function applyStartupHeader(ctx: any): void {
284
305
  if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function" || !config.showStartupHeader) {
285
306
  return;
286
307
  }
287
- ctx.ui.setHeader((_tui: unknown, theme: any) => ({
288
- render(width: number): string[] {
289
- return renderHeaderLines(width, theme);
308
+ ctx.ui.setHeader(headerFactory());
309
+ }
310
+
311
+ type EarlyHeaderPatch = {
312
+ active: boolean;
313
+ original: (...args: any[]) => unknown;
314
+ installed: (...args: any[]) => unknown;
315
+ };
316
+
317
+ type HeaderContainerSwap = {
318
+ original: (child: any) => unknown;
319
+ installed: (child: any) => unknown;
320
+ };
321
+
322
+ /**
323
+ * Pi 在 session_start 之前就把原生启动头画上去了:init() 先建 header 再 requestRender,
324
+ * 之后才 await 工具检查、加载扩展,所以扩展最早只能到 session_start 换头,启动时会闪一下原生头。
325
+ * 这里补 init,在原生 header 刚进容器的瞬间换成 ccstyle 的,首次绘制就已经是我们的。
326
+ * 判定用 child === mode.builtInHeader,不依赖类名;配置关掉时不碰槽位,保留原生头。
327
+ */
328
+ export function installEarlyStartupHeader(): void {
329
+ const prototype = (InteractiveMode as any)?.prototype;
330
+ if (!prototype || typeof prototype.init !== "function") return;
331
+ const previous = patchRegistry.get<EarlyHeaderPatch>(EARLY_STARTUP_HEADER_PATCH);
332
+ if (previous) previous.active = false;
333
+ const original =
334
+ previous && prototype.init === previous.installed ? previous.original : prototype.init;
335
+ const patch: EarlyHeaderPatch = {
336
+ active: true,
337
+ original,
338
+ installed: async function (this: any, ...args: any[]) {
339
+ installHeaderContainerSwap(this, patch);
340
+ return original.apply(this, args);
290
341
  },
291
- invalidate() {},
292
- }));
342
+ };
343
+ prototype.init = patch.installed;
344
+ patchRegistry.install(EARLY_STARTUP_HEADER_PATCH, patch);
345
+ }
346
+
347
+ /** 只包一次 headerContainer.addChild;/reload 后按所有权接管旧包装。 */
348
+ function installHeaderContainerSwap(mode: any, patch: EarlyHeaderPatch): void {
349
+ const container = mode?.headerContainer;
350
+ if (!container || typeof container.addChild !== "function") return;
351
+ const previous = container[EARLY_STARTUP_HEADER_SWAP_KEY] as HeaderContainerSwap | undefined;
352
+ const original =
353
+ previous && container.addChild === previous.installed
354
+ ? previous.original
355
+ : container.addChild.bind(container);
356
+ const installed = function (child: any) {
357
+ const result = original(child);
358
+ if (!patch.active || !config.showStartupHeader || child !== mode.builtInHeader) return result;
359
+ if (typeof mode.setExtensionHeader !== "function") return result;
360
+ try {
361
+ mode.setExtensionHeader(headerFactory());
362
+ } catch {
363
+ // 换头失败就退回原生 header,不影响启动
364
+ }
365
+ return result;
366
+ };
367
+ container.addChild = installed;
368
+ container[EARLY_STARTUP_HEADER_SWAP_KEY] = { original, installed };
293
369
  }
294
370
 
295
371
  /** 用户从 /ccstyle 关掉本扩展启动头时,恢复官方 header。 */
@@ -115,18 +115,30 @@ export default function (pi: ExtensionAPI): void {
115
115
  return parts.length ? `Working... (${parts.join(" · ")})` : "";
116
116
  }
117
117
 
118
+ function workingUiAvailable(): boolean {
119
+ try {
120
+ return activeCtx?.hasUI === true;
121
+ } catch {
122
+ // 会话替换/reload 后捕获的 ctx 失效,getter 抛错;停止驱动 footer。
123
+ turnActive = false;
124
+ activeCtx = null;
125
+ stopRefreshLoop();
126
+ return false;
127
+ }
128
+ }
129
+
118
130
  function restoreDefaultWorkingMessage(): void {
119
131
  lastMessage = null;
120
- if (!activeCtx?.hasUI) return;
132
+ if (!workingUiAvailable()) return;
121
133
  try {
122
- activeCtx.ui?.setWorkingMessage();
134
+ activeCtx?.ui?.setWorkingMessage();
123
135
  } catch {
124
136
  // Noop when the TUI is unavailable.
125
137
  }
126
138
  }
127
139
 
128
140
  function syncWorkingMessage(force = false): void {
129
- if (!activeCtx?.hasUI) return;
141
+ if (!workingUiAvailable()) return;
130
142
  const next = buildWorkingMessage();
131
143
  if (!next) {
132
144
  if (force) restoreDefaultWorkingMessage();
@@ -135,7 +147,7 @@ export default function (pi: ExtensionAPI): void {
135
147
  if (!force && next === lastMessage) return;
136
148
  lastMessage = next;
137
149
  try {
138
- activeCtx.ui?.setWorkingMessage(next);
150
+ activeCtx?.ui?.setWorkingMessage(next);
139
151
  } catch {
140
152
  // Noop when the TUI is unavailable.
141
153
  }
@@ -147,9 +159,12 @@ export default function (pi: ExtensionAPI): void {
147
159
  refreshTimer = null;
148
160
  try {
149
161
  syncWorkingMessage();
150
- } finally {
151
- scheduleRefreshTick();
162
+ } catch {
163
+ // 定时器内的异常会成为 uncaughtException 终止 Pi;装饰性刷新直接停止。
164
+ turnActive = false;
165
+ return;
152
166
  }
167
+ scheduleRefreshTick();
153
168
  }, REFRESH_INTERVAL_MS);
154
169
  refreshTimer.unref?.();
155
170
  }
@@ -5,7 +5,7 @@ import { config } from "./config/config.ts";
5
5
  import piAliases from "./feature/shell/aliases.ts";
6
6
  import { installFlushDockedBash } from "./feature/shell/flush-docked-bash.ts";
7
7
  import customFooter from "./feature/shell/footer.ts";
8
- import piStartupHeader from "./feature/shell/startup-header.ts";
8
+ import piStartupHeader, { installEarlyStartupHeader } from "./feature/shell/startup-header.ts";
9
9
  import workingMessage from "./feature/shell/working-message.ts";
10
10
 
11
11
  // feature
@@ -23,6 +23,7 @@ export default function (pi: ExtensionAPI): void {
23
23
  // shell chrome
24
24
  if (config.enableAliases) piAliases(pi);
25
25
  installFlushDockedBash();
26
+ installEarlyStartupHeader();
26
27
  piStartupHeader(pi);
27
28
  if (config.enableWorkingMessage) workingMessage(pi);
28
29
  customFooter(pi);
@@ -1167,6 +1167,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1167
1167
  stopRoundTick();
1168
1168
  return;
1169
1169
  }
1170
+ settleStrandedRounds();
1170
1171
  try {
1171
1172
  // 非 force:保留 fullscreen 布局缓存和差分绘制。
1172
1173
  uiRef?.requestRender?.();
@@ -1320,12 +1321,36 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1320
1321
  return found;
1321
1322
  };
1322
1323
 
1324
+ /** 组件在树里的直属父容器。TUI 切换/reparent 后缓存的 container 会过期。 */
1325
+ const containerOfHost = (host: any): any => {
1326
+ let found: any;
1327
+ walkComponentTree(getToolMouseTui(), (value: any) => {
1328
+ if (found) return false;
1329
+ if (Array.isArray(value?.children) && value.children.includes(host)) {
1330
+ found = value;
1331
+ return false;
1332
+ }
1333
+ return undefined;
1334
+ });
1335
+ return found;
1336
+ };
1337
+
1338
+ /** 尾行实际所在容器;缓存的 container 过期时按树找回来。 */
1339
+ const tailHostContainer = (tail: MountedRoundTail): any =>
1340
+ containerOfHost(tail.host) ??
1341
+ (tail.container?.children?.includes?.(tail.host) === true ? tail.container : undefined);
1342
+
1343
+ /** 摘尾行:按实际所在容器摘,避免缓存过期时摘了个空、行留在树上。 */
1344
+ const removeTailHost = (tail: MountedRoundTail): void => {
1345
+ tailHostContainer(tail)?.removeChild?.(tail.host);
1346
+ };
1347
+
1323
1348
  const ensureTailHost = (round: CompactRound): MountedRoundTail => {
1324
1349
  if (round.tail) return round.tail;
1325
1350
  // 同一 anchor 上残留的旧回合尾行(reset/重组后建新回合)先摘除,避免双摘要。
1326
1351
  const stale = tailByAnchor.get(round.anchor);
1327
1352
  if (stale) {
1328
- stale.container?.removeChild?.(stale.host);
1353
+ removeTailHost(stale);
1329
1354
  mountedTails.delete(stale);
1330
1355
  tailByAnchor.delete(round.anchor);
1331
1356
  }
@@ -1410,7 +1435,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1410
1435
  const tail = round.tail;
1411
1436
  if (!tail) return;
1412
1437
  round.tail = undefined;
1413
- tail.container?.removeChild?.(tail.host);
1438
+ removeTailHost(tail);
1414
1439
  mountedTails.delete(tail);
1415
1440
  if (tailByAnchor.get(round.anchor) === tail) tailByAnchor.delete(round.anchor);
1416
1441
  };
@@ -1442,7 +1467,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1442
1467
  const stale = tail.round as CompactRound | undefined;
1443
1468
  if (
1444
1469
  stale &&
1445
- (stale.messages.get(round.anchor) === message || stale.detachedMessages.includes(message))
1470
+ // 先认组件:重放时消息对象未必是同一个(会话重读/重建),
1471
+ // 只认消息身份会漏掉本该接回的回合,摘要行就此掉队。
1472
+ (stale.anchor === round.anchor ||
1473
+ stale.messages.has(round.anchor) ||
1474
+ stale.detachedMessages.includes(message))
1446
1475
  ) {
1447
1476
  prior = stale;
1448
1477
  break;
@@ -1815,12 +1844,14 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1815
1844
  // 一致;同 anchor 重建回合时 ensureTailHost 去重摘除),非 compact 摘掉,
1816
1845
  // 别把紧凑摘要带进 default 渲染。
1817
1846
  for (const tail of [...mountedTails]) {
1818
- if (tail.container?.children?.includes?.(tail.host) !== true) {
1847
+ const container = tailHostContainer(tail);
1848
+ if (!container) {
1819
1849
  mountedTails.delete(tail);
1820
1850
  continue;
1821
1851
  }
1852
+ tail.container = container;
1822
1853
  if (config.mode === "compact") continue;
1823
- tail.container.removeChild(tail.host);
1854
+ container.removeChild(tail.host);
1824
1855
  mountedTails.delete(tail);
1825
1856
  if (tail.round?.tail === tail) tail.round.tail = undefined;
1826
1857
  if (tail.round?.anchor && tailByAnchor.get(tail.round.anchor) === tail) {
@@ -1831,6 +1862,19 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1831
1862
  stopRoundTick();
1832
1863
  };
1833
1864
 
1865
+ /**
1866
+ * 兜底:resetRounds 会丢掉回合索引但保留已挂的尾行,重放没能接回的回合会一直
1867
+ * active——尾行永远显示 Running...(时长还在涨)却不再计入新工具。既不是活动
1868
+ * 回合也不在收尾静默期的活回合就地收尾,不让它永远挂在屏幕上。
1869
+ */
1870
+ const settleStrandedRounds = (): void => {
1871
+ for (const tail of [...mountedTails]) {
1872
+ const round = tail.round as CompactRound | undefined;
1873
+ if (!round?.active || round === activeRound || pendingFoldRounds.has(round)) continue;
1874
+ endRound(round, true);
1875
+ }
1876
+ };
1877
+
1834
1878
  patch.assistantInstalled = function (this: any, message: any, isStreaming?: boolean) {
1835
1879
  const self = this as any;
1836
1880
  // 同 compact-thinking:isStreaming 丢失 → mermaid 流式误渲染来回闪。
@@ -19,6 +19,7 @@ import {
19
19
  toolCallSummary,
20
20
  type ToolCallSummary,
21
21
  } from "./names.ts";
22
+ import { toolViewportWidth } from "./result.ts";
22
23
  import {
23
24
  patchRegistry,
24
25
  TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
@@ -378,7 +379,7 @@ export class ToolGroupComponent extends Container {
378
379
  "",
379
380
  truncateToWidth(
380
381
  ` ${fg(overallColor, "●")} ${label}: ${countText}${nameList} ${hint}`,
381
- width,
382
+ toolViewportWidth(width),
382
383
  "…",
383
384
  ),
384
385
  ];
@@ -411,11 +412,13 @@ export class ToolGroupComponent extends Container {
411
412
  const summary = toolSummary(tool);
412
413
  const prefix = ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `;
413
414
  const detail = fg("dim", summary.detail);
414
- const mainWidth = Math.max(0, width - visibleWidth(prefix) - visibleWidth(detail));
415
+ // 与单工具卡标题同宽,宽屏右侧留白一致
416
+ const rowWidth = toolViewportWidth(width);
417
+ const mainWidth = Math.max(0, rowWidth - visibleWidth(prefix) - visibleWidth(detail));
415
418
  lines.push(
416
419
  truncateToWidth(
417
420
  `${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}`,
418
- width,
421
+ rowWidth,
419
422
  "",
420
423
  ),
421
424
  );
@@ -5,8 +5,13 @@ import { config } from "../../config/config.ts";
5
5
  import { oneLine } from "../../utils/format.ts";
6
6
  import { headTruncateToWidth } from "./result.ts";
7
7
 
8
+ /** inputClip=0 时不设字符上限,交给渲染时的实际宽度截断。 */
9
+ function clipLimit(): number {
10
+ return config.inputClip > 0 ? config.inputClip : Number.POSITIVE_INFINITY;
11
+ }
12
+
8
13
  function clip(value: unknown): string {
9
- return oneLine(value, config.inputClip);
14
+ return oneLine(value, clipLimit());
10
15
  }
11
16
 
12
17
  /** 载荷(入参 JSON / 脚本代码)至少留出这么多宽度才显示,否则整段省略(避免只剩一个孤零零的 " {…")。 */
@@ -125,7 +130,7 @@ export function truncatePathToWidth(path: string, width: number): string {
125
130
 
126
131
  /** 路径展示的统一入口:相对化后按配置和当前可用宽度截断。 */
127
132
  export function formatDisplayPath(value: unknown, cwd: string | undefined, width: number): string {
128
- return truncatePathToWidth(displayPath(value, cwd), Math.min(width, config.inputClip));
133
+ return truncatePathToWidth(displayPath(value, cwd), Math.min(width, clipLimit()));
129
134
  }
130
135
 
131
136
  /** 按最终终端宽度渲染摘要;路径摘要不会再被整行头部截断。 */
@@ -134,7 +139,7 @@ export function fitToolCallSummary(summary: ToolCallSummary, width: number): str
134
139
  const prefix = summary.path.prefix;
135
140
  const pathWidth = Math.max(0, width - visibleWidth(prefix) - 1);
136
141
  if (pathWidth <= 0) return headToWidth(prefix, width, "…");
137
- return `${prefix} ${truncatePathToWidth(summary.path.value, Math.min(pathWidth, config.inputClip))}`;
142
+ return `${prefix} ${truncatePathToWidth(summary.path.value, Math.min(pathWidth, clipLimit()))}`;
138
143
  }
139
144
 
140
145
  function pathSummary(
@@ -145,7 +150,7 @@ function pathSummary(
145
150
  ): ToolCallSummary {
146
151
  const path = displayPath(value, cwd);
147
152
  return {
148
- main: `${prefix} ${truncatePathToWidth(path, config.inputClip)}`,
153
+ main: `${prefix} ${truncatePathToWidth(path, clipLimit())}`,
149
154
  detail,
150
155
  path: { prefix, value: path },
151
156
  };
@@ -7,9 +7,15 @@ import { getToolMouseTui } from "../mouse/scroll.ts";
7
7
  import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
8
8
 
9
9
  const TOOL_VIEWPORT_WIDTH_RATIO = 0.8;
10
+ /** 宽屏右侧留白上限:比例留白超过这么多列时改用固定留白。 */
11
+ const TOOL_VIEWPORT_MAX_GUTTER = 24;
10
12
 
11
13
  export function toolViewportWidth(width: number): number {
12
- return Math.max(1, Math.floor(width * TOOL_VIEWPORT_WIDTH_RATIO));
14
+ return Math.max(
15
+ 1,
16
+ Math.floor(width * TOOL_VIEWPORT_WIDTH_RATIO),
17
+ Math.floor(width) - TOOL_VIEWPORT_MAX_GUTTER,
18
+ );
13
19
  }
14
20
 
15
21
  /** 与默认工具结果相同的一级缩进包装。子组件只扣始终加上的 1 列;↳ 行多出的 2 列由 truncateToWidth 吃掉。 */
@@ -49,6 +49,8 @@ export const TOOL_MOUSE_TUI_SLOT = Symbol.for("pi.ccstyle.tool-mouse-tui");
49
49
  export const SCROLL_BUTTON_STATE_SLOT = Symbol.for("pi.ccstyle.scroll-button-state");
50
50
  export const OFFICIAL_SCROLL_TO_END_KEY = Symbol.for("pi.ccstyle.official-scroll-to-end");
51
51
  export const FLUSH_DOCKED_BASH_PATCH = Symbol.for("pi.ccstyle.flush-docked-bash-patch");
52
+ export const EARLY_STARTUP_HEADER_PATCH = Symbol.for("pi.ccstyle.early-startup-header-patch");
53
+ export const EARLY_STARTUP_HEADER_SWAP_KEY = Symbol.for("pi.ccstyle.early-startup-header-swap");
52
54
  export const TOOL_HOVER_STATE_KEY = Symbol.for("pi.ccstyle.tool-hover-state");
53
55
 
54
56
  // ── rich diff 组件标记 ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "license": "MIT",
5
5
  "description": "Claude Code-style TUI output with some personal touches and handy utilities.",
6
6
  "type": "module",
@@ -56,15 +56,18 @@
56
56
  },
57
57
  "bundledDependencies": [],
58
58
  "peerDependencies": {
59
- "@earendil-works/pi-ai": "^0.84.0",
60
- "@earendil-works/pi-coding-agent": "^0.84.0",
61
- "@earendil-works/pi-tui": "^0.84.0"
59
+ "@earendil-works/pi-ai": "*",
60
+ "@earendil-works/pi-coding-agent": "*",
61
+ "@earendil-works/pi-tui": "*"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=22.19.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@biomejs/biome": "2.5.5",
68
+ "@earendil-works/pi-ai": "0.99.1",
69
+ "@earendil-works/pi-coding-agent": "0.99.1",
70
+ "@earendil-works/pi-tui": "0.99.1",
68
71
  "@types/node": "^26.1.1",
69
72
  "typescript": "^5.9.3"
70
73
  }