@herbertgao/pi-extensions 2026.9.19 → 2026.10.0

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 (444) hide show
  1. package/README.md +14 -16
  2. package/THIRD_PARTY_NOTICES.md +29 -3
  3. package/node_modules/@herbertgao/pi-bark/README.md +39 -3
  4. package/node_modules/@herbertgao/pi-bark/package.json +1 -1
  5. package/node_modules/@herbertgao/pi-bark/src/index.ts +275 -15
  6. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +85 -68
  7. package/node_modules/@herbertgao/pi-cc-extensions/README.md +79 -62
  8. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +116 -34
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +370 -118
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +31 -33
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +521 -0
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/mcp-chip.ts +136 -0
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +58 -38
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1127 -231
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +267 -262
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +38 -48
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +287 -114
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +303 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +151 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +4 -4
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +103 -48
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +8 -7
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +2 -1
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +171 -196
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +27 -93
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +429 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +259 -164
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +77 -0
  61. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  62. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  63. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  64. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  65. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  66. package/node_modules/@herbertgao/pi-cc-extensions/package.json +10 -8
  67. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  68. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  69. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +3 -2
  70. package/node_modules/@herbertgao/sol-pi/CONTRIBUTING.md +22 -0
  71. package/node_modules/@herbertgao/sol-pi/README.md +36 -17
  72. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +10 -6
  73. package/node_modules/@herbertgao/sol-pi/agents-install.md +15 -12
  74. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +24 -8
  75. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +2 -2
  76. package/node_modules/@herbertgao/sol-pi/docs/star-history.md +18 -0
  77. package/node_modules/@herbertgao/sol-pi/package.json +4 -2
  78. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +1 -1
  79. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +1 -1
  80. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +23 -4
  81. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +32 -1
  82. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +3 -2
  83. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +1 -1
  84. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +27 -7
  85. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +137 -41
  86. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +1 -0
  87. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +2 -1
  88. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/projection.ts +58 -0
  89. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +28 -6
  90. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +2 -1
  91. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +15 -2
  92. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  93. package/node_modules/@pi-plugins/fast-mode/README.md +7 -1
  94. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  95. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +7 -1
  96. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  97. package/node_modules/@pi-plugins/fast-mode/package.json +5 -5
  98. package/node_modules/pi-antigravity/README.md +3 -0
  99. package/node_modules/pi-antigravity/package.json +8 -8
  100. package/node_modules/pi-antigravity/src/auth/oauth.ts +6 -3
  101. package/node_modules/pi-antigravity/src/index.ts +114 -110
  102. package/node_modules/pi-antigravity/src/stream/stream.ts +1 -1
  103. package/node_modules/pi-antigravity/src/utils/util.ts +18 -1
  104. package/node_modules/pi-multi-account/CHANGELOG.md +20 -0
  105. package/node_modules/pi-multi-account/README.md +46 -8
  106. package/node_modules/pi-multi-account/config-file-transaction.ts +42 -0
  107. package/node_modules/pi-multi-account/index.ts +255 -269
  108. package/node_modules/pi-multi-account/model-pins.ts +36 -0
  109. package/node_modules/pi-multi-account/package.json +9 -7
  110. package/node_modules/pi-multi-account/slot-proxy-auth.ts +12 -2
  111. package/node_modules/pi-multi-account/slot-proxy.ts +27 -5
  112. package/node_modules/pi-web-access/CHANGELOG.md +17 -0
  113. package/node_modules/pi-web-access/README.md +6 -3
  114. package/node_modules/pi-web-access/brave-rate-limit.ts +170 -0
  115. package/node_modules/pi-web-access/brave.ts +28 -11
  116. package/node_modules/pi-web-access/dist/index.js +202 -33
  117. package/node_modules/pi-web-access/gemini-search.ts +2 -5
  118. package/node_modules/pi-web-access/index.ts +13 -7
  119. package/node_modules/pi-web-access/openai-search.ts +24 -10
  120. package/node_modules/pi-web-access/package.json +1 -1
  121. package/node_modules/pi-web-access/summary-review.ts +8 -2
  122. package/node_modules/{@herbertgao/resume-from → resume-from}/README.md +16 -12
  123. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/adapter.d.ts +0 -5
  124. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/adapter.js +2 -3
  125. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/contract.d.ts +2 -2
  126. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/reader.d.ts +3 -0
  127. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/reader.js +27 -15
  128. package/node_modules/{@herbertgao/resume-from/dist/adapters/codex → resume-from/dist/adapters/claude-code}/redaction.js +16 -3
  129. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/writer.d.ts +2 -4
  130. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/writer.js +1 -1
  131. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/contract.d.ts +2 -4
  132. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/index.js +1 -2
  133. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/read.d.ts +16 -0
  134. package/node_modules/resume-from/dist/adapters/codex/read.js +723 -0
  135. package/node_modules/{@herbertgao/resume-from/dist/adapters/pi → resume-from/dist/adapters/codex}/redaction.js +16 -3
  136. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/rollout.d.ts +44 -4
  137. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/rollout.js +88 -4
  138. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/write.d.ts +2 -2
  139. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/write.js +2 -2
  140. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/contract.d.ts +5 -1
  141. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/adapter.js +10 -6
  142. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/contract.d.ts +2 -2
  143. package/node_modules/{@herbertgao/resume-from/dist/adapters/claude-code → resume-from/dist/adapters/pi}/redaction.js +16 -3
  144. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/serialize.d.ts +2 -4
  145. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/serialize.js +2 -2
  146. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/render.d.ts +1 -1
  147. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/render.js +4 -4
  148. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/runner.js +2 -2
  149. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/contract.d.ts +2 -2
  150. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/command.js +31 -8
  151. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/wiring.d.ts +1 -1
  152. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/wiring.js +1 -2
  153. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/confirmation.d.ts +4 -1
  154. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/confirmation.js +2 -2
  155. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/contract.d.ts +8 -6
  156. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/contract.d.ts +10 -5
  157. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/finder.d.ts +2 -1
  158. package/node_modules/resume-from/dist/import/discovery/finder.js +269 -0
  159. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/contract.d.ts +3 -3
  160. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/lander.js +3 -3
  161. package/node_modules/resume-from/dist/import/pipeline.d.ts +28 -0
  162. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/pipeline.js +31 -11
  163. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/builder.d.ts +2 -2
  164. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/builder.js +9 -5
  165. package/node_modules/resume-from/dist/import/preview/warnings.d.ts +14 -0
  166. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/warnings.js +26 -20
  167. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/wiring.js +12 -2
  168. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/contract.d.ts +9 -3
  169. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/git.d.ts +4 -4
  170. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/git.js +7 -4
  171. package/node_modules/resume-from/dist/platform/repo/reader.d.ts +3 -0
  172. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/reader.js +60 -20
  173. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/session/contract.d.ts +8 -1
  174. package/node_modules/{@herbertgao/resume-from → resume-from}/package.json +19 -32
  175. package/node_modules/{@herbertgao/resume-from → resume-from}/shims/pi/extensions/resume-from.js +2 -2
  176. package/package.json +18 -22
  177. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  178. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  179. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  180. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  181. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  182. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  183. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/read.js +0 -270
  184. package/node_modules/@herbertgao/resume-from/dist/import/discovery/finder.js +0 -129
  185. package/node_modules/@herbertgao/resume-from/dist/import/pipeline.d.ts +0 -17
  186. package/node_modules/@herbertgao/resume-from/dist/import/preview/warnings.d.ts +0 -11
  187. package/node_modules/@herbertgao/resume-from/dist/platform/repo/reader.d.ts +0 -8
  188. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  189. package/node_modules/pi-footer/LICENSE +0 -21
  190. package/node_modules/pi-footer/README.md +0 -354
  191. package/node_modules/pi-footer/package.json +0 -68
  192. package/node_modules/pi-footer/src/cache.ts +0 -106
  193. package/node_modules/pi-footer/src/colors.ts +0 -237
  194. package/node_modules/pi-footer/src/config.ts +0 -202
  195. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  196. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  197. package/node_modules/pi-footer/src/git.ts +0 -135
  198. package/node_modules/pi-footer/src/index.ts +0 -268
  199. package/node_modules/pi-footer/src/metrics.ts +0 -141
  200. package/node_modules/pi-footer/src/presets.ts +0 -383
  201. package/node_modules/pi-footer/src/render.ts +0 -114
  202. package/node_modules/pi-footer/src/separators.ts +0 -52
  203. package/node_modules/pi-footer/src/types.ts +0 -122
  204. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  205. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  206. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  207. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  208. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  209. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  210. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  211. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  212. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  213. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  214. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  215. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  216. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  217. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  218. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  219. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  220. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  221. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  222. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  223. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  224. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  225. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  226. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  227. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  228. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  229. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  230. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  231. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  232. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  233. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  234. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  235. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  236. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  237. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  238. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  239. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  240. package/node_modules/pi-footer/src/ui.ts +0 -66
  241. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  242. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  243. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  244. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  245. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  246. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  247. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  248. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  249. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  250. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  251. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  252. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  253. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  254. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  255. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  256. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  257. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  258. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  259. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  260. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  261. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  262. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  263. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  264. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  265. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  266. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  267. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  268. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  269. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  270. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  271. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  272. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  273. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  274. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  275. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  276. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  277. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  278. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  279. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  280. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  281. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  282. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  283. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  284. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  285. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  286. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  287. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  288. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  289. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  290. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  291. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  292. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  293. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  294. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  295. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  296. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  297. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  298. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  299. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  300. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  301. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  302. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  303. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  304. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  305. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  306. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
  307. /package/node_modules/{@herbertgao/resume-from → resume-from}/LICENSE +0 -0
  308. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/contract.js +0 -0
  309. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/entries.d.ts +0 -0
  310. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/entries.js +0 -0
  311. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/index.d.ts +0 -0
  312. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/index.js +0 -0
  313. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/layout.d.ts +0 -0
  314. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/layout.js +0 -0
  315. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/readback.d.ts +0 -0
  316. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/readback.js +0 -0
  317. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/redaction.d.ts +0 -0
  318. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/validation.d.ts +0 -0
  319. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/validation.js +0 -0
  320. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/contract.js +0 -0
  321. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/index.d.ts +0 -0
  322. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/readback.d.ts +0 -0
  323. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/readback.js +0 -0
  324. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/redaction.d.ts +0 -0
  325. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/validation.d.ts +0 -0
  326. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/validation.js +0 -0
  327. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/contract.js +0 -0
  328. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/adapter.d.ts +0 -0
  329. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/contract.js +0 -0
  330. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/format.d.ts +0 -0
  331. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/format.js +0 -0
  332. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/index.d.ts +0 -0
  333. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/index.js +0 -0
  334. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/parse.d.ts +0 -0
  335. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/parse.js +0 -0
  336. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/redaction.d.ts +0 -0
  337. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/validate.d.ts +0 -0
  338. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/validate.js +0 -0
  339. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/bin.d.ts +0 -0
  340. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/bin.js +0 -0
  341. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/contract.d.ts +0 -0
  342. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/contract.js +0 -0
  343. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/agents.d.ts +0 -0
  344. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/agents.js +0 -0
  345. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/args.d.ts +0 -0
  346. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/args.js +0 -0
  347. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/contract.d.ts +0 -0
  348. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/contract.js +0 -0
  349. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/index.d.ts +0 -0
  350. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/index.js +0 -0
  351. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/presentation.d.ts +0 -0
  352. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/presentation.js +0 -0
  353. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/runner.d.ts +0 -0
  354. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/contract.js +0 -0
  355. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/entry.d.ts +0 -0
  356. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/entry.js +0 -0
  357. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/index.d.ts +0 -0
  358. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/index.js +0 -0
  359. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/command.d.ts +0 -0
  360. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/contract.d.ts +0 -0
  361. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/contract.js +0 -0
  362. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/index.d.ts +0 -0
  363. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/index.js +0 -0
  364. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/picker.d.ts +0 -0
  365. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/picker.js +0 -0
  366. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/presentation.d.ts +0 -0
  367. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/presentation.js +0 -0
  368. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/register.d.ts +0 -0
  369. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/register.js +0 -0
  370. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/ui.d.ts +0 -0
  371. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/ui.js +0 -0
  372. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/profile.d.ts +0 -0
  373. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/profile.js +0 -0
  374. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/registry.d.ts +0 -0
  375. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/registry.js +0 -0
  376. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/contract.js +0 -0
  377. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/contract.js +0 -0
  378. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/errors.d.ts +0 -0
  379. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/errors.js +0 -0
  380. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/homes.d.ts +0 -0
  381. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/homes.js +0 -0
  382. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/index.d.ts +0 -0
  383. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/index.js +0 -0
  384. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/ordering.d.ts +0 -0
  385. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/ordering.js +0 -0
  386. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/errors.d.ts +0 -0
  387. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/errors.js +0 -0
  388. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/index.d.ts +0 -0
  389. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/index.js +0 -0
  390. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/contract.js +0 -0
  391. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/errors.d.ts +0 -0
  392. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/errors.js +0 -0
  393. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/handover.d.ts +0 -0
  394. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/handover.js +0 -0
  395. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/index.d.ts +0 -0
  396. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/index.js +0 -0
  397. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/lander.d.ts +0 -0
  398. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/marker.d.ts +0 -0
  399. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/marker.js +0 -0
  400. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/contract.d.ts +0 -0
  401. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/contract.js +0 -0
  402. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/format.d.ts +0 -0
  403. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/format.js +0 -0
  404. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/index.d.ts +0 -0
  405. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/index.js +0 -0
  406. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/contract.d.ts +0 -0
  407. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/contract.js +0 -0
  408. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/index.d.ts +0 -0
  409. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/index.js +0 -0
  410. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/rules.d.ts +0 -0
  411. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/rules.js +0 -0
  412. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/wiring.d.ts +0 -0
  413. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/index.d.ts +0 -0
  414. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/index.js +0 -0
  415. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/contract.d.ts +0 -0
  416. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/contract.js +0 -0
  417. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/defaults.d.ts +0 -0
  418. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/defaults.js +0 -0
  419. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/errors.d.ts +0 -0
  420. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/errors.js +0 -0
  421. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/index.d.ts +0 -0
  422. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/index.js +0 -0
  423. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/loader.d.ts +0 -0
  424. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/loader.js +0 -0
  425. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/paths.d.ts +0 -0
  426. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/paths.js +0 -0
  427. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/validate.d.ts +0 -0
  428. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/validate.js +0 -0
  429. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/contract.js +0 -0
  430. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/index.d.ts +0 -0
  431. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/index.js +0 -0
  432. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/contract.d.ts +0 -0
  433. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/contract.js +0 -0
  434. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/file-committer.d.ts +0 -0
  435. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/file-committer.js +0 -0
  436. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/index.d.ts +0 -0
  437. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/index.js +0 -0
  438. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/contract.d.ts +0 -0
  439. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/contract.js +0 -0
  440. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/estimator.d.ts +0 -0
  441. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/estimator.js +0 -0
  442. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/index.d.ts +0 -0
  443. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/index.js +0 -0
  444. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/session/contract.js +0 -0
