@herbertgao/pi-extensions 2026.10.5 → 2026.10.6

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 (46) hide show
  1. package/README.md +2 -2
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +5 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +22 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +10 -6
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +15 -3
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +2 -23
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +105 -55
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +23 -471
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +6 -3
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +2 -2
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +53 -5
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +1 -1
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +1 -1
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +1 -1
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +1 -1
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +53 -34
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +41 -1
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -862
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
  38. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -1
  39. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
  40. package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
  41. package/node_modules/@herbertgao/sol-pi/README.md +3 -3
  42. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
  43. package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
  44. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  45. package/package.json +2 -2
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/show-more-hint.ts +0 -48
@@ -42,7 +42,7 @@ import { toolLoadingIcon } from "../utils/tool-loading-icon.ts";
42
42
  import { sanitizeToolResultText } from "../utils/tool-result-sanitize.ts";
43
43
  import { refreshTranscriptComponent } from "./transcript-refresh.ts";
44
44
  import { getMessageDisplayTheme } from "./tool/message-display.ts";
45
- import { showMoreHintText } from "./tool/show-more-hint.ts";
45
+ import { showMoreHintText } from "./tool/names.ts";
46
46
  import {
47
47
  countEditDiffStats,
48
48
  countWriteDiffStats,
@@ -53,13 +53,11 @@ import { displayConfigCacheKey } from "./tool/diff/diff-component.ts";
53
53
  import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
54
54
  import { isToolCallHovered } from "./mouse/hover.ts";
55
55
  import { getToolMouseTui } from "./mouse/scroll.ts";
56
- import {
57
- insetComponent,
58
- isLiveToolCall,
59
- renderExpandedToolResult,
60
- scheduleAnimation,
61
- } from "./tool/result.ts";
56
+ import { insetComponent } from "./tool/result.ts";
57
+ import { isLiveToolCall, scheduleAnimation } from "./tool/tool-state.ts";
58
+ import { renderExpandedToolResult } from "./tool/tool-io.ts";
62
59
  import { paddedBackgroundRow } from "./tool/grouping.ts";
60
+ import { styleChangeCounts } from "./tool/ccstyle-tool.ts";
63
61
  import { formatDisplayPath } from "./tool/names.ts";
64
62
  import {
65
63
  hasVisibleText,
@@ -594,6 +592,11 @@ export type CompactModeHooks = {
594
592
  assertOwnership(): void;
595
593
  /** 重新渲染包含指定 toolCallId 的 assistant 消息(思考收尾时刷新)。 */
596
594
  refreshToolCallMessage(toolCallId: string | undefined): void;
595
+ /**
596
+ * 收尾 resume 重放留下的活动回合。重放是同步的,回合之间靠下一条消息收口,
597
+ * 只有最后一条没有后续消息,会停在 Running。
598
+ */
599
+ settleReplayedRound(): void;
597
600
  shutdown(): void;
598
601
  };
599
602
 
@@ -620,17 +623,6 @@ const trackedAssistantComponents = new Set<any>();
620
623
  const trackedToolComponents = new Set<any>();
621
624
  let hoveredAssistantComponent: any;
622
625
 
623
- /**
624
- * 活动回合摘要 getter:常驻状态行(pi working row)镜像用。
625
- * installCompactMode 注册、卸载时清空,直接读 activeRound,不持有回合闭包。
626
- */
627
- let compactRunStatusGetter: (() => string | undefined) | undefined;
628
-
629
- /** 活动回合的摘要文本(`Running... · 9s, bash×1`);无活动回合返回 undefined。 */
630
- export function getCompactRunStatusText(): string | undefined {
631
- return compactRunStatusGetter?.();
632
- }
633
-
634
626
  /**
635
627
  * 回合尾行记录:摘要行(live 时连槽位卡)挂成 transcript 容器里回合末尾的
636
628
  * 兄弟组件,而不是 anchor 内部。anchor 内部的可变内容一旦被工具/diff 顶出视口
@@ -663,31 +655,37 @@ export function markCompactRoundToolExpanded(tool: any): void {
663
655
  roundToolExpansion?.markUserExpanded(tool);
664
656
  }
665
657
 
666
- /** 面板展开前的视口位置:收起时还原,避免面板撑高后收起导致视口错位。 */
667
- const panelViewportBeforeExpand = new WeakMap<
668
- object,
669
- { scrollTop: number; followingEnd: boolean }
670
- >();
658
+ /**
659
+ * 展开面板前的分隔空行与面板绑成一体:挂在同一尾行上,才不会被拆到
660
+ * anchor 正文之后(面板挂回合尾行,空行却留在消息里)。
661
+ * 带 childAtRow 返回 null:拆分判据认它为面板成员,行命中落空时归外层面板。
662
+ */
663
+ function panelBoundSpacer(): any {
664
+ return {
665
+ render: () => [""],
666
+ childAtRow: () => null,
667
+ invalidate() {},
668
+ };
669
+ }
670
+
671
+ /** 面板展开前的视口标记:收起时需要重钉视口的卡。 */
672
+ const panelViewportBeforeExpand = new WeakSet<object>();
671
673
 
672
674
  function capturePanelViewport(card: any): void {
673
- if (!card || panelViewportBeforeExpand.has(card)) return;
674
- const view = getToolMouseTui()?.getPrimaryScrollView?.();
675
- if (!view || typeof view.scrollTop !== "number" || typeof view.scrollTo !== "function") return;
676
- panelViewportBeforeExpand.set(card, {
677
- scrollTop: view.scrollTop,
678
- followingEnd: view.isFollowingEnd !== false,
679
- });
675
+ if (!card) return;
676
+ panelViewportBeforeExpand.add(card);
680
677
  }
681
678
 
682
679
  function restorePanelViewport(card: any): void {
683
- const snapshot = panelViewportBeforeExpand.get(card);
684
- if (!snapshot) return;
685
- panelViewportBeforeExpand.delete(card);
680
+ if (!panelViewportBeforeExpand.delete(card)) return;
686
681
  const view = getToolMouseTui()?.getPrimaryScrollView?.();
687
682
  if (!view) return;
688
- // 展开前就在底部:交回官方 follow;否则回到展开前的滚动偏移。
689
- if (snapshot.followingEnd && typeof view.scrollToEnd === "function") view.scrollToEnd();
690
- else if (typeof view.scrollTo === "function") view.scrollTo(snapshot.scrollTop);
683
+ // 不用展开前的快照:展开期间用户可能滚进面板里,回跳旧位置才是"跳"。
684
+ // 面板在摘要行之下,收起只删它以下的行,保持当前 scrollTop 摘要行原地不动;
685
+ // clamp 后落在新底时 scrollTo 会自动恢复 followingEnd(并清 followSuppressedAtEnd)。
686
+ if (typeof view.scrollTo === "function" && typeof view.scrollTop === "number") {
687
+ view.scrollTo(view.scrollTop);
688
+ }
691
689
  }
692
690
 
693
691
  function compactEditWriteLine(
@@ -724,8 +722,13 @@ function compactEditWriteLine(
724
722
  )
725
723
  : undefined;
726
724
  if (stats) {
727
- statsText = ` (+${stats.added} -${stats.removed})`;
728
- statsStyled = ` ${theme.fg("dim", "(")}${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)}${theme.fg("dim", ")")}`;
725
+ const counts = styleChangeCounts(
726
+ (color, text) => theme.fg(color, text),
727
+ stats.added,
728
+ stats.removed,
729
+ );
730
+ statsText = counts.text;
731
+ statsStyled = counts.styled;
729
732
  }
730
733
  }
731
734
  // 展开卡 Box(1,1) 已 pad;折叠行自己留 1 格前导空格
@@ -1187,6 +1190,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1187
1190
  liveSlotToolIds: Set<string>;
1188
1191
  /** 收尾后的静默期截止时间戳;期间仍按围观态渲染,到点才收拢。 */
1189
1192
  settleUntil?: number;
1193
+ /** 消息已有 toolCall、工具组件还没到:等它一 updateDisplay 就重画槽位。 */
1194
+ pendingLiveTool?: boolean;
1190
1195
  foldTimer?: ReturnType<typeof setTimeout>;
1191
1196
  /** 回合挂钟起点,保证 Running 时长连续递增。 */
1192
1197
  startedAt: number;
@@ -1348,12 +1353,6 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1348
1353
  ...round.detachedMessages,
1349
1354
  ];
1350
1355
 
1351
- /** 镜像给常驻状态行的文案:只认当前活动回合(摘要行被顶出视口时由 working-message 取用)。 */
1352
- compactRunStatusGetter = () => {
1353
- const round = activeRound;
1354
- return round?.active ? summarize(roundMessages(round), true, round) || undefined : undefined;
1355
- };
1356
-
1357
1356
  const roundToolCallIds = (round: CompactRound): Set<string> => {
1358
1357
  const ids = new Set<string>();
1359
1358
  for (const message of roundMessages(round)) {
@@ -1364,6 +1363,26 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1364
1363
  return ids;
1365
1364
  };
1366
1365
 
1366
+ /**
1367
+ * 摘要行有东西可汇总才挂:普通工具(edit/write/ask 之外)或非空思考。
1368
+ * 只剩外层卡的回合不挂——那些卡各自能展开,空摘要点开只会跳位。
1369
+ */
1370
+ const roundHasSummaryContent = (round: CompactRound): boolean => {
1371
+ for (const message of roundMessages(round)) {
1372
+ for (const item of Array.isArray(message?.content) ? message.content : []) {
1373
+ if (item?.type === "thinking" && String(item.thinking ?? "").trim()) return true;
1374
+ if (
1375
+ item?.type === "toolCall" &&
1376
+ typeof item.name === "string" &&
1377
+ !OUTER_TOOLS.has(item.name)
1378
+ ) {
1379
+ return true;
1380
+ }
1381
+ }
1382
+ }
1383
+ return false;
1384
+ };
1385
+
1367
1386
  /**
1368
1387
  * anchor 所在的 transcript 容器(pi chatContainer)。
1369
1388
  * 优先走分组补丁在 addChild 时挂的父指针;没有则沿 tui 组件树找直属父容器。
@@ -1572,10 +1591,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1572
1591
  const syncTailForTool = (toolCallId: string): void => {
1573
1592
  if (!toolCallId) return;
1574
1593
  for (const round of [activeRound, ...pendingFoldRounds]) {
1575
- if (round?.tail && roundToolCallIds(round).has(toolCallId)) {
1576
- syncTailPosition(round);
1577
- return;
1578
- }
1594
+ if (!round || !roundToolCallIds(round).has(toolCallId)) continue;
1595
+ // 槽位是在消息更新时画的,当时工具组件还不存在:组件到了就补上。
1596
+ // 参数流式更新也走这里,槽位卡跟着换内容。
1597
+ if (round.pendingLiveTool || roundLive(round)) renderRound(round);
1598
+ else if (round.tail) syncTailPosition(round);
1599
+ return;
1579
1600
  }
1580
1601
  };
1581
1602
 
@@ -1719,6 +1740,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1719
1740
  }
1720
1741
  let runningTool: any;
1721
1742
  let lastTool: any;
1743
+ let missingTool = false;
1722
1744
  for (const [, message] of round.messages) {
1723
1745
  for (const item of Array.isArray(message?.content) ? message.content : []) {
1724
1746
  if (item?.type !== "toolCall" || typeof item.id !== "string") continue;
@@ -1727,11 +1749,17 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1727
1749
  if (OUTER_TOOLS.has(String(tool?.toolName ?? item.name ?? ""))) continue;
1728
1750
  round.liveSlotToolIds.add(item.id);
1729
1751
  liveSlotToolIds.add(item.id);
1730
- if (!tool) continue;
1752
+ // pi 的 message_update 先 updateContent 再建工具组件:这一帧计数已经算上,
1753
+ // 组件要等 updateDisplay 才能进槽位。记下来,组件一到就补画。
1754
+ if (!tool) {
1755
+ missingTool = true;
1756
+ continue;
1757
+ }
1731
1758
  lastTool = tool;
1732
1759
  if (isRunningTool(tool)) runningTool = tool;
1733
1760
  }
1734
1761
  }
1762
+ round.pendingLiveTool = missingTool;
1735
1763
  // 槽位单块:活动思考最新;否则本回合最近的工具(运行中原位增长,完成也留着)。
1736
1764
  const slotTool = thinkingKids.length > 0 ? undefined : (runningTool ?? lastTool);
1737
1765
  const cardItems: Array<{ child?: any; tool?: any }> = slotTool
@@ -1786,6 +1814,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1786
1814
  }
1787
1815
  for (const id of round.liveSlotToolIds) liveSlotToolIds.delete(id);
1788
1816
  round.liveSlotToolIds.clear();
1817
+ // pendingLiveTool 留给 renderRoundLive 重算:这里清掉的话,组件刚到的那次
1818
+ // 补画会把自己的触发条件抹掉,槽位仍是空的。
1789
1819
 
1790
1820
  for (const [component, message] of round.messages) {
1791
1821
  component.lastMessage = message;
@@ -1807,6 +1837,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1807
1837
  };
1808
1838
  }
1809
1839
 
1840
+ if (round.anchor.expanded === true) {
1841
+ // 没有可汇总内容的回合不进展开态:摘要行不存在,展开只会把正文搬回回合开头。
1842
+ if (!roundHasSummaryContent(round)) {
1843
+ for (const member of round.messages.keys()) member.expanded = false;
1844
+ round.anchor.expanded = false;
1845
+ }
1846
+ }
1810
1847
  if (round.anchor.expanded === true) {
1811
1848
  // 展开内容留在摘要行那个位置:回合最后一个成员(含 write/edit)之后。
1812
1849
  // 放进 anchor 内部会跳到回合起点,盖到中间的 write/edit 上面。
@@ -1828,7 +1865,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1828
1865
  } else {
1829
1866
  // 面板上沿留 1 行卡外空行:面板底色与 userMessage 同色,
1830
1867
  // 贴在一起会看成一个块;卡内顶部再加 1 行 padding 保持上下对称。
1831
- anchorChildren.push(new Spacer(1));
1868
+ // 空行必须与面板同进退(childAtRow 参与面板拆分),用 panelBoundSpacer。
1869
+ anchorChildren.push(panelBoundSpacer());
1832
1870
  anchorChildren.push(
1833
1871
  compactRoundCard(
1834
1872
  cardItems,
@@ -1949,8 +1987,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1949
1987
  for (const [component, message] of round.messages) {
1950
1988
  if (component === round.anchor) {
1951
1989
  renderAssistantWithoutThinking(component, message);
1952
- // 空摘要不挂行(getter 在 Running 启动瞬间也可能短暂为空)
1953
- if (summary || round.active) {
1990
+ // 空摘要不挂行(getter 在 Running 启动瞬间也可能短暂为空)。
1991
+ // 只剩外层卡的回合同样不挂:摘要行没有可汇总的内容。
1992
+ if ((summary || round.active) && (round.active || roundHasSummaryContent(round))) {
1954
1993
  mountRoundTail(round, (identity) => [
1955
1994
  compactAssistantLineComponent(identity, getSummary, deps.query),
1956
1995
  ]);
@@ -2142,8 +2181,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
2142
2181
 
2143
2182
  patch.toolInstalledUpdateDisplay = function (this: any) {
2144
2183
  const id = String(this.toolCallId ?? "");
2145
- if (patch.active && config.mode === "compact" && expandedRoundToolIds.has(id)) {
2146
- // 用户点开的保留展开;其余(含全局展开被收回的)继续强制折叠。
2184
+ if (
2185
+ patch.active &&
2186
+ config.mode === "compact" &&
2187
+ (expandedRoundToolIds.has(id) || liveSlotToolIds.has(id))
2188
+ ) {
2189
+ // 用户点开的保留展开;其余(含全局展开、live 槽位接管的)继续强制折叠。
2190
+ // 槽位卡在消息更新时就接管了 id,组件后到时 setExpanded 会把它翻开,外层多出一张卡。
2147
2191
  if (this.expanded !== true) explicitRoundToolIds.delete(id);
2148
2192
  else if (!explicitRoundToolIds.has(id)) this.expanded = false;
2149
2193
  }
@@ -2192,7 +2236,6 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
2192
2236
  trackedToolComponents.clear();
2193
2237
  hoveredAssistantComponent = undefined;
2194
2238
  resetRounds();
2195
- compactRunStatusGetter = undefined;
2196
2239
  };
2197
2240
 
2198
2241
  assistantPrototype.updateContent = patch.assistantInstalled;
@@ -2249,6 +2292,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
2249
2292
  }
2250
2293
  }
2251
2294
  },
2295
+ settleReplayedRound() {
2296
+ if (!patch.active || config.mode !== "compact") return;
2297
+ const round = activeRound;
2298
+ if (!round) return;
2299
+ finalizeRound(round); // clears activeRound — capture it first
2300
+ renderRound(round);
2301
+ },
2252
2302
  shutdown() {
2253
2303
  patch.dispose();
2254
2304
  },