@@ -1,11 +1,21 @@
1
1
  /**
2
2
  * Compact mode:每条含 toolCall 的 assistant message 折叠为一条逐步累加的摘要行
3
- * (`Ran for 8s, bash×2, read×2`),edit/write 独立单行(`✓ write <path> (+25 -0)`),
4
- * 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen 点击)在单个工具卡中恢复
5
- * compact-thinking/Pi 原生或专用 renderer。edit/write 折叠时显示统计,展开时显示 rich diff。
3
+ * (`Ran for 8s, bash×2, read×2`),edit/write 独立标题行(`✓ write <path> (+25 -0)`),
4
+ * 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen 点击)时助手文本按原生渲染,
5
+ * thinking 与工具卡装进 userMessageBg 面板(卡内恢复 compact-thinking/Pi 原生或专用
6
+ * renderer)。edit/write 复用 mode=on 的 rich diff
7
+ * 与 `editDiffCollapsedLines` / `writeDiffCollapsedLines` / `expandedPreviewMaxLines`。
6
8
  * Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
7
9
  * 底部 Agents/Tasks 面板由 pi-subagents/pi-tasks 独立 widget 负责,不经 tool 卡外置。
8
10
  *
11
+ * live 围观态(回合进行中 / 收尾静默期):正文跟折叠态同形;摘要行与单一槽位卡
12
+ * (`↳` + 最新的一个块:仍在增长的思考预览,或本回合最近的工具,运行中优先)挂在
13
+ * transcript 容器的回合末尾(尾行组件,anchor 与工具卡之后、下一消息之前)。
14
+ * 思考原地增长不轮换,只有新的思考块或工具调用进来才换槽位内容。
15
+ * 摘要行挂末尾是刻意的:运行中它恒在可写视口底缘,收尾时原地从 Running... 翻成
16
+ * Ran for 真实落进 scrollback;放在 anchor 内部则会被工具/diff 顶出视口后定格。
17
+ * (anchor 未挂载进容器的独立渲染场景退回组件内旧行为。)
18
+ *
9
19
  * 工具计数:read 按非空路径去重、其余按调用计数(首次出现顺序);edit/write 不进摘要。
10
20
  * 时长 = 回合流逝挂钟;进行中 Running...,结束 Ran for。
11
21
  * abort/error/length 状态行挂在摘要外层,避免被折叠吞掉。
@@ -19,13 +29,13 @@ import {
19
29
  ToolExecutionComponent,
20
30
  type Theme,
21
31
  } from "@earendil-works/pi-coding-agent";
22
- import { truncateToWidth, visibleWidth, Box } from "@earendil-works/pi-tui";
32
+ import { truncateToWidth, visibleWidth, Box, Spacer } from "@earendil-works/pi-tui";
23
33
  import { config, getToolDisplayConfig } from "../config/config.ts";
24
34
  import { toolLoadingIcon } from "../utils/tool-loading-icon.ts";
25
35
  import { sanitizeToolResultText } from "../utils/tool-result-sanitize.ts";
26
36
  import { refreshTranscriptComponent } from "./transcript-refresh.ts";
27
- import { getMessageDisplayTheme } from "./message-display.ts";
28
- import { showMoreHintText } from "./show-more-hint.ts";
37
+ import { getMessageDisplayTheme } from "./tool/message-display.ts";
38
+ import { showMoreHintText } from "./tool/show-more-hint.ts";
29
39
  import {
30
40
  countEditDiffStats,
31
41
  countWriteDiffStats,
@@ -33,13 +43,33 @@ import {
33
43
  } from "./tool/diff/diff-renderer.ts";
34
44
  import { renderRichToolResult } from "./tool/diff/index.ts";
35
45
  import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
46
+ import { isToolCallHovered } from "./mouse/hover.ts";
47
+ import { getToolMouseTui } from "./mouse/scroll.ts";
48
+ import { insetComponent, renderExpandedToolResult, scheduleAnimation } from "./tool/result.ts";
49
+ import { paddedBackgroundRow } from "./tool/grouping.ts";
50
+ import { formatDisplayPath } from "./tool/names.ts";
51
+ import {
52
+ hasVisibleText,
53
+ removeStyledRange,
54
+ stripBackgroundAnsi,
55
+ stripTerminalSequencesPreservingLayout,
56
+ } from "../utils/ansi-text.ts";
57
+ import { walkComponentTree } from "../utils/component-tree.ts";
36
58
  import {
37
- formatDisplayPath,
38
- insetComponent,
39
- renderExpandedToolResult,
40
- scheduleAnimation,
41
- } from "./tool/result.ts";
42
- import { paddedBackgroundRow, stripBackgroundAnsi } from "./tool/grouping.ts";
59
+ ASSISTANT_REENTRY_KEY,
60
+ ASSISTANT_SET_EXPANDED_KEY,
61
+ ASSISTANT_TOGGLE_ROUND_KEY,
62
+ COMPACT_MODE_PATCH_KEY,
63
+ COMPACT_THINKING_PATCH_KEY,
64
+ patchRegistry,
65
+ PROTOTYPE_ORIGINAL_KEY,
66
+ TOOL_GROUPING_PARENT_KEY,
67
+ } from "../utils/patch-keys.ts";
68
+
69
+ /** Pi 的 native 组件与扩展可能各持一份 pi-tui,Spacer 不能只靠 instanceof 判。 */
70
+ function isSpacerComponent(value: any): boolean {
71
+ return value instanceof Spacer || value?.constructor?.name === "Spacer";
72
+ }
43
73
 
44
74
  /** compact 渲染层对 compact-thinking 的只读查询面(不建第二套计时器)。 */
45
75
  export type CompactThinkingQuery = {
@@ -49,19 +79,14 @@ export type CompactThinkingQuery = {
49
79
  setCompactSummaryActive?(active: boolean): void;
50
80
  };
51
81
 
52
- const COMPACT_MODE_PATCH_KEY = Symbol.for("pi.ccstyle.compact-mode-patch");
53
- const COMPACT_THINKING_OWNER_KEY = Symbol.for("pi.ccstyle.compact-thinking-owner");
54
- const COMPACT_THINKING_PATCH_KEY = Symbol.for("pi.ccstyle.compact-thinking-update");
55
- const PROTOTYPE_ORIGINAL_KEY = Symbol.for("pi.ccstyle.prototype-original");
56
- const ASSISTANT_SET_EXPANDED_DESCRIPTION = "pi.ccstyle.compact-assistant-set-expanded";
57
- const ASSISTANT_TOGGLE_ROUND_DESCRIPTION = "pi.ccstyle.compact-assistant-toggle-round";
58
- const ASSISTANT_REENTRY_DESCRIPTION = "pi.ccstyle.compact-assistant-reentry";
59
- const ASSISTANT_SET_EXPANDED_KEY = Symbol.for(ASSISTANT_SET_EXPANDED_DESCRIPTION);
60
- const ASSISTANT_TOGGLE_ROUND_KEY = Symbol.for(ASSISTANT_TOGGLE_ROUND_DESCRIPTION);
61
- const ASSISTANT_REENTRY_KEY = Symbol.for(ASSISTANT_REENTRY_DESCRIPTION);
62
-
63
82
  const EDIT_WRITE_TOOLS = new Set(["edit", "write"]);
64
83
 
84
+ /**
85
+ * 回合收尾后延迟收拢的静默期(ms)。正文 token 往往先于 toolCall 到达,
86
+ * 立刻收拢会得到「先收再开」两次转场;静默期内被新回合接替则一并收拢。
87
+ */
88
+ const FOLD_SETTLE_MS = 250;
89
+
65
90
  type CompactThinkingTheme = Pick<Theme, "fg" | "italic" | "bold">;
66
91
 
67
92
  /** 与 compact-thinking 主渲染器共用的静态文字样式。 */
@@ -290,16 +315,6 @@ function darkenBgAnsi(theme: any, slot: string): string {
290
315
  return `\x1b[48;2;${r};${g};${b}m`;
291
316
  }
292
317
 
293
- /** 去掉行内所有 CSI/OSC 序列后是否有可见文本(判断工具卡首尾内容行)。 */
294
- function hasVisibleText(line: string): boolean {
295
- return (
296
- line
297
- .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
298
- .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
299
- .trim().length > 0
300
- );
301
- }
302
-
303
318
  /** 工具卡深色行:左右内缩(左 2 右 3 格,含工具卡自身 padding),背景只到内容区,
304
319
  * 修复行尾 reset 截断。 */
305
320
  function toolCardBgRow(
@@ -320,47 +335,80 @@ function toolCardBgRow(
320
335
  const body = ` ${clipped}${" ".repeat(pad)} `;
321
336
  const stable = body.replace(/\x1b\[(?:0)?m/g, (reset) => reset + bgAnsi);
322
337
  const outerBg = typeof theme?.getBgAnsi === "function" ? String(theme.getBgAnsi(slot)) : "";
323
- const inset = (n) => (outerBg ? `${outerBg}${" ".repeat(n)}\x1b[49m` : " ".repeat(n));
338
+ const inset = (n: number) => (outerBg ? `${outerBg}${" ".repeat(n)}\x1b[49m` : " ".repeat(n));
324
339
  return `${inset(leftInset)}${bgAnsi}${stable}\x1b[49m${inset(rightInset)}`;
325
340
  }
326
- /** edit/write 展开卡:保持原样式(userMessageBg Box),不应用工具卡深色/间距改动。 */
341
+ /** edit/write 展开卡:保持原样式(展开卡背景 Box),不应用工具卡深色/间距改动。 */
327
342
  function editWriteExpandedCard(theme: any): any {
328
343
  return new Box(
329
344
  1,
330
345
  1,
331
- typeof theme.bg === "function" ? (text: string) => theme.bg("userMessageBg", text) : undefined,
346
+ typeof theme.bg === "function"
347
+ ? (text: string) => theme.bg(config.expandedCardBackground, text)
348
+ : undefined,
332
349
  );
333
350
  }
334
351
 
335
- /** compact 展开卡:保留本地嵌套工具背景,并记录 thinking 的点击行区间。 */
352
+ /** compact 展开面板:外卡片保持展开卡背景;展开的 thinking 另包一层更深的
353
+ * 内卡(自身 Box 背景与外卡同色,不区分会糊在一起),工具卡直接铺在外卡背景上。
354
+ * 每个子卡前面补 1 行分隔(上一条已经是空行时不再补),子卡自带的首尾空行会被裁掉,
355
+ * 避免与外卡/内卡 padding 叠出双空行。
356
+ * hits:子卡的行区间与命中目标(thinking 命中自身,工具命中 tool 组件),
357
+ * 供展开后点击 hint。未开启 toolHits 时不映射工具,点击仍归外层面板。
358
+ * live=true:live 槽位用,不铺底色也不留上下空行,首行紧跟 `↳`、其余行缩进对齐
359
+ * (Claude Code 的 ⎿ 口径)。行的产出仍是一行对一行,hits 行号无需平移。
360
+ */
336
361
  function layoutExpandedToolCard(
337
362
  theme: any,
338
363
  children: any[],
339
364
  width: number,
365
+ paints?: string[][],
366
+ toolHits = false,
367
+ live = false,
340
368
  ): { lines: string[]; hits: Array<{ child: any; start: number; end: number }> } {
341
- const slot = "userMessageBg";
369
+ const slot = config.expandedCardBackground;
342
370
  const toolBgAnsi = darkenBgAnsi(theme, slot);
343
- const innerWidth = Math.max(0, width - 2);
371
+ /** live 每行前面的标记宽度(首行 `↳ `,其余对齐空格)。 */
372
+ const markerWidth = live ? 2 : 0;
373
+ const innerWidth = Math.max(0, width - 2 - markerWidth);
344
374
  const lines: string[] = [];
345
375
  const hits: Array<{ child: any; start: number; end: number }> = [];
346
- lines.push(paddedBackgroundRow(theme, slot, "", width));
347
- let skipLeadingBlank = true;
348
- for (const child of children) {
349
- const childLines = child.render(innerWidth);
350
- if (!child.__ccToolCard) {
351
- let start = 0;
352
- if (skipLeadingBlank) {
353
- while (start < childLines.length && !hasVisibleText(childLines[start])) start++;
354
- skipLeadingBlank = false;
355
- }
356
- const rangeStart = lines.length;
357
- for (let i = start; i < childLines.length; i++) {
358
- lines.push(paddedBackgroundRow(theme, slot, childLines[i], width));
359
- }
360
- if (lines.length > rangeStart) hits.push({ child, start: rangeStart, end: lines.length });
361
- continue;
376
+ const isThinkingPreview = (child: any) => typeof child?.setHintHovered === "function";
377
+ let lastBlank = false;
378
+ /** live 首行被吃掉的前导缩进:续行同步左移同样格数,保持块内相对对齐。 */
379
+ let liveLead = 0;
380
+ const pushRow = (raw: string) => {
381
+ if (!live) {
382
+ lines.push(paddedBackgroundRow(theme, slot, raw, width));
383
+ return;
362
384
  }
363
- skipLeadingBlank = false;
385
+ // 槽位里不重复展开入口(见 SLOT_HINT_PATTERNS)。
386
+ const line = stripSlotHint(raw);
387
+ // 工具卡行首带 SGR:ANSI 之后才是缩进空格。
388
+ const head = line.match(/^((?:\x1b\[[0-9;]*m)*)( *)/);
389
+ const ansi = head?.[1] ?? "";
390
+ const spaces = head?.[2]?.length ?? 0;
391
+ const first = lines.length === 0;
392
+ if (first) liveLead = spaces;
393
+ // 首行挂 muted 的 `↳`(与 default-mode 工具结果行同色);续行按 liveLead 去缩进。
394
+ // 两侧都保留行首 SGR:丢掉它整行会掉回终端默认前景色。
395
+ const marker = first ? `${theme.fg("muted", "↳")} ` : " ";
396
+ const lead = first ? spaces : Math.min(spaces, liveLead);
397
+ lines.push(
398
+ `${marker}${ansi}${truncateToWidth(line.slice(ansi.length + lead), innerWidth, "")}`,
399
+ );
400
+ };
401
+ const pushBlank = () => {
402
+ if (live || lastBlank) return;
403
+ lines.push(paddedBackgroundRow(theme, slot, "", width));
404
+ lastBlank = true;
405
+ };
406
+ for (let childIndex = 0; childIndex < children.length; childIndex++) {
407
+ const child = children[childIndex];
408
+ const painted = paints?.[childIndex];
409
+ const childLines = Array.isArray(painted) ? painted : child.render(innerWidth);
410
+ // live 不留空行、也不叠深色内卡:槽位没有边框。
411
+ const innerCard = !live && isThinkingPreview(child) && child.expanded === true;
364
412
  let first = -1;
365
413
  let last = -1;
366
414
  for (let i = 0; i < childLines.length; i++) {
@@ -370,25 +418,41 @@ function layoutExpandedToolCard(
370
418
  }
371
419
  }
372
420
  if (first < 0) {
421
+ if (live) continue;
373
422
  for (const line of childLines) {
374
423
  lines.push(paddedBackgroundRow(theme, slot, line, width));
424
+ lastBlank = !hasVisibleText(line);
375
425
  }
376
426
  continue;
377
427
  }
378
- lines.push(paddedBackgroundRow(theme, slot, "", width));
379
- lines.push(toolCardBgRow(theme, slot, toolBgAnsi, "", width));
380
- for (let i = first; i <= last; i++) {
381
- lines.push(toolCardBgRow(theme, slot, toolBgAnsi, childLines[i], width));
428
+ if (!innerCard) pushBlank();
429
+ const rangeStart = lines.length;
430
+ // 展开的 thinking 走深色内卡(上下各 1 行 padding),工具卡直接铺在外卡上。
431
+ const paintRow = innerCard
432
+ ? (line: string) => lines.push(toolCardBgRow(theme, slot, toolBgAnsi, line, width))
433
+ : pushRow;
434
+ if (innerCard) paintRow("");
435
+ for (let i = first; i <= last; i++) paintRow(childLines[i]);
436
+ if (innerCard) paintRow("");
437
+ // 内卡的下 padding 不能当分隔:后面的子卡要再留 1 行外卡空行。
438
+ lastBlank = false;
439
+ // thinking 命中自身;工具在展开卡内需要能二次展开时才映射回 tool 组件。
440
+ if (isThinkingPreview(child)) hits.push({ child, start: rangeStart, end: lines.length });
441
+ else if (toolHits && child.__ccTool) {
442
+ hits.push({ child: child.__ccTool, start: rangeStart, end: lines.length });
382
443
  }
383
- lines.push(toolCardBgRow(theme, slot, toolBgAnsi, "", width));
384
444
  }
385
- lines.push(paddedBackgroundRow(theme, slot, "", width));
445
+ pushBlank();
386
446
  return { lines, hits };
387
447
  }
388
448
 
389
449
  function compactRoundCard(
390
450
  cardItems: Array<{ child?: any; tool?: any }>,
391
451
  toolRender: (tool: any, width: number) => string[],
452
+ /** true 时卡内工具行归 tool 组件,可点击二次展开(整回合展开卡)。 */
453
+ toolHits = false,
454
+ /** true 时走 live 槽位排布:不留空行、首行紧跟 `↳`。 */
455
+ live = false,
392
456
  ): any {
393
457
  const children: any[] = [];
394
458
  for (const item of cardItems) {
@@ -397,26 +461,67 @@ function compactRoundCard(
397
461
  const tool = item.tool;
398
462
  children.push({
399
463
  __ccToolCard: true,
464
+ __ccTool: tool,
400
465
  render: (innerWidth: number) => toolRender(tool, innerWidth),
401
466
  invalidate: () => tool.invalidate?.(),
402
467
  });
403
468
  }
404
469
  }
470
+ /** 子块可用宽度:live 还要让出首行 `↳ ` 的标记位。 */
471
+ const contentWidth = (width: number) => Math.max(0, width - 2 - (live ? 2 : 0));
472
+ let paint:
473
+ | {
474
+ width: number;
475
+ theme: unknown;
476
+ /** 背景槽位也是面板行的输入:改配置后不能复用旧底色的整卡。 */
477
+ bgSlot: string;
478
+ paints: unknown[];
479
+ lines: string[];
480
+ hits: Array<{ child: any; start: number; end: number }>;
481
+ }
482
+ | undefined;
483
+ const layout = (width: number) => {
484
+ const theme = themeOf();
485
+ const bgSlot = config.expandedCardBackground;
486
+ const innerWidth = contentWidth(width);
487
+ const paints = children.map((child) => {
488
+ const lines = child.render?.(innerWidth);
489
+ return Array.isArray(lines) ? lines : [];
490
+ });
491
+ if (
492
+ paint &&
493
+ paint.width === width &&
494
+ paint.theme === theme &&
495
+ paint.bgSlot === bgSlot &&
496
+ paint.paints.length === paints.length &&
497
+ paint.paints.every((item, index) => item === paints[index])
498
+ ) {
499
+ return paint;
500
+ }
501
+ const laid = layoutExpandedToolCard(theme, children, width, paints, toolHits, live);
502
+ paint = {
503
+ width,
504
+ theme,
505
+ bgSlot,
506
+ paints,
507
+ lines: laid.lines,
508
+ hits: laid.hits,
509
+ };
510
+ return paint;
511
+ };
405
512
  return {
406
513
  children,
407
514
  render(width: number): string[] {
408
- return ["", ...layoutExpandedToolCard(themeOf(), children, width).lines];
515
+ return layout(width).lines;
409
516
  },
410
517
  childAtRow(localRow: number, width: number) {
411
- if (localRow < 1) return null;
412
- const row = localRow - 1;
413
- const { hits } = layoutExpandedToolCard(themeOf(), children, width);
414
- for (const hit of hits) {
415
- if (row >= hit.start && row < hit.end) return hit.child;
518
+ for (const hit of layout(width).hits) {
519
+ if (localRow >= hit.start && localRow < hit.end) return hit.child;
416
520
  }
417
521
  return null;
418
522
  },
419
523
  invalidate() {
524
+ paint = undefined;
420
525
  for (const child of children) child.invalidate?.();
421
526
  },
422
527
  };
@@ -431,28 +536,16 @@ function isToolComponent(value: any): boolean {
431
536
  }
432
537
 
433
538
  function detachAssistantExpansion(component: any): void {
434
- let owned =
435
- typeof component?.[ASSISTANT_SET_EXPANDED_KEY] === "function" &&
436
- component.setExpanded === component[ASSISTANT_SET_EXPANDED_KEY];
437
- if (!owned && component && typeof component === "object") {
438
- // 首次升级时清理旧模块使用 Symbol() 留下的实例方法。
439
- owned = Object.getOwnPropertySymbols(component).some(
440
- (symbol) =>
441
- symbol.description === ASSISTANT_SET_EXPANDED_DESCRIPTION && component[symbol] === true,
442
- );
539
+ if (
540
+ typeof component?.[ASSISTANT_SET_EXPANDED_KEY] !== "function" ||
541
+ component.setExpanded !== component[ASSISTANT_SET_EXPANDED_KEY]
542
+ ) {
543
+ return;
443
544
  }
444
- if (!owned) return;
445
-
446
545
  delete component.setExpanded;
447
- for (const symbol of Object.getOwnPropertySymbols(component)) {
448
- if (
449
- symbol.description === ASSISTANT_SET_EXPANDED_DESCRIPTION ||
450
- symbol.description === ASSISTANT_TOGGLE_ROUND_DESCRIPTION ||
451
- symbol.description === ASSISTANT_REENTRY_DESCRIPTION
452
- ) {
453
- delete component[symbol];
454
- }
455
- }
546
+ delete component[ASSISTANT_SET_EXPANDED_KEY];
547
+ delete component[ASSISTANT_TOGGLE_ROUND_KEY];
548
+ delete component[ASSISTANT_REENTRY_KEY];
456
549
  }
457
550
 
458
551
  /** 供 mouse-interaction 识别可点击的 compact assistant 行(仅 compact 模式下生效)。 */
@@ -500,27 +593,91 @@ const trackedAssistantComponents = new Set<any>();
500
593
  const trackedToolComponents = new Set<any>();
501
594
  let hoveredAssistantComponent: any;
502
595
 
596
+ /**
597
+ * 活动回合摘要 getter:常驻状态行(pi working row)镜像用。
598
+ * installCompactMode 注册、卸载时清空,直接读 activeRound,不持有回合闭包。
599
+ */
600
+ let compactRunStatusGetter: (() => string | undefined) | undefined;
601
+
602
+ /** 活动回合的摘要文本(`Running... · 9s, bash×1`);无活动回合返回 undefined。 */
603
+ export function getCompactRunStatusText(): string | undefined {
604
+ return compactRunStatusGetter?.();
605
+ }
606
+
607
+ /**
608
+ * 回合尾行记录:摘要行(live 时连槽位卡)挂成 transcript 容器里回合末尾的
609
+ * 兄弟组件,而不是 anchor 内部。anchor 内部的可变内容一旦被工具/diff 顶出视口
610
+ * 就再也写不进 scrollback(regular 主屏只能差分可视区),运行态 Running 会永久
611
+ * 定格在挤出前最后一帧;尾行恒在可写底缘,收尾的 Ran for 才真实落进历史。
612
+ * 模块级持有(与 trackedAssistantComponents 同理):跨 /reload 去重与清理。
613
+ */
614
+ type MountedRoundTail = {
615
+ host: any;
616
+ container: any;
617
+ parts: any[];
618
+ round: any;
619
+ };
620
+ const tailByAnchor = new WeakMap<object, MountedRoundTail>();
621
+ const mountedTails = new Set<MountedRoundTail>();
622
+
503
623
  export function setHoveredCompactAssistant(component: any): boolean {
504
624
  if (hoveredAssistantComponent === component) return false;
505
625
  hoveredAssistantComponent = component;
506
626
  return true;
507
627
  }
508
628
 
629
+ /**
630
+ * 整回合展开卡内的工具被点击展开时调用:让 compact 的强制折叠放行这一个,
631
+ * 并把同一回合里其他展开的工具收回去(保持单开)。非 round 内工具是空操作。
632
+ */
633
+ let roundToolExpansion: { markUserExpanded(tool: any): void } | undefined;
634
+
635
+ export function markCompactRoundToolExpanded(tool: any): void {
636
+ roundToolExpansion?.markUserExpanded(tool);
637
+ }
638
+
639
+ /** 面板展开前的视口位置:收起时还原,避免面板撑高后收起导致视口错位。 */
640
+ const panelViewportBeforeExpand = new WeakMap<
641
+ object,
642
+ { scrollTop: number; followingEnd: boolean }
643
+ >();
644
+
645
+ function capturePanelViewport(card: any): void {
646
+ if (!card || panelViewportBeforeExpand.has(card)) return;
647
+ const view = getToolMouseTui()?.getPrimaryScrollView?.();
648
+ if (!view || typeof view.scrollTop !== "number" || typeof view.scrollTo !== "function") return;
649
+ panelViewportBeforeExpand.set(card, {
650
+ scrollTop: view.scrollTop,
651
+ followingEnd: view.isFollowingEnd !== false,
652
+ });
653
+ }
654
+
655
+ function restorePanelViewport(card: any): void {
656
+ const snapshot = panelViewportBeforeExpand.get(card);
657
+ if (!snapshot) return;
658
+ panelViewportBeforeExpand.delete(card);
659
+ const view = getToolMouseTui()?.getPrimaryScrollView?.();
660
+ if (!view) return;
661
+ // 展开前就在底部:交回官方 follow;否则回到展开前的滚动偏移。
662
+ if (snapshot.followingEnd && typeof view.scrollToEnd === "function") view.scrollToEnd();
663
+ else if (typeof view.scrollTo === "function") view.scrollTo(snapshot.scrollTop);
664
+ }
665
+
509
666
  function compactEditWriteLine(
510
667
  component: any,
511
668
  width: number,
512
669
  writeMetadata?: WriteExecutionMetadataStore,
670
+ options: { hint?: boolean; flushLeft?: boolean } = {},
513
671
  ): string[] {
514
672
  const theme = themeOf();
515
673
  const name = String(component.toolName ?? "tool");
516
674
  const args = component.args ?? {};
517
- const path = sanitizeToolResultText(
675
+ const path =
518
676
  typeof args.path === "string" && args.path
519
677
  ? args.path
520
678
  : typeof args.file_path === "string" && args.file_path
521
679
  ? args.file_path
522
- : "",
523
- );
680
+ : "";
524
681
  const isError = component.result?.isError === true;
525
682
  const isPending = !component.result || component.isPartial === true;
526
683
  const icon = isError ? "✗" : isPending ? toolLoadingIcon() : "✓";
@@ -543,9 +700,11 @@ function compactEditWriteLine(
543
700
  statsStyled = ` ${theme.fg("dim", "(")}${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)}${theme.fg("dim", ")")}`;
544
701
  }
545
702
  }
546
- const iconPart = ` ${theme.fg(iconColor, icon)} `;
703
+ // 展开卡 Box(1,1) 已 pad;折叠行自己留 1 格前导空格
704
+ const iconPart = `${options.flushLeft ? "" : " "}${theme.fg(iconColor, icon)} `;
547
705
  const namePart = theme.fg("toolTitle", name);
548
- const hintText = component.expanded ? "" : ` • ${showMoreHintText()}`;
706
+ const hintText =
707
+ options.hint !== false && component.expanded !== true ? ` · ${showMoreHintText()}` : "";
549
708
  const fixedWidth =
550
709
  visibleWidth(iconPart) +
551
710
  visibleWidth(namePart) +
@@ -558,34 +717,133 @@ function compactEditWriteLine(
558
717
  return ["", truncateToWidth(line, width, "")];
559
718
  }
560
719
 
561
- /** compact edit/write 展开:复用 mode=on 的 rich diff;不可用时回退 Input/Output。 */
562
- function compactEditWriteExpandedLines(
720
+ /** expanded 写进 rich diff 闭包,折叠/展开分槽缓存,避免每帧 parseDiff。 */
721
+ const compactRichDiffCache = new WeakMap<
722
+ object,
723
+ { result: unknown; collapsed?: unknown; expanded?: unknown }
724
+ >();
725
+
726
+ type CompactEditPaintHit = {
727
+ width: number;
728
+ theme: unknown;
729
+ /** 展开卡的背景槽位同样参与命中判断。 */
730
+ bgSlot: string;
731
+ expanded: boolean;
732
+ result: unknown;
733
+ isPartial: boolean;
734
+ args: unknown;
735
+ hover: boolean;
736
+ ioHover: unknown;
737
+ lines: string[];
738
+ };
739
+ const compactEditPaintCache = new WeakMap<object, CompactEditPaintHit>();
740
+
741
+ function compactEditIoHover(component: any): unknown {
742
+ const view = component?.resultRendererComponent;
743
+ return typeof view?.getHoveredSection === "function" ? view.getHoveredSection() : null;
744
+ }
745
+
746
+ /**
747
+ * compact edit/write:标题行 + mode=on 同一套 rich diff。
748
+ * 折叠/展开都走 `renderRichToolResult`,limits 不另开一套。
749
+ * 已完成的结果跨帧复用行;pending 不缓存,避免 loader 动画冻住。
750
+ */
751
+ function compactEditWriteLines(
752
+ component: any,
753
+ width: number,
754
+ writeMetadata?: WriteExecutionMetadataStore,
755
+ ): string[] {
756
+ const expanded = component.expanded === true;
757
+ const isPartial = component.isPartial === true;
758
+ const pending = !component.result || isPartial;
759
+ const hover = isToolCallHovered(component.toolCallId);
760
+ const ioHover = compactEditIoHover(component);
761
+ const theme = themeOf();
762
+ if (!pending) {
763
+ const hit = compactEditPaintCache.get(component);
764
+ if (
765
+ hit &&
766
+ hit.width === width &&
767
+ hit.theme === theme &&
768
+ hit.bgSlot === config.expandedCardBackground &&
769
+ hit.expanded === expanded &&
770
+ hit.result === component.result &&
771
+ hit.isPartial === isPartial &&
772
+ hit.args === component.args &&
773
+ hit.hover === hover &&
774
+ hit.ioHover === ioHover
775
+ ) {
776
+ return hit.lines;
777
+ }
778
+ }
779
+ const lines = paintCompactEditWrite(component, width, writeMetadata);
780
+ if (!pending) {
781
+ compactEditPaintCache.set(component, {
782
+ width,
783
+ theme,
784
+ bgSlot: config.expandedCardBackground,
785
+ expanded,
786
+ result: component.result,
787
+ isPartial,
788
+ args: component.args,
789
+ hover,
790
+ ioHover: compactEditIoHover(component),
791
+ lines,
792
+ });
793
+ }
794
+ return lines;
795
+ }
796
+
797
+ function paintCompactEditWrite(
563
798
  component: any,
564
799
  width: number,
565
800
  writeMetadata?: WriteExecutionMetadataStore,
566
801
  ): string[] {
567
802
  const theme = themeOf();
568
803
  const result = component.result;
804
+ const expanded = component.expanded === true;
569
805
  const isError = result?.isError === true;
570
- const candidate = writeMetadata
571
- ? renderRichToolResult(
806
+ const isPending = !result || component.isPartial === true;
807
+ let candidate: unknown;
808
+ if (!isPending && writeMetadata) {
809
+ let entry = compactRichDiffCache.get(component);
810
+ if (!entry || entry.result !== result) {
811
+ entry = { result };
812
+ compactRichDiffCache.set(component, entry);
813
+ }
814
+ const slot = expanded ? "expanded" : "collapsed";
815
+ candidate = entry[slot];
816
+ if (!isRichDiffComponent(candidate)) {
817
+ candidate = renderRichToolResult(
572
818
  String(component.toolName ?? ""),
573
819
  result,
574
820
  {
575
- expanded: true,
821
+ expanded,
576
822
  isPartial: component.isPartial === true,
577
823
  isError,
824
+ isHovered: () => isToolCallHovered(component.toolCallId),
578
825
  },
579
826
  theme,
580
827
  component,
581
828
  writeMetadata,
582
829
  getToolDisplayConfig,
583
- )
584
- : undefined;
585
- let detail: any;
586
- if (isRichDiffComponent(candidate)) {
830
+ );
831
+ if (isRichDiffComponent(candidate)) entry[slot] = candidate;
832
+ }
833
+ }
834
+ const hasRich = isRichDiffComponent(candidate);
835
+ if (hasRich) {
587
836
  component.resultRendererComponent = candidate;
588
- detail = insetComponent(candidate as any);
837
+ }
838
+
839
+ const title = compactEditWriteLine(component, width, writeMetadata, { hint: !hasRich });
840
+ if (!hasRich && !expanded) {
841
+ return title;
842
+ }
843
+
844
+ let detail: any;
845
+ if (hasRich) {
846
+ detail = expanded ? candidate : insetComponent(candidate as any);
589
847
  } else {
590
848
  const outputText = sanitizeToolResultText(
591
849
  Array.isArray(result?.content)
@@ -603,19 +861,28 @@ function compactEditWriteExpandedLines(
603
861
  state.ccstyleIoView,
604
862
  component.args,
605
863
  component,
864
+ true, // 外层 Box(1,1) 已 pad
606
865
  );
607
866
  component.resultRendererComponent = detail;
608
867
  }
609
868
 
869
+ if (!expanded) {
870
+ return [...title, ...detail.render(width)];
871
+ }
872
+
610
873
  const box = editWriteExpandedCard(theme);
611
874
  box.addChild({
612
875
  render(innerWidth: number): string[] {
613
- return compactEditWriteLine(component, innerWidth, writeMetadata).slice(1);
876
+ return compactEditWriteLine(component, innerWidth, writeMetadata, {
877
+ hint: false,
878
+ flushLeft: true,
879
+ }).slice(1);
614
880
  },
615
881
  invalidate() {},
616
882
  });
617
883
  box.addChild(detail);
618
- return box.render(width);
884
+ // 与原生 ToolExecutionComponent 一样:Spacer(1) + Box,否则贴上一条 tool 少 1 行间距
885
+ return ["", ...box.render(width)];
619
886
  }
620
887
 
621
888
  function compactAssistantLineComponent(
@@ -626,17 +893,49 @@ function compactAssistantLineComponent(
626
893
  options: { hint?: boolean; leadingBlank?: boolean; pad?: number } = {},
627
894
  ): any {
628
895
  const self = component as any;
896
+ let paint:
897
+ | {
898
+ width: number;
899
+ theme: unknown;
900
+ resolved: string;
901
+ hover: boolean;
902
+ hint: boolean;
903
+ pad: number;
904
+ frame: number;
905
+ leadingBlank: boolean;
906
+ lines: string[];
907
+ }
908
+ | undefined;
629
909
  return {
630
910
  render(width: number): string[] {
631
911
  const theme = themeOf();
632
912
  const pad = Math.max(0, options.pad ?? (Number(self.outputPad) || 0));
633
913
  const available = Math.max(0, width - pad);
634
- const hintText = options.hint === false ? "" : ` • ${showMoreHintText()}`;
914
+ const hint = options.hint !== false;
915
+ const hintText = hint ? ` · ${showMoreHintText()}` : "";
635
916
  const summaryWidth = Math.max(0, available - visibleWidth(hintText));
636
917
  const resolved = typeof summary === "function" ? summary() : summary;
637
918
  const runningActive = resolved.startsWith("Running...");
919
+ const hover = hoveredAssistantComponent === component;
920
+ const frame = runningActive ? (query?.getThinkingAnimationFrame?.() ?? 0) : 0;
921
+ const leadingBlank = options.leadingBlank !== false;
922
+ if (
923
+ paint &&
924
+ paint.width === width &&
925
+ paint.theme === theme &&
926
+ paint.resolved === resolved &&
927
+ paint.hover === hover &&
928
+ paint.hint === hint &&
929
+ paint.pad === pad &&
930
+ paint.frame === frame &&
931
+ paint.leadingBlank === leadingBlank
932
+ ) {
933
+ return paint.lines;
934
+ }
638
935
  const plainText = truncateToWidth(resolved, summaryWidth, "…");
639
- let text = theme.fg("muted", plainText);
936
+ // Dim thinking text 开启时整行统一 dim,否则工具计数保持 muted。
937
+ const plain = (value: string) => theme.fg(config.dimThinkingText ? "dim" : "muted", value);
938
+ let text = plain(plainText);
640
939
  if (runningActive || plainText.startsWith("Ran for ")) {
641
940
  const separator = plainText.indexOf(", ");
642
941
  const heading = separator < 0 ? plainText : plainText.slice(0, separator);
@@ -645,21 +944,31 @@ function compactAssistantLineComponent(
645
944
  const durationSeparator = heading.indexOf(" · ");
646
945
  const label = durationSeparator < 0 ? heading : heading.slice(0, durationSeparator);
647
946
  const duration = durationSeparator < 0 ? "" : heading.slice(durationSeparator);
648
- text = `${animateCompactThinkingText(
649
- label,
650
- theme,
651
- query?.getThinkingAnimationFrame?.() ?? 0,
652
- )}${styleCompactThinkingText(duration, theme)}${theme.fg("muted", tools)}`;
947
+ text = `${animateCompactThinkingText(label, theme, query?.getThinkingAnimationFrame?.() ?? 0)}${styleCompactThinkingText(duration, theme)}${plain(tools)}`;
653
948
  } else {
654
- text = `${styleCompactThinkingText(heading, theme)}${theme.fg("muted", tools)}`;
949
+ text = `${styleCompactThinkingText(heading, theme)}${plain(tools)}`;
655
950
  }
656
951
  }
657
- const hintColor = hoveredAssistantComponent === component ? "text" : "dim";
952
+ const hintColor = hover ? "text" : "dim";
658
953
  const line = `${text}${hintText ? theme.fg(hintColor, hintText) : ""}`;
659
954
  const rendered = `${" ".repeat(pad)}${truncateToWidth(line, available, "")}`;
660
- return options.leadingBlank === false ? [rendered] : ["", rendered];
955
+ const lines = leadingBlank ? ["", rendered] : [rendered];
956
+ paint = {
957
+ width,
958
+ theme,
959
+ resolved,
960
+ hover,
961
+ hint,
962
+ pad,
963
+ frame,
964
+ leadingBlank,
965
+ lines,
966
+ };
967
+ return lines;
968
+ },
969
+ invalidate() {
970
+ paint = undefined;
661
971
  },
662
- invalidate() {},
663
972
  };
664
973
  }
665
974
 
@@ -675,14 +984,50 @@ function compactStopStatusLine(status: string, pad = 0): any {
675
984
  };
676
985
  }
677
986
 
678
- function compactAssistantLine(
679
- component: any,
680
- summary: string | (() => string),
681
- query?: CompactThinkingQuery,
682
- ): void {
683
- // 空静态串跳过;getter 留给 render 判断(Running 可能稍后才有时长)。
684
- if (typeof summary === "string" && !summary) return;
685
- component.contentContainer.addChild(compactAssistantLineComponent(component, summary, query));
987
+ /**
988
+ * 槽位卡内的展开入口:卡里是只读围观,展开走摘要行自己的 hint,卡内不再重复提示。
989
+ * 提示有两处来源——我们的 showMoreHintText(`… +N more lines · click to show more`)与
990
+ * pi 原生工具卡(`… (N earlier lines, ctrl+o to expand)`),所以按渲染结果收尾裁剪。
991
+ * 只去掉动作词,`(N more lines)` 这类计数保留。
992
+ */
993
+ /** 展开入口的动作词:我们的 `click to show more`、pi 的 `<key> to expand`(键名可能取不到)。 */
994
+ const EXPAND_ACTION = String.raw`(?:click to show more|(?:[\w/+]+\s+)?to (?:show more|expand))`;
995
+
996
+ const SLOT_HINT_PATTERNS: RegExp[] = [
997
+ // 卡在括号里:`(9 more lines, click to show more)` / `(195 earlier lines, ctrl+o to expand)`,
998
+ // 只去掉动作词,收尾括号与计数保留。
999
+ new RegExp(String.raw`,\s*[^()]*?${EXPAND_ACTION}(?=\))`),
1000
+ // 直接挂在行尾:整段去掉。
1001
+ // 尾部分隔点:ccstyle 统一 `·`,同时兼容旧帧与 pi 原生的 `•`
1002
+ new RegExp(String.raw`\s*(?:[•·]\s*)?${EXPAND_ACTION}\s*$`),
1003
+ ];
1004
+
1005
+ function stripSlotHint(line: string): string {
1006
+ const plain = stripTerminalSequencesPreservingLayout(line);
1007
+ for (const re of SLOT_HINT_PATTERNS) {
1008
+ const match = re.exec(plain);
1009
+ if (!match) continue;
1010
+ // 删除区间而非截断后拼纯文本:收尾括号留在原样式里(dim),不会掉回默认前景色。
1011
+ return removeStyledRange(line, match.index, match.index + match[0].length);
1012
+ }
1013
+ return line;
1014
+ }
1015
+
1016
+ /** live 槽位卡:按 pad 缩进;首行 `↳` 由卡自身的 live 排布加,行不增不减。 */
1017
+ function compactLiveSlot(card: any, pad = 0): any {
1018
+ const innerOf = (width: number) => Math.max(0, width - Math.max(0, pad));
1019
+ return {
1020
+ render(width: number): string[] {
1021
+ const indent = " ".repeat(Math.max(0, pad));
1022
+ return card.render(innerOf(width)).map((line: string) => `${indent}${line}`);
1023
+ },
1024
+ childAtRow(localRow: number, width: number) {
1025
+ return card.childAtRow?.(localRow, innerOf(width)) ?? null;
1026
+ },
1027
+ invalidate() {
1028
+ card.invalidate();
1029
+ },
1030
+ };
686
1031
  }
687
1032
 
688
1033
  function appendStopStatus(component: any, status: string | undefined): void {
@@ -724,16 +1069,9 @@ function ensureAssistantSetExpanded(component: any): void {
724
1069
 
725
1070
  function collectMountedComponents(root: any): void {
726
1071
  if (!root || typeof root !== "object") return;
727
- const seen = new Set<any>();
728
1072
  const assistants = new Set<any>();
729
1073
  const tools = new Set<any>();
730
- const visit = (value: any): void => {
731
- if (!value || typeof value !== "object" || seen.has(value)) return;
732
- seen.add(value);
733
- if (Array.isArray(value)) {
734
- for (const child of value) visit(child);
735
- return;
736
- }
1074
+ walkComponentTree(root, (value: any) => {
737
1075
  if (isAssistantComponent(value)) {
738
1076
  assistants.add(value);
739
1077
  // 仅在 compact 模式给实例装 setExpanded,避免 on/off 下 Ctrl+O/mouse 回归。
@@ -741,20 +1079,7 @@ function collectMountedComponents(root: any): void {
741
1079
  } else if (isToolComponent(value)) {
742
1080
  tools.add(value);
743
1081
  }
744
- const children = value.children;
745
- if (Array.isArray(children)) {
746
- for (const child of children) visit(child);
747
- }
748
- try {
749
- const mounted = value.getMountedRoots?.();
750
- if (Array.isArray(mounted)) {
751
- for (const root2 of mounted) visit(root2);
752
- }
753
- } catch {
754
- // renderer 切换中的惰性 Proxy 可能暂时没有 mounted roots
755
- }
756
- };
757
- visit(root);
1082
+ });
758
1083
  // 扫到组件才替换跟踪表。面板/custom UI 打开时 root 往往扫不到 transcript,
759
1084
  // 若此时清空会丢掉 live updateContent/updateDisplay 已登记的实例,
760
1085
  // /ccstyle 切换就只能靠 /reload 重建。
@@ -769,14 +1094,8 @@ function collectMountedComponents(root: any): void {
769
1094
  }
770
1095
 
771
1096
  export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHooks {
772
- const host = globalThis as any;
773
- const previous = host[COMPACT_MODE_PATCH_KEY] as CompactModePatch | undefined;
774
- if (previous) {
775
- // Replacement reload:先释放旧 compact 外层,再清理其 thinking owner,
776
- // 否则下面会把旧 thinking wrapper 误采样为 native prototype。
777
- previous.dispose();
778
- host[COMPACT_THINKING_OWNER_KEY]?.stop?.();
779
- }
1097
+ const previous = patchRegistry.get<CompactModePatch>(COMPACT_MODE_PATCH_KEY);
1098
+ if (previous) previous.dispose();
780
1099
 
781
1100
  const assistantPrototype = AssistantMessageComponent.prototype as any;
782
1101
  const toolPrototype = ToolExecutionComponent.prototype as any;
@@ -812,13 +1131,24 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
812
1131
  detachedMessages: any[];
813
1132
  active: boolean;
814
1133
  suppressedToolIds: Set<string>;
1134
+ /** live 槽位内联渲染的工具 id;回合结束时随槽位一起交还外层。 */
1135
+ liveSlotToolIds: Set<string>;
1136
+ /** 收尾后的静默期截止时间戳;期间仍按围观态渲染,到点才收拢。 */
1137
+ settleUntil?: number;
1138
+ foldTimer?: ReturnType<typeof setTimeout>;
815
1139
  /** 回合挂钟起点,保证 Running 时长连续递增。 */
816
1140
  startedAt: number;
817
1141
  endedAt?: number;
1142
+ /** 回合尾行(摘要 + live 槽位),挂在 transcript 容器的回合末尾。 */
1143
+ tail?: MountedRoundTail;
818
1144
  };
819
1145
  let activeRound: CompactRound | undefined;
820
1146
  let roundByComponent = new WeakMap<object, CompactRound>();
821
1147
  const expandedRoundToolIds = new Set<string>();
1148
+ /** 用户显式点开的 round 内工具;强制折叠不压回这些 id。 */
1149
+ const explicitRoundToolIds = new Set<string>();
1150
+ const liveSlotToolIds = new Set<string>();
1151
+ const pendingFoldRounds = new Set<CompactRound>();
822
1152
  let uiRef: { requestRender?: (force?: boolean) => void } | undefined;
823
1153
  let roundTickTimer: ReturnType<typeof setInterval> | undefined;
824
1154
 
@@ -849,17 +1179,32 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
849
1179
  stopRoundTick();
850
1180
  return;
851
1181
  }
1182
+ settleStrandedRounds();
852
1183
  try {
853
- uiRef?.requestRender?.(true);
1184
+ // 非 force:保留 fullscreen 布局缓存和差分绘制。
1185
+ uiRef?.requestRender?.();
854
1186
  } catch {
855
1187
  /* 无 UI */
856
1188
  }
857
1189
  }, 250);
858
- roundTickTimer.unref?.();
859
1190
  };
860
1191
 
861
- /** 结束回合活动态;可选立即重绘。 */
862
- const endRound = (round: CompactRound, render = false): void => {
1192
+ /** 围观中:活动回合,或收尾静默期还没走完的回合(槽位卡仍挂在屏幕上)。 */
1193
+ const roundLive = (round: CompactRound): boolean =>
1194
+ round.active || (round.settleUntil !== undefined && Date.now() < round.settleUntil);
1195
+
1196
+ const clearFoldTimer = (round: CompactRound): void => {
1197
+ if (round.foldTimer !== undefined) {
1198
+ clearTimeout(round.foldTimer);
1199
+ round.foldTimer = undefined;
1200
+ }
1201
+ round.settleUntil = undefined;
1202
+ pendingFoldRounds.delete(round);
1203
+ };
1204
+
1205
+ /** 结束回合活动态;不做任何重绘。 */
1206
+ const finalizeRound = (round: CompactRound): void => {
1207
+ clearFoldTimer(round);
863
1208
  round.active = false;
864
1209
  if (round.endedAt === undefined) round.endedAt = Date.now();
865
1210
  if (activeRound === round) {
@@ -867,7 +1212,36 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
867
1212
  deps.query?.setCompactSummaryActive?.(false);
868
1213
  stopRoundTick();
869
1214
  }
870
- if (render) renderRound(round);
1215
+ };
1216
+
1217
+ /**
1218
+ * 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。
1219
+ * 分组语义不受影响:activeRound 立即让位,新消息不会被并进已收尾的回合。
1220
+ */
1221
+ const endRound = (round: CompactRound, render = false): void => {
1222
+ if (!render || !round.active) {
1223
+ finalizeRound(round);
1224
+ if (render) renderRound(round);
1225
+ return;
1226
+ }
1227
+ finalizeRound(round);
1228
+ if (round.foldTimer !== undefined) return;
1229
+ round.settleUntil = Date.now() + FOLD_SETTLE_MS;
1230
+ pendingFoldRounds.add(round);
1231
+ round.foldTimer = setTimeout(() => {
1232
+ round.foldTimer = undefined;
1233
+ round.settleUntil = undefined;
1234
+ pendingFoldRounds.delete(round);
1235
+ renderRound(round);
1236
+ }, FOLD_SETTLE_MS);
1237
+ };
1238
+
1239
+ /** 新回合接替时立即收掉还在静默期的回合,把两次转场并成一次。 */
1240
+ const flushSettle = (): void => {
1241
+ for (const round of [...pendingFoldRounds]) {
1242
+ clearFoldTimer(round);
1243
+ renderRound(round);
1244
+ }
871
1245
  };
872
1246
 
873
1247
  const renderAssistantWithoutThinking = (
@@ -888,11 +1262,46 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
888
1262
  return result;
889
1263
  };
890
1264
 
1265
+ /** compact 只允许单开:收回一张工具卡的展开态。 */
1266
+ const collapseTool = (tool: any): void => {
1267
+ if (typeof tool?.setExpanded === "function") tool.setExpanded(false);
1268
+ else {
1269
+ tool.expanded = false;
1270
+ tool.updateDisplay?.();
1271
+ }
1272
+ };
1273
+
1274
+ /** 按条件收回已展开的工具卡。 */
1275
+ const collapseTools = (shouldCollapse: (tool: any) => boolean): void => {
1276
+ for (const tool of trackedToolComponents) {
1277
+ if (tool.expanded === true && shouldCollapse(tool)) collapseTool(tool);
1278
+ }
1279
+ };
1280
+
1281
+ const markRoundToolUserExpanded = (tool: any): void => {
1282
+ const id = String(tool?.toolCallId ?? "");
1283
+ if (!id || !expandedRoundToolIds.has(id)) return;
1284
+ for (const other of trackedToolComponents) {
1285
+ const otherId = String(other?.toolCallId ?? "");
1286
+ if (other === tool || !expandedRoundToolIds.has(otherId) || other.expanded !== true) continue;
1287
+ explicitRoundToolIds.delete(otherId);
1288
+ collapseTool(other);
1289
+ }
1290
+ explicitRoundToolIds.add(id);
1291
+ };
1292
+ roundToolExpansion = { markUserExpanded: markRoundToolUserExpanded };
1293
+
891
1294
  const roundMessages = (round: CompactRound): any[] => [
892
1295
  ...round.messages.values(),
893
1296
  ...round.detachedMessages,
894
1297
  ];
895
1298
 
1299
+ /** 镜像给常驻状态行的文案:只认当前活动回合(摘要行被顶出视口时由 working-message 取用)。 */
1300
+ compactRunStatusGetter = () => {
1301
+ const round = activeRound;
1302
+ return round?.active ? summarize(roundMessages(round), true, round) || undefined : undefined;
1303
+ };
1304
+
896
1305
  const roundToolCallIds = (round: CompactRound): Set<string> => {
897
1306
  const ids = new Set<string>();
898
1307
  for (const message of roundMessages(round)) {
@@ -903,83 +1312,552 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
903
1312
  return ids;
904
1313
  };
905
1314
 
1315
+ /**
1316
+ * anchor 所在的 transcript 容器(pi chatContainer)。
1317
+ * 优先走分组补丁在 addChild 时挂的父指针;没有则沿 tui 组件树找直属父容器。
1318
+ */
1319
+ const tailContainerOf = (anchor: any): any => {
1320
+ const keyed = anchor?.[TOOL_GROUPING_PARENT_KEY];
1321
+ if (keyed && Array.isArray(keyed.children) && keyed.children.includes(anchor)) {
1322
+ return keyed;
1323
+ }
1324
+ let found: any;
1325
+ walkComponentTree(getToolMouseTui(), (value: any) => {
1326
+ if (found) return false;
1327
+ if (Array.isArray(value?.children) && value.children.includes(anchor)) {
1328
+ found = value;
1329
+ return false;
1330
+ }
1331
+ return undefined;
1332
+ });
1333
+ return found;
1334
+ };
1335
+
1336
+ /** 组件在树里的直属父容器。TUI 切换/reparent 后缓存的 container 会过期。 */
1337
+ const containerOfHost = (host: any): any => {
1338
+ let found: any;
1339
+ walkComponentTree(getToolMouseTui(), (value: any) => {
1340
+ if (found) return false;
1341
+ if (Array.isArray(value?.children) && value.children.includes(host)) {
1342
+ found = value;
1343
+ return false;
1344
+ }
1345
+ return undefined;
1346
+ });
1347
+ return found;
1348
+ };
1349
+
1350
+ /** 尾行实际所在容器;缓存的 container 过期时按树找回来。 */
1351
+ const tailHostContainer = (tail: MountedRoundTail): any =>
1352
+ containerOfHost(tail.host) ??
1353
+ (tail.container?.children?.includes?.(tail.host) === true ? tail.container : undefined);
1354
+
1355
+ /** 摘尾行:按实际所在容器摘,避免缓存过期时摘了个空、行留在树上。 */
1356
+ const removeTailHost = (tail: MountedRoundTail): void => {
1357
+ tailHostContainer(tail)?.removeChild?.(tail.host);
1358
+ };
1359
+
1360
+ const ensureTailHost = (round: CompactRound): MountedRoundTail => {
1361
+ if (round.tail) return round.tail;
1362
+ // 同一 anchor 上残留的旧回合尾行(reset/重组后建新回合)先摘除,避免双摘要。
1363
+ const stale = tailByAnchor.get(round.anchor);
1364
+ if (stale) {
1365
+ removeTailHost(stale);
1366
+ mountedTails.delete(stale);
1367
+ tailByAnchor.delete(round.anchor);
1368
+ }
1369
+ const tail: MountedRoundTail = { host: undefined, container: undefined, parts: [], round };
1370
+ const host: any = {
1371
+ outputPad: Number(round.anchor?.outputPad) || 0,
1372
+ children: [],
1373
+ render: (width: number) => tail.parts.flatMap((part: any) => part?.render?.(width) ?? []),
1374
+ childAtRow(localRow: number, width: number) {
1375
+ let offset = 0;
1376
+ for (const part of tail.parts) {
1377
+ const lines = part?.render?.(width);
1378
+ const count = Array.isArray(lines) ? lines.length : 0;
1379
+ if (localRow < offset + count) {
1380
+ return part?.childAtRow?.(localRow - offset, width) ?? null;
1381
+ }
1382
+ offset += count;
1383
+ }
1384
+ return null;
1385
+ },
1386
+ invalidate: () => {
1387
+ for (const part of tail.parts) part?.invalidate?.();
1388
+ },
1389
+ };
1390
+ Object.defineProperty(host, "expanded", {
1391
+ configurable: true,
1392
+ get: () => tail.round?.anchor?.expanded === true,
1393
+ set: (value: boolean) => {
1394
+ const anchor = tail.round?.anchor;
1395
+ if (anchor) anchor.expanded = value;
1396
+ },
1397
+ });
1398
+ // 点击/悬停归属与 anchor 同一展开入口;toggle 每次 renderRound 重建,按值转发。
1399
+ // 经 tail.round 动态解引用:重建过户时 anchor 可能换成别的组件。
1400
+ ensureAssistantSetExpanded(host);
1401
+ Object.defineProperty(host, "roundAnchor", {
1402
+ configurable: true,
1403
+ get: () => tail.round?.anchor,
1404
+ });
1405
+ host[ASSISTANT_TOGGLE_ROUND_KEY] = (expanded: boolean) =>
1406
+ tail.round?.anchor?.[ASSISTANT_TOGGLE_ROUND_KEY]?.(expanded);
1407
+ tail.host = host;
1408
+ tailByAnchor.set(round.anchor, tail);
1409
+ mountedTails.add(tail);
1410
+ round.tail = tail;
1411
+ return tail;
1412
+ };
1413
+
1414
+ /**
1415
+ * 尾行归位。运行中(含收尾静默期)尾行恒为容器最末一行:计数行钉在
1416
+ * transcript 底缘,运行中到达的提示行/custom 条目只能落在它上方,计数
1417
+ * 不会被顶离底部截成两行。回合收尾后摘要行回落到回合最后一个成员之后,
1418
+ * 外来行统一换到摘要行之下,通知行永远落在回合边界外。
1419
+ */
1420
+ const syncTailPosition = (round: CompactRound): void => {
1421
+ const tail = round.tail;
1422
+ if (!tail?.host) return;
1423
+ const container =
1424
+ tail.container?.children?.includes?.(round.anchor) === true
1425
+ ? tail.container
1426
+ : tailContainerOf(round.anchor);
1427
+ if (!container || !Array.isArray(container.children)) return;
1428
+ tail.container = container;
1429
+ const children = container.children as any[];
1430
+ const current = children.indexOf(tail.host);
1431
+ if (current >= 0) children.splice(current, 1);
1432
+ const ids = roundToolCallIds(round);
1433
+ const matchesTool = (value: any): boolean =>
1434
+ typeof value?.toolCallId === "string" && ids.has(value.toolCallId);
1435
+ const isMember = (value: any): boolean =>
1436
+ round.messages.has(value) ||
1437
+ matchesTool(value) ||
1438
+ (Array.isArray(value?.children) && value.children.some(matchesTool));
1439
+ let first = -1;
1440
+ let last = -1;
1441
+ for (let i = 0; i < children.length; i++) {
1442
+ if (!isMember(children[i])) continue;
1443
+ if (first < 0) first = i;
1444
+ last = i;
1445
+ }
1446
+ const lastMember = last >= 0 ? children[last] : undefined;
1447
+ const intruders: any[] = [];
1448
+ for (let i = last - 1; i > first; i--) {
1449
+ if (!isMember(children[i])) intruders.unshift(children.splice(i, 1)[0]);
1450
+ }
1451
+ if (lastMember) last = children.indexOf(lastMember);
1452
+ const insertAt = last < 0 ? children.length : last + 1;
1453
+ if (roundLive(round)) {
1454
+ // 活回合:外来行压到最后成员之后,尾行占容器最末。
1455
+ children.splice(insertAt, 0, ...intruders);
1456
+ children.push(tail.host);
1457
+ } else {
1458
+ children.splice(insertAt, 0, tail.host, ...intruders);
1459
+ }
1460
+ tail.host[TOOL_GROUPING_PARENT_KEY] = container;
1461
+ };
1462
+
1463
+ const unmountRoundTail = (round: CompactRound): void => {
1464
+ const tail = round.tail;
1465
+ if (!tail) return;
1466
+ round.tail = undefined;
1467
+ removeTailHost(tail);
1468
+ mountedTails.delete(tail);
1469
+ if (tailByAnchor.get(round.anchor) === tail) tailByAnchor.delete(round.anchor);
1470
+ };
1471
+
1472
+ /**
1473
+ * pi 把新工具卡 append 在容器末尾(尾行之后):updateDisplay 时把所属回合的
1474
+ * 尾行压回末尾。只有 live/收尾静默期的回合还会长工具。
1475
+ */
1476
+ const syncTailForTool = (toolCallId: string): void => {
1477
+ if (!toolCallId) return;
1478
+ for (const round of [activeRound, ...pendingFoldRounds]) {
1479
+ if (round?.tail && roundToolCallIds(round).has(toolCallId)) {
1480
+ syncTailPosition(round);
1481
+ return;
1482
+ }
1483
+ }
1484
+ };
1485
+
1486
+ /**
1487
+ * refresh 重建时继承旧回合:resetRounds 只清索引不摘 compact 尾行,
1488
+ * 残留尾行带着旧 round。同一组件重放同一消息(lastMessage 回放)
1489
+ * 才算重建——保留挂钟起点、已脱离的工具历史与尾行组件本身;
1490
+ * 旧回合已收尾的直接落成折叠态(不回摆 Running、不重演静默期),
1491
+ * 仍活动的只接回挂钟,Running 时长继续走。
1492
+ */
1493
+ const adoptPriorRound = (round: CompactRound, message: any): void => {
1494
+ let prior: CompactRound | undefined;
1495
+ for (const tail of mountedTails) {
1496
+ const stale = tail.round as CompactRound | undefined;
1497
+ if (
1498
+ stale &&
1499
+ // 先认组件:重放时消息对象未必是同一个(会话重读/重建),
1500
+ // 只认消息身份会漏掉本该接回的回合,摘要行就此掉队。
1501
+ (stale.anchor === round.anchor ||
1502
+ stale.messages.has(round.anchor) ||
1503
+ stale.detachedMessages.includes(message))
1504
+ ) {
1505
+ prior = stale;
1506
+ break;
1507
+ }
1508
+ }
1509
+ if (!prior) return;
1510
+ round.startedAt = prior.startedAt;
1511
+ if (prior.detachedMessages.length) round.detachedMessages.push(...prior.detachedMessages);
1512
+ if (!prior.active) {
1513
+ round.active = false;
1514
+ round.endedAt = prior.endedAt ?? round.startedAt;
1515
+ }
1516
+ const tail = prior.tail;
1517
+ if (tail) {
1518
+ prior.tail = undefined;
1519
+ if (tailByAnchor.get(prior.anchor) === tail) tailByAnchor.delete(prior.anchor);
1520
+ tail.round = round;
1521
+ tailByAnchor.set(round.anchor, tail);
1522
+ round.tail = tail;
1523
+ }
1524
+ };
1525
+
1526
+ /** 新建活回合:literal + 激活 + refresh 重建时的旧回合收编。 */
1527
+ const createRound = (anchor: any, message: any): CompactRound => {
1528
+ const round: CompactRound = {
1529
+ anchor,
1530
+ messages: new Map(),
1531
+ detachedMessages: [],
1532
+ active: true,
1533
+ suppressedToolIds: new Set(),
1534
+ liveSlotToolIds: new Set(),
1535
+ startedAt: Date.now(),
1536
+ };
1537
+ activateRound(round);
1538
+ adoptPriorRound(round, message);
1539
+ return round;
1540
+ };
1541
+
1542
+ /**
1543
+ * 尾行挂载入口:makeParts 按渲染态产出内容(折叠态 [摘要行],live 态
1544
+ * [摘要行, 槽位卡]),identity 决定行内 hover/点击归属组件(尾行模式下是
1545
+ * host,hit-test 直达它)。anchor 未挂进任何容器时(独立渲染)退回
1546
+ * contentContainer——与旧行为一致。
1547
+ */
1548
+ const mountRoundTail = (round: CompactRound, makeParts: (identity: any) => any[]): void => {
1549
+ if (!tailContainerOf(round.anchor)) {
1550
+ // anchor 不在任何容器里(独立渲染/未挂载):退回 anchor 内部,与旧行为一致。
1551
+ unmountRoundTail(round);
1552
+ for (const part of makeParts(round.anchor)) {
1553
+ round.anchor?.contentContainer?.addChild?.(part);
1554
+ }
1555
+ return;
1556
+ }
1557
+ const tail = ensureTailHost(round);
1558
+ tail.parts = makeParts(tail.host);
1559
+ tail.host.children = tail.parts;
1560
+ syncTailPosition(round);
1561
+ };
1562
+
1563
+ /**
1564
+ * 末块仍是 thinking = 思考还在长;后面跟了正文或工具调用就算结束。
1565
+ * 围观态只预览这一种,已完成的思考直接回收进摘要行,不在屏幕上留 Thought 行。
1566
+ */
1567
+ const hasTrailingThinking = (message: any): boolean => {
1568
+ const content = Array.isArray(message?.content) ? message.content : [];
1569
+ for (let i = content.length - 1; i >= 0; i--) {
1570
+ const item = content[i];
1571
+ if (item?.type === "thinking") return true;
1572
+ if (item?.type === "toolCall") return false;
1573
+ if (item?.type === "text" && String(item.text ?? "").trim()) return false;
1574
+ }
1575
+ return false;
1576
+ };
1577
+
1578
+ /** 运行中的工具:已开始且尚无最终结果。 */
1579
+ const isRunningTool = (tool: any): boolean =>
1580
+ tool?.executionStarted === true && (!tool.result || tool.isPartial === true);
1581
+
1582
+ /**
1583
+ * live 围观态:正文 + 摘要行照折叠态渲染,只在摘要行下多一张槽位卡。
1584
+ * - 思考:只预览最后一条仍在思考的消息,它的块是当前最新内容;
1585
+ * - 工具:没有活动思考时才占槽位,运行中优先,否则取本回合最近的,完成也留着;
1586
+ * - 槽位只放一个块(思考或工具),新的思考块/工具调用进来才轮换;
1587
+ * - 本回合所有工具都由槽位卡接管,外层不再单独成行。
1588
+ * 可见性判断只在这里发生;回合结束(renderRound 非 active)只剩槽位卡消失。
1589
+ */
1590
+ const renderRoundLive = (
1591
+ round: CompactRound,
1592
+ stopStatus: string | undefined,
1593
+ getSummary: () => string,
1594
+ ): void => {
1595
+ // 仍在思考的消息 = 当前最新内容,优先占槽位。
1596
+ let previewComponent: any;
1597
+ for (const [component, message] of round.messages) {
1598
+ if (hasTrailingThinking(message)) previewComponent = component;
1599
+ }
1600
+
1601
+ const thinkingKids: any[] = [];
1602
+ for (const [component, message] of round.messages) {
1603
+ if (component !== previewComponent) {
1604
+ component.contentContainer?.clear?.();
1605
+ continue;
1606
+ }
1607
+ passThroughAssistant(component, message);
1608
+ const kids = Array.isArray(component.contentContainer?.children)
1609
+ ? [...component.contentContainer.children]
1610
+ : [];
1611
+ component.contentContainer?.clear?.();
1612
+ // 只摘思考预览块进槽位;已收尾的思考回收进摘要行。
1613
+ for (const kid of kids) {
1614
+ if (typeof kid?.setHintHovered === "function") thinkingKids.push(kid);
1615
+ }
1616
+ }
1617
+ // 正文照折叠态渲染(不含 thinking):live 与收尾后同形,收尾只剩槽位卡消失。
1618
+ renderAssistantWithoutThinking(round.anchor, round.messages.get(round.anchor));
1619
+
1620
+ const toolsById = new Map<string, any>();
1621
+ for (const tool of trackedToolComponents) {
1622
+ if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
1623
+ }
1624
+ let runningTool: any;
1625
+ let lastTool: any;
1626
+ for (const [, message] of round.messages) {
1627
+ for (const item of Array.isArray(message?.content) ? message.content : []) {
1628
+ if (item?.type !== "toolCall" || typeof item.id !== "string") continue;
1629
+ const tool = toolsById.get(item.id);
1630
+ // edit/write 保留自己的 compact 行,不进槽位。
1631
+ if (EDIT_WRITE_TOOLS.has(String(tool?.toolName ?? item.name ?? ""))) continue;
1632
+ round.liveSlotToolIds.add(item.id);
1633
+ liveSlotToolIds.add(item.id);
1634
+ if (!tool) continue;
1635
+ lastTool = tool;
1636
+ if (isRunningTool(tool)) runningTool = tool;
1637
+ }
1638
+ }
1639
+ // 槽位单块:活动思考最新;否则本回合最近的工具(运行中原位增长,完成也留着)。
1640
+ const slotTool = thinkingKids.length > 0 ? undefined : (runningTool ?? lastTool);
1641
+ const cardItems: Array<{ child?: any; tool?: any }> = slotTool
1642
+ ? [{ tool: slotTool }]
1643
+ : thinkingKids.map((child) => ({ child }));
1644
+
1645
+ const anchor = round.anchor;
1646
+ const pad = Number(anchor.outputPad) || 0;
1647
+ // 摘要行与槽位卡挂回合尾行(transcript 容器内回合末尾,而非 anchor 内部):
1648
+ // 运行中恒在可写视口底缘,收尾的 Ran for 原地落进 scrollback,不再留下
1649
+ // 被挤出视口后定格的 Running 帧。
1650
+ mountRoundTail(round, (identity) => {
1651
+ const parts: any[] = [compactAssistantLineComponent(identity, getSummary, deps.query)];
1652
+ if (cardItems.length > 0) {
1653
+ parts.push(
1654
+ compactLiveSlot(
1655
+ compactRoundCard(
1656
+ cardItems,
1657
+ (tool, innerWidth) => patch.toolOriginalRender.call(tool, innerWidth),
1658
+ false,
1659
+ true,
1660
+ ),
1661
+ pad,
1662
+ ),
1663
+ );
1664
+ }
1665
+ return parts;
1666
+ });
1667
+ appendStopStatus(anchor, stopStatus);
1668
+ };
1669
+
906
1670
  const renderRound = (round: CompactRound): void => {
907
1671
  const messages = roundMessages(round);
908
1672
  const stopStatus = roundStopStatus(messages);
909
1673
  if (stopStatus) endRound(round);
910
1674
  // Running 时每次 render 重算时长(含挂钟下限);结束后固定。
1675
+ // 静默期内回合已收尾,摘要行直接给 Ran for,只有槽位卡多留一拍。
911
1676
  const getSummary = () => summarize(roundMessages(round), round.active, round);
912
1677
  const summary = getSummary();
913
1678
  for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
914
1679
  round.suppressedToolIds.clear();
1680
+ // 回合收起(或本回合不在展开态)时忘掉用户的单开选择,并把面板接管的工具一起收回:
1681
+ // 少了后一步,被点开的工具会带着自身的 expanded 落到 toolInstalledRender 的放行分支,
1682
+ // 面板一收就单独渲染成一张卡。
1683
+ if (round.anchor.expanded !== true) {
1684
+ const ids = roundToolCallIds(round);
1685
+ for (const id of ids) explicitRoundToolIds.delete(id);
1686
+ collapseTools(
1687
+ (tool) =>
1688
+ ids.has(String(tool.toolCallId ?? "")) &&
1689
+ !EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")),
1690
+ );
1691
+ }
1692
+ for (const id of round.liveSlotToolIds) liveSlotToolIds.delete(id);
1693
+ round.liveSlotToolIds.clear();
915
1694
 
916
1695
  for (const [component, message] of round.messages) {
917
1696
  component.lastMessage = message;
918
1697
  ensureAssistantSetExpanded(component);
919
1698
  component[ASSISTANT_TOGGLE_ROUND_KEY] = (expanded: boolean) => {
1699
+ const wasExpanded = round.anchor.expanded === true;
1700
+ if (expanded && !wasExpanded) capturePanelViewport(round.anchor);
920
1701
  for (const member of round.messages.keys()) member.expanded = expanded;
921
1702
  if (!expanded) {
922
1703
  for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
923
1704
  round.suppressedToolIds.clear();
924
1705
  }
925
1706
  renderRound(round);
1707
+ if (!expanded && wasExpanded) restorePanelViewport(round.anchor);
926
1708
  };
927
1709
  }
928
1710
 
929
1711
  if (round.anchor.expanded === true) {
930
- const assistantChildren: any[] = [];
1712
+ // 展开内容留在摘要行那个位置:回合最后一个成员(含 write/edit)之后。
1713
+ // 放进 anchor 内部会跳到回合起点,盖到中间的 write/edit 上面。
1714
+ // anchor 未挂进 transcript 时没有这个位置,退回 anchor 内部。
1715
+ const mounted = Boolean(tailContainerOf(round.anchor));
1716
+ const toolsById = new Map<string, any>();
1717
+ for (const tool of trackedToolComponents) {
1718
+ if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
1719
+ }
1720
+ // 助手文本按原生渲染、不进面板(面板只留 thinking 与工具卡)。
1721
+ // thinking/工具卡按连续段各包一张面板;先出文本再出面板,
1722
+ // 否则消息内容里的 thinking([thinking, text, toolCall])会盖到文本上面。
1723
+ const anchorChildren: any[] = [];
1724
+ let cardItems: Array<{ child?: any; tool?: any }> = [];
1725
+ const flushPanel = () => {
1726
+ if (cardItems.length === 0) return;
1727
+ if (cardItems.every((item) => !item.tool && isSpacerComponent(item.child))) {
1728
+ for (const item of cardItems) anchorChildren.push(item.child);
1729
+ } else {
1730
+ // 面板上沿留 1 行卡外空行:面板底色与 userMessage 同色,
1731
+ // 贴在一起会看成一个块;卡内顶部再加 1 行 padding 保持上下对称。
1732
+ anchorChildren.push(new Spacer(1));
1733
+ anchorChildren.push(
1734
+ compactRoundCard(
1735
+ cardItems,
1736
+ (tool, innerWidth) => patch.toolOriginalRender.call(tool, innerWidth),
1737
+ // 整回合展开卡:卡内工具行归 tool 组件,可单击二次展开。
1738
+ true,
1739
+ ),
1740
+ );
1741
+ }
1742
+ cardItems = [];
1743
+ };
1744
+ const takeChild = (child: any, outside: boolean) => {
1745
+ if (!outside) {
1746
+ cardItems.push({ child });
1747
+ return;
1748
+ }
1749
+ flushPanel();
1750
+ anchorChildren.push(child);
1751
+ };
1752
+ const placedToolIds = new Set<string>();
1753
+ const placeTool = (id: string, fallbackName?: string) => {
1754
+ const tool = toolsById.get(id);
1755
+ if (!tool || EDIT_WRITE_TOOLS.has(String(tool.toolName ?? fallbackName ?? ""))) return;
1756
+ if (placedToolIds.has(id)) return;
1757
+ placedToolIds.add(id);
1758
+ cardItems.push({ tool });
1759
+ };
1760
+ type LocalEntry = { outside: boolean; child?: any; toolId?: string; toolName?: string };
931
1761
  for (const [component, message] of round.messages) {
932
1762
  passThroughAssistant(component, message);
933
- if (Array.isArray(component.contentContainer?.children)) {
934
- assistantChildren.push(...component.contentContainer.children);
935
- }
1763
+ const kids = Array.isArray(component.contentContainer?.children)
1764
+ ? [...component.contentContainer.children]
1765
+ : [];
936
1766
  component.contentContainer?.clear?.();
1767
+ const cursor = { i: 0 };
1768
+ let inThinkingRun = false;
1769
+ // 本消息产出先收集再发射:文本(含其前导 Spacer)统一排在面板前面。
1770
+ const local: LocalEntry[] = [];
1771
+ const takeRun = (outside: boolean) => {
1772
+ while (cursor.i < kids.length && isSpacerComponent(kids[cursor.i])) {
1773
+ local.push({ outside, child: kids[cursor.i++] });
1774
+ }
1775
+ if (cursor.i < kids.length) local.push({ outside, child: kids[cursor.i++] });
1776
+ };
1777
+ for (const item of Array.isArray(message?.content) ? message.content : []) {
1778
+ if (item?.type === "thinking") {
1779
+ if (!String(item.thinking ?? "").trim()) continue;
1780
+ if (!inThinkingRun) {
1781
+ takeRun(false);
1782
+ inThinkingRun = true;
1783
+ }
1784
+ continue;
1785
+ }
1786
+ inThinkingRun = false;
1787
+ if (item?.type === "text" && String(item.text ?? "").trim()) {
1788
+ takeRun(true);
1789
+ continue;
1790
+ }
1791
+ if (item?.type === "toolCall" && typeof item.id === "string") {
1792
+ local.push({ outside: false, toolId: item.id, toolName: item.name });
1793
+ }
1794
+ }
1795
+ while (cursor.i < kids.length) local.push({ outside: false, child: kids[cursor.i++] });
1796
+ for (const entry of local) if (entry.outside) takeChild(entry.child, true);
1797
+ for (const entry of local) {
1798
+ if (entry.outside) continue;
1799
+ if (entry.toolId) placeTool(entry.toolId, entry.toolName);
1800
+ else takeChild(entry.child, false);
1801
+ }
1802
+ }
1803
+ for (const message of round.detachedMessages) {
1804
+ for (const item of Array.isArray(message?.content) ? message.content : []) {
1805
+ if (item?.type === "toolCall" && typeof item.id === "string") {
1806
+ placeTool(item.id, item.name);
1807
+ }
1808
+ }
937
1809
  }
938
1810
  const ids = roundToolCallIds(round);
939
1811
  for (const id of ids) {
940
1812
  round.suppressedToolIds.add(id);
941
1813
  expandedRoundToolIds.add(id);
1814
+ if (!placedToolIds.has(id)) placeTool(id);
942
1815
  }
943
- // Round 展开只打开外层卡片。普通工具保持折叠,避免长输出递归撑满屏幕。
944
- for (const tool of trackedToolComponents) {
945
- if (
946
- !ids.has(tool.toolCallId) ||
947
- EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")) ||
948
- tool.expanded !== true
949
- ) {
950
- continue;
951
- }
952
- if (typeof tool.setExpanded === "function") tool.setExpanded(false);
953
- else {
954
- tool.expanded = false;
955
- tool.updateDisplay?.();
956
- }
957
- }
958
- const cardItems: Array<{ child?: any; tool?: any }> = assistantChildren.map((child) => ({
959
- child,
960
- }));
961
- const roundIds = roundToolCallIds(round);
962
- for (const tool of trackedToolComponents) {
963
- if (!roundIds.has(tool.toolCallId) || EDIT_WRITE_TOOLS.has(String(tool.toolName ?? ""))) {
964
- continue;
1816
+ // Round 展开只打开外层卡片。普通工具保持折叠,避免长输出递归撑满屏幕;
1817
+ // 用户单独点开的(explicitRoundToolIds)不压回去。
1818
+ collapseTools(
1819
+ (tool) =>
1820
+ ids.has(String(tool.toolCallId ?? "")) &&
1821
+ !EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")) &&
1822
+ !explicitRoundToolIds.has(String(tool.toolCallId ?? "")),
1823
+ );
1824
+ flushPanel();
1825
+ if (mounted) {
1826
+ // 正文留在各自的 assistant 消息上,面板挂回摘要行原位。
1827
+ const outside: any[] = [];
1828
+ const panel: any[] = [];
1829
+ for (const child of anchorChildren) {
1830
+ (typeof child?.childAtRow === "function" ? panel : outside).push(child);
965
1831
  }
966
- cardItems.push({ tool });
1832
+ for (const child of outside) round.anchor.contentContainer.addChild(child);
1833
+ if (panel.length > 0) mountRoundTail(round, () => panel);
1834
+ else unmountRoundTail(round);
1835
+ } else {
1836
+ unmountRoundTail(round);
1837
+ for (const child of anchorChildren) round.anchor.contentContainer.addChild(child);
967
1838
  }
968
- round.anchor.contentContainer.addChild(
969
- compactRoundCard(cardItems, (tool, innerWidth) =>
970
- patch.toolOriginalRender.call(tool, innerWidth),
971
- ),
972
- );
973
1839
  // 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
974
1840
  appendStopStatus(round.anchor, stopStatus);
975
1841
  return;
976
1842
  }
977
1843
 
1844
+ if (roundLive(round)) {
1845
+ // 围观态:单一槽位卡(活动思考预览 + 当前工具),回合结束自动收回。
1846
+ renderRoundLive(round, stopStatus, getSummary);
1847
+ return;
1848
+ }
1849
+
978
1850
  for (const [component, message] of round.messages) {
979
1851
  if (component === round.anchor) {
980
1852
  renderAssistantWithoutThinking(component, message);
981
1853
  // 空摘要不挂行(getter 在 Running 启动瞬间也可能短暂为空)
982
- if (summary || round.active) compactAssistantLine(component, getSummary, deps.query);
1854
+ if (summary || round.active) {
1855
+ mountRoundTail(round, (identity) => [
1856
+ compactAssistantLineComponent(identity, getSummary, deps.query),
1857
+ ]);
1858
+ } else {
1859
+ unmountRoundTail(round);
1860
+ }
983
1861
  // 折叠时工具行被隐藏:abort/error/length 必须挂在摘要外层。
984
1862
  appendStopStatus(component, stopStatus);
985
1863
  } else {
@@ -989,6 +1867,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
989
1867
  };
990
1868
 
991
1869
  const activateRound = (round: CompactRound): void => {
1870
+ flushSettle();
992
1871
  round.active = true;
993
1872
  if (!round.startedAt) round.startedAt = Date.now();
994
1873
  delete round.endedAt;
@@ -1000,15 +1879,50 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1000
1879
  const finishRound = (round: CompactRound): void => endRound(round, true);
1001
1880
 
1002
1881
  const resetRounds = (): void => {
1882
+ for (const round of [...pendingFoldRounds]) clearFoldTimer(round);
1003
1883
  activeRound = undefined;
1004
1884
  roundByComponent = new WeakMap();
1005
1885
  expandedRoundToolIds.clear();
1886
+ explicitRoundToolIds.clear();
1887
+ liveSlotToolIds.clear();
1888
+ // 尾行是 transcript 兄弟组件:compact 下保留为静态行(与原 anchor 内摘要
1889
+ // 一致;同 anchor 重建回合时 ensureTailHost 去重摘除),非 compact 摘掉,
1890
+ // 别把紧凑摘要带进 default 渲染。
1891
+ for (const tail of [...mountedTails]) {
1892
+ const container = tailHostContainer(tail);
1893
+ if (!container) {
1894
+ mountedTails.delete(tail);
1895
+ continue;
1896
+ }
1897
+ tail.container = container;
1898
+ if (config.mode === "compact") continue;
1899
+ container.removeChild(tail.host);
1900
+ mountedTails.delete(tail);
1901
+ if (tail.round?.tail === tail) tail.round.tail = undefined;
1902
+ if (tail.round?.anchor && tailByAnchor.get(tail.round.anchor) === tail) {
1903
+ tailByAnchor.delete(tail.round.anchor);
1904
+ }
1905
+ }
1006
1906
  deps.query?.setCompactSummaryActive?.(false);
1007
1907
  stopRoundTick();
1008
1908
  };
1009
1909
 
1910
+ /**
1911
+ * 兜底:resetRounds 会丢掉回合索引但保留已挂的尾行,重放没能接回的回合会一直
1912
+ * active——尾行永远显示 Running...(时长还在涨)却不再计入新工具。既不是活动
1913
+ * 回合也不在收尾静默期的活回合就地收尾,不让它永远挂在屏幕上。
1914
+ */
1915
+ const settleStrandedRounds = (): void => {
1916
+ for (const tail of [...mountedTails]) {
1917
+ const round = tail.round as CompactRound | undefined;
1918
+ if (!round?.active || round === activeRound || pendingFoldRounds.has(round)) continue;
1919
+ endRound(round, true);
1920
+ }
1921
+ };
1922
+
1010
1923
  patch.assistantInstalled = function (this: any, message: any, isStreaming?: boolean) {
1011
1924
  const self = this as any;
1925
+ // 同 compact-thinking:isStreaming 丢失 → mermaid 流式误渲染来回闪。
1012
1926
  if (isStreaming !== undefined) self.isStreaming = isStreaming;
1013
1927
  if (self[ASSISTANT_REENTRY_KEY] === patch) {
1014
1928
  return patch.assistantNative.call(this, message, isStreaming);
@@ -1034,27 +1948,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1034
1948
  if (hasText && (!round || round.anchor !== this)) {
1035
1949
  if (round) round.messages.delete(this);
1036
1950
  if (activeRound) finishRound(activeRound);
1037
- round = {
1038
- anchor: this,
1039
- messages: new Map(),
1040
- detachedMessages: [],
1041
- active: true,
1042
- suppressedToolIds: new Set(),
1043
- startedAt: Date.now(),
1044
- };
1951
+ round = createRound(this, message);
1045
1952
  roundByComponent.set(this, round);
1046
- activateRound(round);
1047
1953
  } else if (!round) {
1048
- round = activeRound ?? {
1049
- anchor: this,
1050
- messages: new Map(),
1051
- detachedMessages: [],
1052
- active: true,
1053
- suppressedToolIds: new Set(),
1054
- startedAt: Date.now(),
1055
- };
1954
+ round = activeRound ?? createRound(this, message);
1056
1955
  roundByComponent.set(this, round);
1057
- if (!activeRound) activateRound(round);
1058
1956
  }
1059
1957
  round.messages.set(this, message);
1060
1958
  renderRound(round);
@@ -1074,7 +1972,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1074
1972
  return renderAssistantWithoutThinking(this, message);
1075
1973
  }
1076
1974
  if (round) {
1077
- endRound(round);
1975
+ // 围观态下 anchor 还挂着槽位卡,必须重绘才能收回。
1976
+ endRound(round, true);
1078
1977
  roundByComponent.delete(this);
1079
1978
  return passThroughAssistant(this, message);
1080
1979
  }
@@ -1086,16 +1985,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1086
1985
  if (hasThinking) {
1087
1986
  let round = roundByComponent.get(this);
1088
1987
  if (!round) {
1089
- round = activeRound ?? {
1090
- anchor: this,
1091
- messages: new Map(),
1092
- detachedMessages: [],
1093
- active: true,
1094
- suppressedToolIds: new Set(),
1095
- startedAt: Date.now(),
1096
- };
1988
+ round = activeRound ?? createRound(this, message);
1097
1989
  roundByComponent.set(this, round);
1098
- if (!activeRound) activateRound(round);
1099
1990
  }
1100
1991
  round.messages.set(this, message);
1101
1992
  renderRound(round);
@@ -1123,12 +2014,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1123
2014
  if (EDIT_WRITE_TOOLS.has(name)) {
1124
2015
  if (this.executionStarted && (!this.result || this.isPartial === true))
1125
2016
  scheduleAnimation(this);
1126
- const lines = compactEditWriteLine(this, width, deps.writeMetadata);
1127
- if (this.expanded !== true) return lines;
1128
- return compactEditWriteExpandedLines(this, width, deps.writeMetadata);
2017
+ return compactEditWriteLines(this, width, deps.writeMetadata);
1129
2018
  }
1130
2019
  // Agent/Task 等同普通工具:折叠不外置(live 面板走独立 widget)。
1131
2020
  if (expandedRoundToolIds.has(String(this.toolCallId ?? ""))) return [];
2021
+ // live 围观态:本回合工具交由槽位卡内联渲染,外层不再单独成行,
2022
+ // 卡片因此不会随工具完成反复挂载/卸载。
2023
+ if (liveSlotToolIds.has(String(this.toolCallId ?? ""))) return [];
1132
2024
  // 普通工具折叠时不显示独立行(摘要行已统计),独立展开走原 renderer。
1133
2025
  if (this.expanded === true) {
1134
2026
  return patch.toolOriginalRender.call(this, width);
@@ -1137,16 +2029,16 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1137
2029
  };
1138
2030
 
1139
2031
  patch.toolInstalledUpdateDisplay = function (this: any) {
1140
- if (
1141
- patch.active &&
1142
- config.mode === "compact" &&
1143
- expandedRoundToolIds.has(String(this.toolCallId ?? ""))
1144
- ) {
1145
- this.expanded = false;
2032
+ const id = String(this.toolCallId ?? "");
2033
+ if (patch.active && config.mode === "compact" && expandedRoundToolIds.has(id)) {
2034
+ // 用户点开的保留展开;其余(含全局展开被收回的)继续强制折叠。
2035
+ if (this.expanded !== true) explicitRoundToolIds.delete(id);
2036
+ else if (!explicitRoundToolIds.has(id)) this.expanded = false;
1146
2037
  }
1147
2038
  const result = patch.toolOriginalUpdateDisplay.call(this);
1148
2039
  if (!patch.active) return result;
1149
2040
  trackedToolComponents.add(this);
2041
+ if (config.mode === "compact") syncTailForTool(id);
1150
2042
  return result;
1151
2043
  };
1152
2044
 
@@ -1170,6 +2062,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1170
2062
  patch.dispose = () => {
1171
2063
  if (!patch.active) return;
1172
2064
  patch.active = false;
2065
+ if (roundToolExpansion?.markUserExpanded === markRoundToolUserExpanded) {
2066
+ roundToolExpansion = undefined;
2067
+ }
1173
2068
  if (assistantPrototype.updateContent === patch.assistantInstalled) {
1174
2069
  assistantPrototype.updateContent = patch.assistantOriginal;
1175
2070
  }
@@ -1179,18 +2074,19 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
1179
2074
  if (toolPrototype.updateDisplay === patch.toolInstalledUpdateDisplay) {
1180
2075
  toolPrototype.updateDisplay = patch.toolOriginalUpdateDisplay;
1181
2076
  }
1182
- if (host[COMPACT_MODE_PATCH_KEY] === patch) delete host[COMPACT_MODE_PATCH_KEY];
2077
+ patchRegistry.dispose(COMPACT_MODE_PATCH_KEY, patch);
1183
2078
  for (const component of trackedAssistantComponents) detachAssistantExpansion(component);
1184
2079
  trackedAssistantComponents.clear();
1185
2080
  trackedToolComponents.clear();
1186
2081
  hoveredAssistantComponent = undefined;
1187
2082
  resetRounds();
2083
+ compactRunStatusGetter = undefined;
1188
2084
  };
1189
2085
 
1190
2086
  assistantPrototype.updateContent = patch.assistantInstalled;
1191
2087
  toolPrototype.render = patch.toolInstalledRender;
1192
2088
  toolPrototype.updateDisplay = patch.toolInstalledUpdateDisplay;
1193
- host[COMPACT_MODE_PATCH_KEY] = patch;
2089
+ patchRegistry.install(COMPACT_MODE_PATCH_KEY, patch);
1194
2090
 
1195
2091
  const syncGlobalExpanded = (ctx: any): void => {
1196
2092
  let globalExpanded = false;