@deepseek-harness-tui/dsh-tui 0.8.8 → 0.9.1

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 (467) hide show
  1. package/README.md +60 -238
  2. package/bin/dsh-tui.js +172 -5
  3. package/cordis.patch.yml +16 -4
  4. package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -13
  5. package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -59
  6. package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -15
  7. package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -32
  8. package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -59
  9. package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -166
  10. package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -42
  11. package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -143
  12. package/dsh-ecosystem-spec/registry/README.md +14 -14
  13. package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -56
  14. package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -13
  15. package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -149
  16. package/lib/types/cc/hyperlink.d.ts +7 -1
  17. package/lib/types/cc/hyperlink.d.ts.map +1 -1
  18. package/lib/types/cc/hyperlink.js +3 -2
  19. package/lib/types/cc/markdown.d.ts.map +1 -1
  20. package/lib/types/cc/markdown.js +33 -3
  21. package/lib/types/cc/sessionColors.d.ts +27 -0
  22. package/lib/types/cc/sessionColors.d.ts.map +1 -0
  23. package/lib/types/cc/sessionColors.js +39 -0
  24. package/lib/types/commands.d.ts.map +1 -1
  25. package/lib/types/commands.js +10 -2
  26. package/lib/types/components/ActivityPicker.d.ts +4 -1
  27. package/lib/types/components/ActivityPicker.d.ts.map +1 -1
  28. package/lib/types/components/ActivityPicker.js +2 -2
  29. package/lib/types/components/AutoRecapRow.d.ts +23 -0
  30. package/lib/types/components/AutoRecapRow.d.ts.map +1 -0
  31. package/lib/types/components/AutoRecapRow.js +23 -0
  32. package/lib/types/components/BtwPanel.d.ts.map +1 -1
  33. package/lib/types/components/BtwPanel.js +1 -1
  34. package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
  35. package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
  36. package/lib/types/components/Chat/SubagentMessage.js +11 -3
  37. package/lib/types/components/ColorPicker.d.ts +17 -0
  38. package/lib/types/components/ColorPicker.d.ts.map +1 -0
  39. package/lib/types/components/ColorPicker.js +20 -0
  40. package/lib/types/components/CommandSuggestions.d.ts +23 -4
  41. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  42. package/lib/types/components/CommandSuggestions.js +41 -14
  43. package/lib/types/components/ContextBarView.d.ts +33 -0
  44. package/lib/types/components/ContextBarView.d.ts.map +1 -0
  45. package/lib/types/components/ContextBarView.js +46 -0
  46. package/lib/types/components/EffortInputBorder.d.ts +12 -1
  47. package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
  48. package/lib/types/components/EffortInputBorder.js +22 -5
  49. package/lib/types/components/EffortSlider.d.ts +4 -1
  50. package/lib/types/components/EffortSlider.d.ts.map +1 -1
  51. package/lib/types/components/EffortSlider.js +4 -2
  52. package/lib/types/components/ExtensionDialog.js +6 -2
  53. package/lib/types/components/FileActionsPanel.d.ts +22 -0
  54. package/lib/types/components/FileActionsPanel.d.ts.map +1 -0
  55. package/lib/types/components/FileActionsPanel.js +28 -0
  56. package/lib/types/components/FileSuggestions.d.ts +26 -7
  57. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  58. package/lib/types/components/FileSuggestions.js +39 -12
  59. package/lib/types/components/GoalTodoPanel.d.ts +26 -2
  60. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  61. package/lib/types/components/GoalTodoPanel.js +86 -17
  62. package/lib/types/components/HelpMenu.d.ts +7 -1
  63. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  64. package/lib/types/components/HelpMenu.js +12 -4
  65. package/lib/types/components/HistorySearchDialog.d.ts +4 -1
  66. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  67. package/lib/types/components/HistorySearchDialog.js +2 -2
  68. package/lib/types/components/LangPicker.d.ts +14 -0
  69. package/lib/types/components/LangPicker.d.ts.map +1 -0
  70. package/lib/types/components/LangPicker.js +17 -0
  71. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  72. package/lib/types/components/LoadedContextPanel.js +3 -1
  73. package/lib/types/components/Markdown.d.ts.map +1 -1
  74. package/lib/types/components/Markdown.js +12 -3
  75. package/lib/types/components/MessageList.d.ts +45 -2
  76. package/lib/types/components/MessageList.d.ts.map +1 -1
  77. package/lib/types/components/MessageList.js +560 -41
  78. package/lib/types/components/ModelPicker.d.ts +27 -2
  79. package/lib/types/components/ModelPicker.d.ts.map +1 -1
  80. package/lib/types/components/ModelPicker.js +26 -6
  81. package/lib/types/components/PermissionsPicker.d.ts +21 -0
  82. package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
  83. package/lib/types/components/PermissionsPicker.js +27 -0
  84. package/lib/types/components/PlanPicker.d.ts +15 -0
  85. package/lib/types/components/PlanPicker.d.ts.map +1 -0
  86. package/lib/types/components/PlanPicker.js +18 -0
  87. package/lib/types/components/PresetPicker.d.ts +4 -1
  88. package/lib/types/components/PresetPicker.d.ts.map +1 -1
  89. package/lib/types/components/PresetPicker.js +2 -2
  90. package/lib/types/components/PromptInput.d.ts +4 -3
  91. package/lib/types/components/PromptInput.d.ts.map +1 -1
  92. package/lib/types/components/PromptInput.js +602 -75
  93. package/lib/types/components/RecapPanel.d.ts +25 -0
  94. package/lib/types/components/RecapPanel.d.ts.map +1 -0
  95. package/lib/types/components/RecapPanel.js +55 -0
  96. package/lib/types/components/RewindPicker.d.ts +12 -1
  97. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  98. package/lib/types/components/RewindPicker.js +6 -4
  99. package/lib/types/components/ScrollbarGutter.d.ts +28 -0
  100. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
  101. package/lib/types/components/ScrollbarGutter.js +119 -0
  102. package/lib/types/components/Select.d.ts +8 -1
  103. package/lib/types/components/Select.d.ts.map +1 -1
  104. package/lib/types/components/Select.js +4 -2
  105. package/lib/types/components/SkillsPicker.d.ts +4 -1
  106. package/lib/types/components/SkillsPicker.d.ts.map +1 -1
  107. package/lib/types/components/SkillsPicker.js +2 -2
  108. package/lib/types/components/SubagentCard.d.ts +2 -1
  109. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  110. package/lib/types/components/SubagentCard.js +9 -2
  111. package/lib/types/components/SubagentDashboard.d.ts +4 -0
  112. package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
  113. package/lib/types/components/SubagentDashboard.js +9 -1
  114. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  115. package/lib/types/components/SubagentDetailScene.js +5 -4
  116. package/lib/types/components/SuggestionCard.d.ts +65 -0
  117. package/lib/types/components/SuggestionCard.d.ts.map +1 -0
  118. package/lib/types/components/SuggestionCard.js +77 -0
  119. package/lib/types/components/ThemePicker.d.ts +4 -1
  120. package/lib/types/components/ThemePicker.d.ts.map +1 -1
  121. package/lib/types/components/ThemePicker.js +2 -2
  122. package/lib/types/components/ThinkingToggle.d.ts +4 -1
  123. package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
  124. package/lib/types/components/ThinkingToggle.js +2 -2
  125. package/lib/types/components/TimelineRail.d.ts +53 -0
  126. package/lib/types/components/TimelineRail.d.ts.map +1 -0
  127. package/lib/types/components/TimelineRail.js +197 -0
  128. package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
  129. package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
  130. package/lib/types/components/WorkspaceFlowPicker.js +4 -2
  131. package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
  132. package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
  133. package/lib/types/components/WorkspaceMenuPicker.js +19 -0
  134. package/lib/types/components/WorkspacePicker.d.ts +4 -1
  135. package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
  136. package/lib/types/components/WorkspacePicker.js +2 -2
  137. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  138. package/lib/types/components/approvals/ApprovalPanel.js +5 -1
  139. package/lib/types/components/design-system/ListItem.d.ts +8 -1
  140. package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
  141. package/lib/types/components/design-system/ListItem.js +8 -3
  142. package/lib/types/components/design-system/ThemeProvider.d.ts +7 -0
  143. package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
  144. package/lib/types/components/design-system/ThemeProvider.js +2 -1
  145. package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
  146. package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
  147. package/lib/types/components/messages/AssistantTextMessage.js +6 -2
  148. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
  149. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  150. package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
  151. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +15 -1
  152. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  153. package/lib/types/components/messages/AssistantToolUseMessage.js +37 -7
  154. package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
  155. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  156. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +7 -3
  157. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  158. package/lib/types/components/questions/AskUserQuestionPanel.js +75 -11
  159. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  160. package/lib/types/components/questions/PlanReviewPanel.js +13 -2
  161. package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
  162. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  163. package/lib/types/components/sessions/SessionListRow.js +4 -2
  164. package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
  165. package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
  166. package/lib/types/components/trajectory/HotspotView.js +8 -2
  167. package/lib/types/components/trajectory/Ledger.d.ts +9 -1
  168. package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
  169. package/lib/types/components/trajectory/Ledger.js +23 -5
  170. package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
  171. package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
  172. package/lib/types/components/trajectory/WaveBand.js +7 -2
  173. package/lib/types/dsh-adapter/channel.d.ts +117 -7
  174. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  175. package/lib/types/dsh-adapter/channel.js +1301 -122
  176. package/lib/types/dsh-adapter/compat/index.d.ts +8 -6
  177. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  178. package/lib/types/dsh-adapter/compat/index.js +8 -6
  179. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +150 -0
  180. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  181. package/lib/types/dsh-adapter/compat/sessionLog.js +518 -41
  182. package/lib/types/dsh-adapter/contract.d.ts +1 -1
  183. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  184. package/lib/types/dsh-adapter/contract.js +1 -0
  185. package/lib/types/dsh-adapter/extension-events.d.ts +1 -1
  186. package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
  187. package/lib/types/dsh-adapter/index.d.ts +20 -2
  188. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  189. package/lib/types/dsh-adapter/index.js +9 -1
  190. package/lib/types/dsh-adapter/plugin.d.ts +0 -9
  191. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  192. package/lib/types/dsh-adapter/plugin.js +453 -14
  193. package/lib/types/dsh-adapter/presets.d.ts +18 -0
  194. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  195. package/lib/types/dsh-adapter/presets.js +31 -0
  196. package/lib/types/dsh-adapter/providerWizard.d.ts +29 -1
  197. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  198. package/lib/types/dsh-adapter/providerWizard.js +89 -5
  199. package/lib/types/dsh-adapter/questions.d.ts +19 -2
  200. package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
  201. package/lib/types/dsh-adapter/questions.js +53 -10
  202. package/lib/types/dsh-adapter/recap.d.ts +49 -0
  203. package/lib/types/dsh-adapter/recap.d.ts.map +1 -0
  204. package/lib/types/dsh-adapter/recap.js +121 -0
  205. package/lib/types/dsh-adapter/sessionTree.d.ts +314 -0
  206. package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -0
  207. package/lib/types/dsh-adapter/sessionTree.js +954 -0
  208. package/lib/types/dsh-adapter/shortcuts.d.ts +15 -44
  209. package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
  210. package/lib/types/dsh-adapter/shortcuts.js +28 -165
  211. package/lib/types/dsh-adapter/types.d.ts +1 -1
  212. package/lib/types/dsh-adapter/types.d.ts.map +1 -1
  213. package/lib/types/herdr.js +1 -1
  214. package/lib/types/i18n.d.ts +770 -181
  215. package/lib/types/i18n.d.ts.map +1 -1
  216. package/lib/types/i18n.js +257 -80
  217. package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
  218. package/lib/types/ink/components/AlternateScreen.js +13 -1
  219. package/lib/types/ink/components/App.d.ts +12 -1
  220. package/lib/types/ink/components/App.d.ts.map +1 -1
  221. package/lib/types/ink/components/App.js +101 -4
  222. package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
  223. package/lib/types/ink/components/ClockContext.js +2 -0
  224. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  225. package/lib/types/ink/components/ScrollBox.js +170 -91
  226. package/lib/types/ink/constants.d.ts +9 -0
  227. package/lib/types/ink/constants.d.ts.map +1 -1
  228. package/lib/types/ink/constants.js +9 -0
  229. package/lib/types/ink/events/click-event.d.ts +7 -16
  230. package/lib/types/ink/events/click-event.d.ts.map +1 -1
  231. package/lib/types/ink/events/click-event.js +16 -19
  232. package/lib/types/ink/events/event-handlers.d.ts +11 -1
  233. package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
  234. package/lib/types/ink/events/event-handlers.js +2 -0
  235. package/lib/types/ink/events/input-event.d.ts +9 -0
  236. package/lib/types/ink/events/input-event.d.ts.map +1 -1
  237. package/lib/types/ink/events/input-event.js +4 -0
  238. package/lib/types/ink/events/pointer-event.d.ts +61 -0
  239. package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
  240. package/lib/types/ink/events/pointer-event.js +61 -0
  241. package/lib/types/ink/events/wheel-event.d.ts +17 -0
  242. package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
  243. package/lib/types/ink/events/wheel-event.js +20 -0
  244. package/lib/types/ink/geometry-trace.d.ts +51 -0
  245. package/lib/types/ink/geometry-trace.d.ts.map +1 -0
  246. package/lib/types/ink/geometry-trace.js +83 -0
  247. package/lib/types/ink/hit-test.d.ts +56 -1
  248. package/lib/types/ink/hit-test.d.ts.map +1 -1
  249. package/lib/types/ink/hit-test.js +146 -8
  250. package/lib/types/ink/ink.d.ts +58 -1
  251. package/lib/types/ink/ink.d.ts.map +1 -1
  252. package/lib/types/ink/ink.js +230 -29
  253. package/lib/types/ink/log-update.d.ts.map +1 -1
  254. package/lib/types/ink/log-update.js +21 -0
  255. package/lib/types/ink/node-cache.d.ts +5 -0
  256. package/lib/types/ink/node-cache.d.ts.map +1 -1
  257. package/lib/types/ink/parse-keypress.d.ts +8 -0
  258. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  259. package/lib/types/ink/parse-keypress.js +101 -12
  260. package/lib/types/ink/render-node-to-output.d.ts +10 -0
  261. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  262. package/lib/types/ink/render-node-to-output.js +111 -18
  263. package/lib/types/ink/timeline-rail.d.ts +138 -0
  264. package/lib/types/ink/timeline-rail.d.ts.map +1 -0
  265. package/lib/types/ink/timeline-rail.js +179 -0
  266. package/lib/types/ink/wrap-text.d.ts.map +1 -1
  267. package/lib/types/ink/wrap-text.js +47 -5
  268. package/lib/types/migrationPrefs.d.ts +60 -0
  269. package/lib/types/migrationPrefs.d.ts.map +1 -0
  270. package/lib/types/migrationPrefs.js +97 -0
  271. package/lib/types/modelGroups.d.ts +72 -0
  272. package/lib/types/modelGroups.d.ts.map +1 -0
  273. package/lib/types/modelGroups.js +97 -0
  274. package/lib/types/modelRecents.d.ts +39 -0
  275. package/lib/types/modelRecents.d.ts.map +1 -0
  276. package/lib/types/modelRecents.js +97 -0
  277. package/lib/types/native-ts/yoga-layout/index.d.ts +11 -0
  278. package/lib/types/native-ts/yoga-layout/index.d.ts.map +1 -1
  279. package/lib/types/native-ts/yoga-layout/index.js +31 -1
  280. package/lib/types/oauth.d.ts +15 -0
  281. package/lib/types/oauth.d.ts.map +1 -0
  282. package/lib/types/oauth.js +14 -0
  283. package/lib/types/reload.d.ts +109 -0
  284. package/lib/types/reload.d.ts.map +1 -0
  285. package/lib/types/reload.js +118 -0
  286. package/lib/types/screens/Chat.d.ts +3 -1
  287. package/lib/types/screens/Chat.d.ts.map +1 -1
  288. package/lib/types/screens/Chat.js +1377 -371
  289. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  290. package/lib/types/screens/SessionBrowser.js +56 -29
  291. package/lib/types/screens/SessionTree.d.ts +27 -0
  292. package/lib/types/screens/SessionTree.d.ts.map +1 -0
  293. package/lib/types/screens/SessionTree.js +555 -0
  294. package/lib/types/screens/Settings.d.ts.map +1 -1
  295. package/lib/types/screens/Settings.js +64 -22
  296. package/lib/types/screens/StatusLine.d.ts +0 -8
  297. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  298. package/lib/types/screens/StatusLine.js +224 -45
  299. package/lib/types/screens/StatusMetrics.d.ts +35 -3
  300. package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
  301. package/lib/types/screens/StatusMetrics.js +63 -8
  302. package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
  303. package/lib/types/screens/TrajectoryScene.js +81 -14
  304. package/lib/types/screens/chatOverlay.d.ts +237 -0
  305. package/lib/types/screens/chatOverlay.d.ts.map +1 -0
  306. package/lib/types/screens/chatOverlay.js +116 -0
  307. package/lib/types/tips.d.ts.map +1 -1
  308. package/lib/types/tips.js +64 -10
  309. package/lib/types/tuiDisplayPrefs.d.ts +15 -0
  310. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  311. package/lib/types/tuiDisplayPrefs.js +9 -0
  312. package/lib/types/update.d.ts +139 -0
  313. package/lib/types/update.d.ts.map +1 -1
  314. package/lib/types/update.js +382 -30
  315. package/lib/types/utils/debug.d.ts +10 -0
  316. package/lib/types/utils/debug.d.ts.map +1 -1
  317. package/lib/types/utils/debug.js +24 -0
  318. package/lib/types/utils/fileTarget.d.ts +46 -0
  319. package/lib/types/utils/fileTarget.d.ts.map +1 -0
  320. package/lib/types/utils/fileTarget.js +178 -0
  321. package/lib/types/utils/keymap.d.ts +134 -0
  322. package/lib/types/utils/keymap.d.ts.map +1 -0
  323. package/lib/types/utils/keymap.js +311 -0
  324. package/lib/types/utils/mentions.d.ts +25 -2
  325. package/lib/types/utils/mentions.d.ts.map +1 -1
  326. package/lib/types/utils/mentions.js +69 -10
  327. package/lib/types/utils/openExternal.d.ts +50 -0
  328. package/lib/types/utils/openExternal.d.ts.map +1 -0
  329. package/lib/types/utils/openExternal.js +170 -0
  330. package/lib/types/utils/paths.d.ts.map +1 -1
  331. package/lib/types/utils/paths.js +1 -0
  332. package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -0
  333. package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +140 -0
  334. package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +11 -0
  335. package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -0
  336. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.d.ts +14 -0
  337. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.js +67 -0
  338. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +64 -0
  339. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.js +170 -0
  340. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +79 -0
  341. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +196 -0
  342. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +68 -0
  343. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +193 -0
  344. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.d.ts +32 -0
  345. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.js +101 -0
  346. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +31 -0
  347. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +68 -0
  348. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.d.ts +69 -0
  349. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.js +129 -0
  350. package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +70 -0
  351. package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
  352. package/node_modules/@dsh-std/command/LICENSE +21 -21
  353. package/node_modules/@dsh-std/command/README.md +26 -26
  354. package/node_modules/@dsh-std/command/README.zh.md +28 -28
  355. package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
  356. package/node_modules/@dsh-std/command/package.json +22 -22
  357. package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  358. package/node_modules/@dsh-std/connection/LICENSE +21 -21
  359. package/node_modules/@dsh-std/connection/README.md +42 -42
  360. package/node_modules/@dsh-std/connection/README.zh.md +42 -42
  361. package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  362. package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  363. package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  364. package/node_modules/@dsh-std/connection/package.json +20 -20
  365. package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  366. package/node_modules/@dsh-std/core/LICENSE +21 -21
  367. package/node_modules/@dsh-std/core/README.md +13 -13
  368. package/node_modules/@dsh-std/core/README.zh.md +13 -13
  369. package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  370. package/node_modules/@dsh-std/core/package.json +21 -21
  371. package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
  372. package/node_modules/@dsh-std/manifest/LICENSE +21 -21
  373. package/node_modules/@dsh-std/manifest/README.md +5 -5
  374. package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
  375. package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
  376. package/node_modules/@dsh-std/manifest/package.json +20 -20
  377. package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
  378. package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
  379. package/node_modules/@dsh-std/messages/LICENSE +21 -21
  380. package/node_modules/@dsh-std/messages/README.md +5 -5
  381. package/node_modules/@dsh-std/messages/README.zh.md +7 -7
  382. package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
  383. package/node_modules/@dsh-std/messages/package.json +20 -20
  384. package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
  385. package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
  386. package/node_modules/@dsh-std/presentation/LICENSE +21 -21
  387. package/node_modules/@dsh-std/presentation/README.md +27 -27
  388. package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
  389. package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
  390. package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
  391. package/node_modules/@dsh-std/presentation/package.json +25 -25
  392. package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
  393. package/node_modules/@dsh-std/storage/LICENSE +21 -21
  394. package/node_modules/@dsh-std/storage/README.md +5 -5
  395. package/node_modules/@dsh-std/storage/README.zh.md +7 -7
  396. package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
  397. package/node_modules/@dsh-std/storage/package.json +20 -20
  398. package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
  399. package/package.json +19 -5
  400. package/presets/liangshen/.dsh-tui-managed.json +1 -1
  401. package/presets/liangshen/instruction-hint.mjs +8 -0
  402. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  403. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
  404. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
  405. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
  406. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  407. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  408. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  409. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
  410. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  411. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
  412. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
  413. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
  414. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  415. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
  416. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
  417. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
  418. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
  419. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
  420. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
  421. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
  422. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
  423. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
  424. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
  425. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
  426. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
  427. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
  428. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
  429. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
  430. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  431. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
  432. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
  433. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
  434. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  435. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
  436. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  437. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
  438. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
  439. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
  440. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  441. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
  442. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  443. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
  444. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
  445. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
  446. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  447. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
  448. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  449. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
  450. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
  451. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
  452. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  453. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  454. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  455. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
  456. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  457. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
  458. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
  459. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
  460. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  461. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
  462. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  463. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
  464. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
  465. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
  466. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  467. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
2
+ import React, { useState } from 'react';
3
3
  import { t } from '../i18n.js';
4
4
  import { Box, Text, useTerminalSize } from '../ui.js';
5
5
  import { Divider } from './design-system/Divider.js';
@@ -9,6 +9,7 @@ import { AssistantThinkingMessage } from './messages/AssistantThinkingMessage.js
9
9
  import { AssistantToolUseMessage } from './messages/AssistantToolUseMessage.js';
10
10
  import { SubagentMessage } from './Chat/SubagentMessage.js';
11
11
  import { isMinimalMode } from '../minimalMode.js';
12
+ import { noteFrameCause, noteListGeometry } from '../ink/geometry-trace.js';
12
13
  import { InterruptedByUser } from './InterruptedByUser.js';
13
14
  import { LogoV2 } from './LogoV2.js';
14
15
  import { StreamingMarkdown } from './StreamingMarkdown.js';
@@ -16,6 +17,7 @@ import { MessageMetadata } from './messages/MessageMetadata.js';
16
17
  import { stripNarration } from '../utils/narration.js';
17
18
  import { stringWidth } from '../ink/stringWidth.js';
18
19
  import { truncateToWidth } from '../ink/truncateToWidth.js';
20
+ import { clipPreview } from '../ink/timeline-rail.js';
19
21
  /**
20
22
  * Transcript rows rendered in the Claude Code visual language: user prompts
21
23
  * on a grey bubble with a `❯` pointer, assistant text with a `●` bullet and
@@ -25,8 +27,14 @@ import { truncateToWidth } from '../ink/truncateToWidth.js';
25
27
  * rows; `selectedId` highlights the selected row.
26
28
  */
27
29
  /** Render cap for very long sessions (CC's MAX_MESSAGES_WITHOUT_VIRTUALIZATION
28
- * equivalent): older rows fold behind a Divider until Ctrl+E expands them. */
29
- const MAX_RENDERED_ROWS = 300;
30
+ * equivalent): older rows fold behind a Divider until Ctrl+E expands them.
31
+ * 120 (was 300): opening a long session paints the whole cap into the
32
+ * main-screen scrollback (historyPaint), and each row's first markdown
33
+ * lex + wrap costs ~2-5ms — 300 rows saturated the main thread for ~6s
34
+ * on open (measured, 800-row inline session). 120 rows ≈ 4-5 screens of
35
+ * paint (<1s) with the rest behind the show-previous divider (CC parity:
36
+ * the transcript is a viewport, not a printout; load-earlier restores). */
37
+ const MAX_RENDERED_ROWS = 120;
30
38
  // --- layout virtualization constants -------------------------------------
31
39
  // Offscreen rows render as fixed-height spacers whose heights come from the
32
40
  // previous commit's Yoga layout, so the pure-JS Yoga engine never walks
@@ -39,23 +47,170 @@ const DEFAULT_ROW_HEIGHT = 2;
39
47
  /** Cold-start estimate of the header block above the rows; corrected by the
40
48
  * first layout measurement. */
41
49
  const DEFAULT_HEADER_LINES = 14;
42
- export function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, model, diffLayout = 'auto', thinkingFold = 'preview', toolBackground = 'none', activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible = true, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, failureHintRowId, failureHint, }) {
50
+ /** Stable fallbacks for the stream-fold props: verify/repro harnesses and
51
+ * embedders render MessageList with prop sets that predate them, and the
52
+ * render must not throw (same rule as Chat's stubbed channel APIs). Module
53
+ * scope keeps the identities stable so MemoRow's shallow compare and the
54
+ * toggle callback's deps never churn. */
55
+ const NO_STREAM_FOLDED = new Set();
56
+ const NOOP_TOGGLE_STREAM_FOLD = (_rowId) => { };
57
+ /**
58
+ * Per-kind layout signature PARTS: the O(1) identity of every input that
59
+ * decides a row's rendered HEIGHT (see sigRef in MessageList). Fields are
60
+ * scoped to the row's own renderer — a global flat signature
61
+ * over-invalidates (a diffLayout switch must not drop user-message
62
+ * heights). Text uses length as the proxy: full-text hashing per row per
63
+ * frame would defeat virtualization's budget, and a same-length miss only
64
+ * degrades to the previous behavior.
65
+ *
66
+ * Returns a PARTS VECTOR instead of a joined string: the caller compares
67
+ * slot-by-slot against the cached vector (all primitives), allocating only
68
+ * when a row's inputs actually changed. Building a joined string per row
69
+ * per render was O(rows) allocation churn on every scroll tick — the GC
70
+ * share of the long-session scroll profile.
71
+ *
72
+ * The vector is a MODULE-LEVEL SCRATCH BUFFER: single-threaded synchronous
73
+ * render makes reuse safe, and the unchanged case (the overwhelming
74
+ * majority) allocates nothing. Callers that retain the vector must copy
75
+ * (parts.slice()).
76
+ */
77
+ const signatureScratch = [];
78
+ function signatureParts(row, columns, expanded, expandedRows, streamFolded, thinkingVisible, thinkingFold, diffLayout, model, failureHintRowId, failureHint) {
79
+ signatureScratch.length = 0;
80
+ // Universal height inputs: width reflows every row; kind switches height
81
+ // semantics wholesale; text length drives wrapping.
82
+ signatureScratch.push(columns, row.kind, row.text?.length ?? 0);
83
+ switch (row.kind) {
84
+ case 'assistant':
85
+ // Streaming vs settled swaps renderers; Ctrl+O/per-row expand adds the
86
+ // metadata row (model only renders expanded — keeps an idle /model
87
+ // switch from touching settled rows).
88
+ signatureScratch.push(row.streaming === true, expanded, expandedRows.has(row.id), expanded ? model : '');
89
+ break;
90
+ case 'reasoning':
91
+ // thinkingFold (preview vs full) and the visibility filter change the
92
+ // folded card's height; streaming shows verbose live unless the user
93
+ // clicked it folded (streamFolded collapses to the ticker/header).
94
+ signatureScratch.push(row.streaming === true, expanded, expandedRows.has(row.id), streamFolded.has(row.id), thinkingVisible, thinkingFold);
95
+ break;
96
+ case 'tool': {
97
+ const tool = row.tool;
98
+ signatureScratch.push(expanded, expandedRows.has(row.id), diffLayout, tool?.status ?? '', tool?.resultText?.length ?? 0, tool?.resultFull?.length ?? 0, tool?.errorText?.length ?? 0, row.id === failureHintRowId ? failureHint ?? '' : '');
99
+ break;
100
+ }
101
+ case 'subagent':
102
+ // 卡片高度输入:running→settled 折叠 waterfall+tool 行(5→1 行),
103
+ // failed 增加 error 行。缺这些字段的话 offscreen 结算后 cached
104
+ // height 永不过期 → 滚回 blank band / 滚不到底。
105
+ signatureScratch.push(row.subagent?.status ?? '', row.subagent?.toolCalls.length ?? 0, row.subagent?.outputLines.length ?? 0, row.subagent?.error?.length ?? 0);
106
+ break;
107
+ case 'compact':
108
+ // Folded one-liner vs full summary text.
109
+ signatureScratch.push(expanded, expandedRows.has(row.id));
110
+ break;
111
+ default:
112
+ // user / notice / interrupt / local / local-output: height follows
113
+ // text + columns alone (selection/background never change height).
114
+ break;
115
+ }
116
+ return signatureScratch;
117
+ }
118
+ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamFoldedRows = NO_STREAM_FOLDED, onToggleStreamFold = NOOP_TOGGLE_STREAM_FOLD, model, diffLayout = 'auto', thinkingFold = 'preview', toolBackground = 'none', activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible = true, historyPaintEnabled = true, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, onOpenFile, }) {
43
119
  const hiddenCount = rows.length - MAX_RENDERED_ROWS;
44
120
  // The thinking filter runs BEFORE virtualization so window indices line up.
45
- const visibleRows = (showAll || hiddenCount <= 0
46
- ? rows
47
- : rows.slice(hiddenCount)).filter(row => thinkingVisible || row.kind !== 'reasoning');
48
- // CC addMargin: every rendered block gets a 1-row top margin except the
49
- // first. Pre-pass over the FULL list so a windowed row keeps the exact
50
- // spacing it would have in a fully-mounted list.
51
- const margins = new Map();
52
- {
53
- let prev;
54
- for (const row of visibleRows) {
55
- margins.set(row.id, prev !== undefined);
56
- prev = row.kind;
121
+ //
122
+ // Fingerprint memo: every scroll tick re-rendered this pipeline even when
123
+ // nothing changed — slice + filter allocate a fresh rows-length array and
124
+ // the margins pre-pass allocates a Map with one entry per row (3200-row
125
+ // session ⇒ ~200KB churn per 16ms tick, the GC share of the scroll
126
+ // profile). The inputs are all identity-stable across ticks: channel.rows
127
+ // is a live in-place array (identity changes only on rewind/new session),
128
+ // showAll/thinkingVisible are React state. Rows APPENDED in place keep the
129
+ // identity — the cache must key on rows.length too (streaming appends).
130
+ const visibleRowsCacheRef = React.useRef(null);
131
+ /** Generation counter for the visibleRows cache (timeline memo key). */
132
+ const visGenRef = React.useRef(0);
133
+ const visibleCache = visibleRowsCacheRef.current;
134
+ // Streaming-bit fingerprint: in-place `streaming = false` writes (turn
135
+ // settle) are invisible to the rows-identity/length key above, but an
136
+ // assistant row that settles with EMPTY text crosses the empty-assistant
137
+ // filter boundary (visible-while-streaming → filtered-when-settled).
138
+ // Allocation-free scan; rebuild only when a bit actually flipped.
139
+ let streamBitsSame = visibleCache !== null && visibleCache.streamBits.length === rows.length;
140
+ if (streamBitsSame) {
141
+ const bits = visibleCache.streamBits;
142
+ for (let i = 0; i < rows.length; i++) {
143
+ if (bits[i] !== (rows[i].streaming === true ? 1 : 0)) {
144
+ streamBitsSame = false;
145
+ break;
146
+ }
57
147
  }
58
148
  }
149
+ if (visibleCache === null ||
150
+ visibleCache.rows !== rows ||
151
+ visibleCache.rowsLength !== rows.length ||
152
+ visibleCache.showAll !== (showAll || hiddenCount <= 0) ||
153
+ visibleCache.thinkingVisible !== thinkingVisible ||
154
+ !streamBitsSame) {
155
+ const sliced = showAll || hiddenCount <= 0
156
+ ? rows
157
+ : rows.slice(hiddenCount);
158
+ // Empty settled assistant rows (PR #383's duplicate-dot bug): when the
159
+ // model calls a tool without producing text, the assistant/message event
160
+ // carries empty text — rendered as a lone `●` bullet dangling above the
161
+ // tool card. Filter them BEFORE virtualization (not by rendering null in
162
+ // TranscriptRow): a null row never mounts, never enters paintedOnce, and
163
+ // would stall the main-screen history-paint batch loop forever. A row
164
+ // that is STILL STREAMING keeps its place even with empty text — the
165
+ // live dot is the "model is answering" affordance and content may yet
166
+ // arrive.
167
+ // The emptiness test must match what RENDERING shows: the `⏵`
168
+ // self-narration line (dsh-working-activity narrate contract) is
169
+ // stripped at render (stripNarration below), so a narration-only step —
170
+ // thinking, `⏵ …` line, straight to a tool call — has non-empty raw
171
+ // text but RENDERS as that same lone `●`. Test the stripped text, or
172
+ // the raw-text check lets the dot through forever.
173
+ const rendersEmptyAssistant = (row) => row.kind === 'assistant' && row.streaming !== true && stripNarration(row.text ?? '').trim() === '';
174
+ let hasEmptyAssistant = false;
175
+ for (const row of sliced) {
176
+ if (rendersEmptyAssistant(row)) {
177
+ hasEmptyAssistant = true;
178
+ break;
179
+ }
180
+ }
181
+ const out = hasEmptyAssistant
182
+ ? sliced.filter(row => !rendersEmptyAssistant(row) &&
183
+ (thinkingVisible || row.kind !== 'reasoning'))
184
+ : thinkingVisible
185
+ ? sliced
186
+ : sliced.filter(row => row.kind !== 'reasoning');
187
+ // CC addMargin: every rendered block gets a 1-row top margin except the
188
+ // first. Pre-pass over the FULL list so a windowed row keeps the exact
189
+ // spacing it would have in a fully-mounted list.
190
+ const margins = new Map();
191
+ {
192
+ let prev;
193
+ for (const row of out) {
194
+ margins.set(row.id, prev !== undefined);
195
+ prev = row.kind;
196
+ }
197
+ }
198
+ const streamBits = new Uint8Array(rows.length);
199
+ for (let i = 0; i < rows.length; i++)
200
+ streamBits[i] = rows[i].streaming === true ? 1 : 0;
201
+ visibleRowsCacheRef.current = {
202
+ rows,
203
+ rowsLength: rows.length,
204
+ showAll: showAll || hiddenCount <= 0,
205
+ thinkingVisible,
206
+ out,
207
+ margins,
208
+ streamBits,
209
+ };
210
+ visGenRef.current++;
211
+ }
212
+ const visibleRows = visibleRowsCacheRef.current.out;
213
+ const margins = visibleRowsCacheRef.current.margins;
59
214
  // Selection keeps its highlight; expanded rows render with no fill (the
60
215
  // diff line tints inside cards are the only backgrounds in the transcript).
61
216
  const rowBackground = (rowId) => {
@@ -74,6 +229,9 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
74
229
  // DEFAULT_ROW_HEIGHT, which only perturbs deep scrollback estimates.
75
230
  const HEIGHTS_CACHE_MAX = 5000;
76
231
  const heightsRef = React.useRef(new Map());
232
+ /** Geometry version: bumped at EVERY heightsRef mutation so downstream
233
+ * memos (timeline tops) can key on it instead of re-deriving offsets. */
234
+ const heightsVersionRef = React.useRef(0);
77
235
  const localRefs = React.useRef(new Map());
78
236
  /** Row ids that have been mounted (and therefore painted into the
79
237
  * terminal) at least once. The sticky window may skip a row ONLY after
@@ -94,9 +252,20 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
94
252
  * free: those rows sit in scrollback and the diff skips them. */
95
253
  const lastStartRef = React.useRef(-1);
96
254
  const holdUntilRef = React.useRef(0);
255
+ /** True when frame-budgeted history painting still has batches left
256
+ * (main-screen open): the layout effect schedules the next slice. */
257
+ const paintPendingRef = React.useRef(false);
258
+ const paintQueuedRef = React.useRef(false);
259
+ /** Persistent history-paint edge: how far batched painting has advanced
260
+ * (index into visibleRows). -1 = not painting / reset (list head change:
261
+ * rewind, new session, loadOlder — must repaint from scratch). */
262
+ const paintEdgeRef = React.useRef(-1);
97
263
  const listHeadId = visibleRows[0]?.id;
98
264
  if (listHeadId !== undefined && paintedBaseRef.current !== undefined && listHeadId !== paintedBaseRef.current) {
99
265
  paintedOnceRef.current = new Set();
266
+ // History repaints from scratch after a head change too (rewind /
267
+ // loadOlder prepends rows that must paint again).
268
+ paintEdgeRef.current = -1;
100
269
  }
101
270
  if (listHeadId !== undefined)
102
271
  paintedBaseRef.current = listHeadId;
@@ -110,8 +279,66 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
110
279
  if (lastColumns.current !== columns) {
111
280
  lastColumns.current = columns;
112
281
  heightsRef.current.clear();
282
+ heightsVersionRef.current++;
113
283
  baseRef.current = null;
114
284
  }
285
+ // --- layout signature: stale-height invalidation ------------------------
286
+ // heightsRef entries outlive the commits that measured them, but many
287
+ // state changes rewrite a row's height WITHOUT a columns change: Ctrl+O
288
+ // (expanded), single-row expand (expandedRows), reasoning stream→fold,
289
+ // a tool result/error/footnote arriving, diff layout switch, assistant
290
+ // text growth, thinking visibility. A cached height from before such a
291
+ // change feeds topPad/bottomPad spacers, the offsets scan, and the
292
+ // ScrollBox clamps with geometry that no longer exists — blank bands,
293
+ // overlapping rows, wrong scrollTop after toggles (the audit's stale
294
+ // height cache). Track the inputs that decide each row's height; when
295
+ // one changes, drop the cached height. The window extension further
296
+ // down remounts invalidated rows so useLayoutEffect re-measures them.
297
+ // Text identity uses length as an O(1) proxy — per-frame full-text
298
+ // hashing over every row would defeat virtualization's budget, and a
299
+ // same-length miss only degrades to the previous behavior.
300
+ //
301
+ // VECTORS, not strings: the cache stores the parts VECTOR and the
302
+ // comparison is slot-by-slot — the unchanged case (every settled row on
303
+ // every scroll tick) allocates nothing. A joined string per row per
304
+ // render was O(rows) garbage per tick (3200-row session ⇒ several MB/s
305
+ // into minor GC; the GC share of the scroll profile).
306
+ const sigRef = React.useRef(new Map());
307
+ {
308
+ const sigs = sigRef.current;
309
+ for (let i = 0; i < visibleRows.length; i++) {
310
+ const row = visibleRows[i];
311
+ // Per-kind signature: only the inputs that row's OWN renderer consumes.
312
+ // A global flat array (every field × every row) over-invalidates — a
313
+ // /settings diffLayout switch used to drop every user/assistant/
314
+ // reasoning height at once, remounting the widened window over rows
315
+ // whose rendering never changed (Yoga spike + measure churn for
316
+ // nothing). Base parts cover the universal height inputs.
317
+ const parts = signatureParts(row, columns, expanded, expandedRows, streamFoldedRows, thinkingVisible, thinkingFold, diffLayout, model, failureHintRowId, failureHint);
318
+ const cachedParts = sigs.get(row.id);
319
+ let same = false;
320
+ if (cachedParts !== undefined && cachedParts.length === parts.length) {
321
+ same = true;
322
+ for (let s = 0; s < parts.length; s++) {
323
+ if (parts[s] !== cachedParts[s]) {
324
+ same = false;
325
+ break;
326
+ }
327
+ }
328
+ }
329
+ if (same)
330
+ continue;
331
+ if (sigs.size >= HEIGHTS_CACHE_MAX) {
332
+ const oldest = sigs.keys().next().value;
333
+ if (oldest !== undefined)
334
+ sigs.delete(oldest);
335
+ }
336
+ // Copy out of the module-level scratch buffer — the next row reuses it.
337
+ sigs.set(row.id, parts.slice());
338
+ heightsRef.current.delete(row.id);
339
+ heightsVersionRef.current++;
340
+ }
341
+ }
115
342
  // Scrolling bypasses React (imperative DOM scrollTop): subscribe so the
116
343
  // window follows the viewport.
117
344
  React.useEffect(() => {
@@ -120,8 +347,17 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
120
347
  const tick = () => { setScrollTick(t => t + 1); };
121
348
  return scrollHandle.subscribe(tick);
122
349
  }, [scrollHandle]);
350
+ // Cached rail/header preview per user row (see the timeline block for why
351
+ // the length guard exists alongside the id key).
352
+ const previewCacheRef = React.useRef(new Map());
123
353
  const heightOf = (row) => heightsRef.current.get(row.id) ?? DEFAULT_ROW_HEIGHT;
124
- const offsets = new Array(visibleRows.length);
354
+ // Reused offsets buffer: the prefix-scan itself must run every render
355
+ // (heightsRef mutates in the measure effect), but the ARRAY need not be
356
+ // fresh — offsets never escapes this render scope. A new 3200-slot array
357
+ // per scroll tick was pure GC fodder.
358
+ const offsetsBufRef = React.useRef([]);
359
+ const offsets = offsetsBufRef.current;
360
+ offsets.length = visibleRows.length;
125
361
  let total = 0;
126
362
  for (let i = 0; i < visibleRows.length; i++) {
127
363
  offsets[i] = total;
@@ -188,9 +424,87 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
188
424
  // vanish from the user's scrollback. Extending mounts everything above
189
425
  // on the first frame (topPad 0, full paint), then the set fills and the
190
426
  // window tightens to the tail.
427
+ // MAIN-SCREEN ONLY (historyPaintEnabled): the alt-screen has no
428
+ // scrollback — a row outside the window has no "copy" to preserve, and
429
+ // mounting the whole fold window on open lexed/highlighted/laid out
430
+ // hundreds of markdown rows the user never sees (measured: 4.3s of
431
+ // saturated main thread before first paint on a 960-row session; the
432
+ // virtualization window re-mounts rows on demand as they scroll in).
433
+ // FRAME-BUDGETED: mounting the whole window in ONE commit saturated the
434
+ // main thread for seconds (measured 6s wall with ZERO frames in the
435
+ // first second on an 800-row inline open — every input queued behind
436
+ // the React render). Extend in batches of ~2 viewports of measured
437
+ // height per commit instead; the pending-batch effect schedules the
438
+ // next slice, so the app paints, drains input, and stays interactive
439
+ // while history streams in above the fold (opencode-style progressive
440
+ // transcript hydration; the terminal accepts rows whenever they land).
191
441
  const paintedOnce = paintedOnceRef.current;
442
+ let paintPending = false;
443
+ if (historyPaintEnabled) {
444
+ // Persistent batch edge: the sticky floor-walk RESETS start to the
445
+ // tail every frame, so a per-frame budget walk from `start` re-spends
446
+ // its whole budget on already-painted rows and never reaches deeper
447
+ // unpainted ones (measured: an infinite 0.3ms/frame batch loop).
448
+ // Remember how far painting has advanced; each batch extends THAT
449
+ // edge upward by the budget.
450
+ if (paintEdgeRef.current < 0)
451
+ paintEdgeRef.current = start;
452
+ let firstUnpainted = -1;
453
+ for (let i = 0; i < paintEdgeRef.current; i++) {
454
+ if (!paintedOnce.has(visibleRows[i].id)) {
455
+ firstUnpainted = i;
456
+ break;
457
+ }
458
+ }
459
+ if (firstUnpainted !== -1) {
460
+ // Budget: extend the paint edge upward by ~half a viewport of
461
+ // measured content per commit (height estimates; unknown rows fall
462
+ // back to DEFAULT_ROW_HEIGHT and over-mount slightly — harmless).
463
+ // Measured: larger batches (2 viewports) put 50ms+ of first-wrap
464
+ // yoga into one frame; half a viewport keeps batches near the
465
+ // 16ms frame budget while still finishing an 800-row open in
466
+ // well under two seconds of background batches.
467
+ let budget = Math.min(viewport, termRows) / 2 + OVERSCAN_LINES;
468
+ let j = paintEdgeRef.current;
469
+ while (j > firstUnpainted && budget > 0) {
470
+ j--;
471
+ budget -= heightOf(visibleRows[j]);
472
+ }
473
+ paintEdgeRef.current = j;
474
+ start = Math.min(start, j);
475
+ paintPending = j > 0;
476
+ }
477
+ }
478
+ else {
479
+ paintEdgeRef.current = -1;
480
+ }
481
+ paintPendingRef.current = paintPending;
482
+ // Unknown-height extension (layout signature, see sigRef): a row whose
483
+ // cached height was just INVALIDATED must remount to re-measure even
484
+ // when it sits outside the window — its spacer otherwise falls back to
485
+ // DEFAULT_ROW_HEIGHT until the row scrolls back into view, leaving the
486
+ // content geometry wrong for exactly that long (blank band after
487
+ // Ctrl+O, unreachable scroll bottom after a tool result lands). One
488
+ // remount per change; the measure tick + hold then tighten again.
489
+ // Guard 1: only rows that have actually MOUNTED here once qualify
490
+ // (paintedOnce fills from localRefs post-commit) — a brand-new
491
+ // streaming row has never been measured, and extending over it would
492
+ // mount everything below the window every frame while the user reads
493
+ // scrolled-up (virtualization defeated, per-frame full mount = the
494
+ // long-session stall). New rows keep the original path: their height
495
+ // lands once the window reaches them.
496
+ // Guard 2: rows actively STREAMING are skipped too. A streaming row's
497
+ // signature invalidates EVERY chunk (text length grows), so a painted
498
+ // streaming row below/above the window remounted per chunk — full
499
+ // markdown re-lex + wrap of the whole growing text, invisible to the
500
+ // user reading history (measured: 3s of streaming while scrolled up
501
+ // burned 2.5s of yoga and +84MB heap). Its height is in flux anyway;
502
+ // the final settle invalidates once more and remounts exactly once.
192
503
  for (let i = 0; i < start; i++) {
193
- if (!paintedOnce.has(visibleRows[i].id)) {
504
+ if (visibleRows[i].streaming === true)
505
+ continue;
506
+ const rowId = visibleRows[i].id;
507
+ if (!heightsRef.current.has(rowId) && paintedOnceRef.current.has(rowId)) {
194
508
  start = i;
195
509
  break;
196
510
  }
@@ -208,6 +522,21 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
208
522
  }
209
523
  lastStartRef.current = start;
210
524
  }
525
+ // Tail-side invalidated-height extension (see the start-side loop above
526
+ // for the rationale and both guards): rows BELOW the window whose height
527
+ // was just invalidated remount to re-measure, so bottomPad keeps real
528
+ // geometry while the user reads scrolled-up content and the tail streams.
529
+ // Runs for non-sticky views; sticky mounts the tail anyway. Streaming
530
+ // rows are skipped — THIS loop is the measured hot path of the
531
+ // read-while-streaming stall (the streaming tail row sits below the
532
+ // window and invalidated per chunk).
533
+ for (let i = end; i < visibleRows.length; i++) {
534
+ if (visibleRows[i].streaming === true)
535
+ continue;
536
+ const rowId = visibleRows[i].id;
537
+ if (!heightsRef.current.has(rowId) && paintedOnceRef.current.has(rowId))
538
+ end = i + 1;
539
+ }
211
540
  if (forceMountRowId !== undefined && forceMountRowId !== null) {
212
541
  const idx = visibleRows.findIndex(row => row.id === forceMountRowId);
213
542
  if (idx !== -1) {
@@ -227,6 +556,20 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
227
556
  const topPad = offsets[start] ?? 0;
228
557
  const mountedBottom = end < visibleRows.length ? offsets[end] : total;
229
558
  const bottomPad = total - mountedBottom;
559
+ noteListGeometry({
560
+ start,
561
+ end,
562
+ topPad,
563
+ bottomPad,
564
+ total,
565
+ base,
566
+ sticky,
567
+ pending,
568
+ viewport,
569
+ termRows,
570
+ columns,
571
+ rowCount: visibleRows.length,
572
+ });
230
573
  // New-messages pill count: rows past the seen-anchor whose top edge is
231
574
  // still below the viewport bottom. Same rows-space math as the window
232
575
  // (offsets are rows-space, scrollTop content-space — subtract the header
@@ -252,6 +595,131 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
252
595
  onUnseenCount?.(unseenCount);
253
596
  }
254
597
  });
598
+ // Conversation timeline snapshot: one entry per user turn (stable id +
599
+ // content-space text top + preview), plus the viewport-derived targets
600
+ // consumed by BOTH the sticky prompt header (active) and the transcript
601
+ // turn rail (active highlight, ▲/▼). Top-anchored semantics (Grok
602
+ // timeline): active = the LAST turn whose prompt top is at-or-above the
603
+ // viewport top — the turn whose content owns the top row, "the turn
604
+ // being read" — with the first turn standing in while pre-turn content
605
+ // (logo / loaded-context) owns the top. The highlight moves only when a
606
+ // turn boundary crosses the viewport top, never when a later prompt
607
+ // merely becomes visible lower on screen, so it cannot leap when
608
+ // nudging off the bottom and the header/rail can never disagree. The
609
+ // projected top (scrollTop + pending) is used so boundary crossings
610
+ // register during wheel bursts, not one drain frame late. Same
611
+ // rows-space math as the mount window (offsets are rows-space,
612
+ // scrollTop content-space — add the header base). downId additionally
613
+ // requires the turn's top ≤ maxScroll: the renderer clamps scrollTop
614
+ // there, so a turn past it could never own the top row, and naming it
615
+ // would make ▼ repeat itself forever (the stuck-▼ bug). Reported
616
+ // post-commit, only when the signature changes.
617
+ let timelineTurns = [];
618
+ let activeTurnIndex = null;
619
+ let upTurnIndex = null;
620
+ let downTurnIndex = null;
621
+ const timelineMemoRef = React.useRef(null);
622
+ {
623
+ // Split into (a) a GEOMETRY-memoized turns list and (b) a per-frame
624
+ // allocation-free target scan. Before the split this block rebuilt on
625
+ // EVERY scroll tick: an 800-object turns array, an 800-entry
626
+ // measuredTops Map, and the report effect's turns.map().join('|')
627
+ // signature — several hundred KB of churn per second of scrolling on a
628
+ // tool-heavy session.
629
+ //
630
+ // (a) turns/tops/folded change ONLY when geometry changes: row heights
631
+ // (heightsVersion — bumped at every heightsRef mutation), the visible
632
+ // window's content (visGen — bumped when the visibleRows cache
633
+ // rebuilds), the measured header base, or the rows array growing. Key
634
+ // on those; previews stay in their own id-keyed cache.
635
+ const memo = timelineMemoRef.current;
636
+ const memoKey = `${visGenRef.current}:${heightsVersionRef.current}:${base}:${rows.length}:${columns}`;
637
+ if (memo === null || memo.key !== memoKey) {
638
+ const previewCache = previewCacheRef.current;
639
+ if (previewCache.size > 2000)
640
+ previewCache.clear();
641
+ // Measured tops for turns INSIDE the fold window (user rows are never
642
+ // filtered by the thinking toggle, so a user row absent from
643
+ // visibleRows is exactly a folded one).
644
+ const measuredTops = new Map();
645
+ for (let i = 0; i < visibleRows.length; i++) {
646
+ const row = visibleRows[i];
647
+ if (row.kind !== 'user')
648
+ continue;
649
+ measuredTops.set(row.id, base + offsets[i] + (margins.get(row.id) === true ? 1 : 0));
650
+ }
651
+ // Walk ALL rows (not the fold window): the rail must cover the whole
652
+ // conversation — a tool-heavy session packs 300 rows into a handful
653
+ // of turns, and window-only turns made the rail show "2-3 nodes".
654
+ // Folded turns carry folded:true + top:-1; their tops are unknown
655
+ // until revealed (they are all ABOVE the viewport, so active/down
656
+ // over measured turns is unaffected; ▲ may name a folded turn — its
657
+ // click goes through the reveal path, not scrollTo).
658
+ const turns = [];
659
+ for (const row of rows) {
660
+ if (row.kind !== 'user')
661
+ continue;
662
+ let cached = previewCache.get(row.id);
663
+ if (cached === undefined || cached.len !== row.text.length) {
664
+ cached = { len: row.text.length, preview: clipPreview(row.text) };
665
+ previewCache.set(row.id, cached);
666
+ }
667
+ const textTop = measuredTops.get(row.id);
668
+ if (textTop !== undefined) {
669
+ turns.push({ id: row.id, top: textTop, preview: cached.preview });
670
+ }
671
+ else {
672
+ turns.push({ id: row.id, top: -1, preview: cached.preview, folded: true });
673
+ }
674
+ }
675
+ timelineMemoRef.current = { key: memoKey, turns };
676
+ }
677
+ timelineTurns = timelineMemoRef.current.turns;
678
+ // (b) per-frame target scan — pure integer comparisons over the
679
+ // memoized list; viewTop is projected (scrollTop + pending) so
680
+ // boundary crossings register during wheel bursts, not one drain
681
+ // frame late. downId additionally requires top ≤ maxScroll: the
682
+ // renderer clamps scrollTop there, so a turn past it could never own
683
+ // the top row, and naming it would make ▼ repeat itself forever.
684
+ const viewTop = scrollTop + pending;
685
+ const maxScroll = Math.max(0, (scrollHandle?.getScrollHeight() ?? 0) - viewport);
686
+ for (let i = 0; i < timelineTurns.length; i++) {
687
+ const t = timelineTurns[i];
688
+ if (t.folded === true) {
689
+ // Above the fold ⇒ strictly above the viewport: a legal ▲ target.
690
+ upTurnIndex = i;
691
+ continue;
692
+ }
693
+ if (t.top <= viewTop)
694
+ activeTurnIndex = i;
695
+ if (t.top < viewTop)
696
+ upTurnIndex = i;
697
+ if (downTurnIndex === null && t.top > viewTop && t.top <= maxScroll) {
698
+ downTurnIndex = i;
699
+ }
700
+ }
701
+ if (timelineTurns.length > 0 && activeTurnIndex === null)
702
+ activeTurnIndex = 0;
703
+ }
704
+ const timeline = {
705
+ turns: timelineTurns,
706
+ activeId: activeTurnIndex === null ? null : timelineTurns[activeTurnIndex].id,
707
+ upId: upTurnIndex === null ? null : timelineTurns[upTurnIndex].id,
708
+ downId: downTurnIndex === null ? null : timelineTurns[downTurnIndex].id,
709
+ };
710
+ const lastTimelineReportRef = React.useRef('');
711
+ React.useEffect(() => {
712
+ // O(1) signature: the memo key pins the turns' geometry identity
713
+ // (heights/window/base/rows-length) and the three ids pin the
714
+ // viewport-derived targets. Any top change bumps the geometry key, so
715
+ // the report still fires exactly when the snapshot content changes —
716
+ // without rebuilding an O(turns) joined string per commit.
717
+ const sig = `${timelineMemoRef.current?.key ?? ''}#a${timeline.activeId}#u${timeline.upId}#d${timeline.downId}`;
718
+ if (sig !== lastTimelineReportRef.current) {
719
+ lastTimelineReportRef.current = sig;
720
+ onTimeline?.(timeline);
721
+ }
722
+ });
255
723
  // Post-commit: measure mounted rows, derive the content-space base from
256
724
  // the first mounted row's Yoga top, and clamp render-time scrollTop to the
257
725
  // mounted coverage so burst scrolls never show blank spacer.
@@ -272,6 +740,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
272
740
  heightsRef.current.delete(oldest);
273
741
  }
274
742
  heightsRef.current.set(id, h);
743
+ heightsVersionRef.current++;
275
744
  changed = true;
276
745
  }
277
746
  }
@@ -301,7 +770,17 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
301
770
  }
302
771
  else {
303
772
  const min = Math.max(0, base + topPad - viewport);
304
- scrollHandle.setClampBounds(min, Math.max(min, base + mountedBottom - viewport));
773
+ // Upper clamp only while unmounted content remains below the window
774
+ // (bottomPad spacer): it exists to pin the paint to the mounted edge
775
+ // during burst scrolls that outrun React's window re-render. Once the
776
+ // tail is fully mounted (bottomPad 0) there IS no unmounted gap — the
777
+ // estimated `mountedBottom` can sit a line short of the real Yoga
778
+ // extent (engine flex-basis cache vs final child layout drift, see
779
+ // render-node-to-output's scrollHeight floor), and an estimated clamp
780
+ // would then cull the last line at every non-sticky paint — the
781
+ // scrolled-away-and-back tail loss. Leave the max open; the renderer
782
+ // still caps at the frame's real maxScroll.
783
+ scrollHandle.setClampBounds(min, bottomPad <= 0 ? undefined : Math.max(min, base + mountedBottom - viewport));
305
784
  }
306
785
  }
307
786
  if (changed && !measureQueuedRef.current) {
@@ -310,9 +789,27 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
310
789
  measureQueuedRef.current = true;
311
790
  queueMicrotask(() => {
312
791
  measureQueuedRef.current = false;
792
+ noteFrameCause('measure');
313
793
  setMeasureTick(t => t + 1);
314
794
  });
315
795
  }
796
+ // History-paint continuation (main-screen open): more never-painted
797
+ // batches remain — schedule the next slice on the macrotask queue so
798
+ // pending input events (wheel, keys) drain between batches and the app
799
+ // stays interactive while preset history streams in. A timeout of 0 is
800
+ // enough: each batch's mount+measure work is bounded (~2 viewports),
801
+ // unlike the previous single-commit full mount that saturated the main
802
+ // thread for seconds.
803
+ if (paintPendingRef.current && !paintQueuedRef.current) {
804
+ paintQueuedRef.current = true;
805
+ setTimeout(() => {
806
+ paintQueuedRef.current = false;
807
+ if (!paintPendingRef.current)
808
+ return;
809
+ noteFrameCause('measure');
810
+ setMeasureTick(t => t + 1);
811
+ }, 0);
812
+ }
316
813
  });
317
814
  // useCallback: the reference feeds MemoRow's shallow compare; a fresh
318
815
  // closure per render would defeat every row's memo.
@@ -323,11 +820,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
323
820
  localRefs.current.delete(rowId);
324
821
  registerRowRef?.(rowId, el);
325
822
  }, [registerRowRef]);
326
- // Second-resolution clock for the running tool card's live elapsed time.
327
- // Computed per render (cheap) but only forwarded to running rows, so
328
- // settled rows never see a changing prop.
329
- const nowSec = Math.floor(Date.now() / 1000);
330
- return (_jsxs(_Fragment, { children: [rows.some(row => row.folded) && (_jsx(Box, { marginTop: 1, onClick: onLoadOlder, children: _jsx(Divider, { title: t('load-earlier') }) })), !showAll && hiddenCount > 0 && (_jsx(Box, { marginTop: 1, onClick: onToggleAll, children: _jsx(Divider, { title: t('show-previous-messages', { n: hiddenCount }) }) })), topPad > 0 && _jsx(Box, { height: topPad, flexShrink: 0 }), visibleRows
823
+ return (_jsxs(_Fragment, { children: [rows.some(row => row.folded) && (_jsx(ClickableDivider, { title: t('load-earlier'), onClick: onLoadOlder })), !showAll && hiddenCount > 0 && (_jsx(ClickableDivider, { title: t('show-previous-messages', { n: hiddenCount }), onClick: onToggleAll })), topPad > 0 && _jsx(Box, { height: topPad, flexShrink: 0 }), visibleRows
331
824
  .slice(start, end)
332
825
  .map((row) => {
333
826
  // CC addMargin: pre-pass result keeps windowed rows at full-mount
@@ -335,30 +828,56 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
335
828
  const addMargin = margins.get(row.id) === true;
336
829
  const tool = row.tool;
337
830
  const subagent = row.kind === 'subagent' ? row.subagent : undefined;
338
- return (_jsx(MemoRow, { rowId: row.id, kind: row.kind, text: row.text, executionTarget: row.executionTarget, streaming: row.streaming === true, durationMs: row.durationMs, time: row.time, addMargin: addMargin, isSelected: selectedId === row.id, isExpanded: expandedRows.has(row.id), expanded: expanded, model: model, diffLayout: diffLayout, thinkingFold: thinkingFold, toolBackground: toolBackground, activityFrames: activityFrames, background: rowBackground(row.id), toolCallId: tool?.callId, toolName: tool?.name, toolArgsText: tool?.argsText, toolArgsFull: tool?.argsFull, toolStatus: tool?.status, toolResultText: tool?.resultText, toolResultFull: tool?.resultFull, toolErrorText: tool?.errorText, toolFootnote: failureHintRowId === row.id ? failureHint : undefined, toolCallView: tool?.callView, toolResultView: tool?.resultView, toolStartedAt: tool?.startedAt, toolDurationMs: tool?.durationMs, nowSec: tool?.status === 'running' ? nowSec : undefined, subagent: subagent, onToggleRow: onToggleRow, setRowRef: setRowRef }, row.id));
831
+ return (_jsx(MemoRow, { rowId: row.id, kind: row.kind, text: row.text, executionTarget: row.executionTarget, streaming: row.streaming === true, durationMs: row.durationMs, time: row.time, addMargin: addMargin, isSelected: selectedId === row.id, isExpanded: expandedRows.has(row.id), expanded: expanded, model: model, diffLayout: diffLayout, thinkingFold: thinkingFold, toolBackground: toolBackground, activityFrames: activityFrames, background: rowBackground(row.id), toolCallId: tool?.callId, toolName: tool?.name, toolArgsText: tool?.argsText, toolArgsFull: tool?.argsFull, toolStatus: tool?.status, toolResultText: tool?.resultText, toolResultFull: tool?.resultFull, toolErrorText: tool?.errorText, toolFootnote: failureHintRowId === row.id ? failureHint : undefined, toolCallView: tool?.callView, toolResultView: tool?.resultView, toolStartedAt: tool?.startedAt, toolDurationMs: tool?.durationMs, subagent: subagent, onToggleRow: onToggleRow, onToggleStreamFold: onToggleStreamFold, streamFolded: streamFoldedRows.has(row.id), onOpenSubagent: onOpenSubagent, onOpenFile: onOpenFile, setRowRef: setRowRef }, row.id));
339
832
  }), bottomPad > 0 && _jsx(Box, { height: bottomPad, flexShrink: 0 })] }));
340
833
  }
341
- function TranscriptRow({ rowId, kind, text, executionTarget, streaming, durationMs, time, addMargin, isSelected, isExpanded, expanded, model, diffLayout, thinkingFold, toolBackground, activityFrames, background, toolCallId, toolName, toolArgsText, toolArgsFull, toolStatus, toolResultText, toolResultFull, toolErrorText, toolFootnote, toolCallView, toolResultView, toolStartedAt, toolDurationMs, subagent, onToggleRow, setRowRef, }) {
834
+ /** Load-earlier / show-previous divider row with a mouse hover tint — the
835
+ * clickability is otherwise invisible (audit C-04). */
836
+ function ClickableDivider({ title, onClick }) {
837
+ const [hovered, setHovered] = useState(false);
838
+ return (_jsx(Box, { marginTop: 1, onClick: onClick, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), backgroundColor: hovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Divider, { title: title }) }));
839
+ }
840
+ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, durationMs, time, addMargin, isSelected, isExpanded, expanded, model, diffLayout, thinkingFold, toolBackground, activityFrames, background, toolCallId, toolName, toolArgsText, toolArgsFull, toolStatus, toolResultText, toolResultFull, toolErrorText, toolFootnote, toolCallView, toolResultView, toolStartedAt, toolDurationMs, subagent, onToggleRow, onToggleStreamFold, streamFolded, onOpenSubagent, onOpenFile, setRowRef, }) {
342
841
  const ref = React.useCallback((el) => {
343
842
  setRowRef(rowId, el);
344
843
  }, [setRowRef, rowId]);
345
- const onClick = React.useCallback(() => {
844
+ // 可折叠行(工具卡/思考/compact 摘要)共用:点击切换展开,全宽行右侧
845
+ // 空白(屏幕缓冲未写入单元格)不触发——点击空白想选字/拖拽时不再误触
846
+ // 展开/收起(审计 C-03/cellIsBlank 零消费)。纯文本行(user/assistant)
847
+ // 保持不可点:转录是阅读区(用户反馈),折叠语义留给带视觉指示的行。
848
+ const foldOnClick = React.useCallback((event) => {
849
+ if (event.cellIsBlank)
850
+ return;
346
851
  onToggleRow(rowId);
347
852
  }, [onToggleRow, rowId]);
853
+ // 流式 reasoning 行:点击折叠/展开 live 视图(默认展开,与落定行的
854
+ // 默认折叠相反——所以走独立开关,落定后语义自动回到 foldOnClick)。
855
+ const streamFoldOnClick = React.useCallback((event) => {
856
+ if (event.cellIsBlank)
857
+ return;
858
+ onToggleStreamFold(rowId);
859
+ }, [onToggleStreamFold, rowId]);
860
+ // 子代理卡:点击打开详情场景(不是折叠)。
861
+ const openSubagent = React.useCallback(() => {
862
+ if (subagent !== undefined)
863
+ onOpenSubagent?.(subagent.agentId);
864
+ }, [onOpenSubagent, subagent]);
865
+ // compact 摘要折叠行 hover 轻指示(∴ 提亮,不刷背景)。
866
+ const [compactHovered, setCompactHovered] = useState(false);
348
867
  switch (kind) {
349
868
  case 'user':
350
- return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(UserPromptMessage, { text: text, addMargin: addMargin, isSelected: isSelected, onClick: onClick }) }));
869
+ return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(UserPromptMessage, { text: text, addMargin: addMargin, isSelected: isSelected }) }));
351
870
  case 'assistant':
352
- return streaming ? (_jsxs(Box, { alignItems: "flex-start", flexDirection: "row", marginTop: addMargin ? 1 : 0, width: "100%", backgroundColor: background, ref: ref, children: [_jsx(Box, { minWidth: 2, children: _jsx(Text, { color: "text", children: "\u25CF" }) }), _jsx(Box, { flexDirection: "column", children: _jsx(StreamingMarkdown, { children: stripNarration(text) }) })] })) : (_jsxs(Box, { width: "100%", flexDirection: "column", backgroundColor: background, ref: ref, children: [expanded && (_jsx(Box, { flexDirection: "row", justifyContent: "flex-end", gap: 1, marginTop: 1, children: _jsx(MessageMetadata, { timestamp: time, model: model }) })), _jsx(AssistantTextMessage, { text: stripNarration(text), addMargin: addMargin, isSelected: isSelected, isExpanded: isExpanded, onClick: onClick })] }));
871
+ return streaming ? (_jsxs(Box, { alignItems: "flex-start", flexDirection: "row", marginTop: addMargin ? 1 : 0, width: "100%", backgroundColor: background, ref: ref, children: [_jsx(Box, { minWidth: 2, children: _jsx(Text, { color: "text", children: "\u25CF" }) }), _jsx(Box, { flexDirection: "column", children: _jsx(StreamingMarkdown, { children: stripNarration(text) }) })] })) : (_jsxs(Box, { width: "100%", flexDirection: "column", backgroundColor: background, ref: ref, children: [expanded && (_jsx(Box, { flexDirection: "row", justifyContent: "flex-end", gap: 1, marginTop: 1, children: _jsx(MessageMetadata, { timestamp: time, model: model }) })), _jsx(AssistantTextMessage, { text: stripNarration(text), addMargin: addMargin, isSelected: isSelected, isExpanded: isExpanded })] }));
353
872
  case 'reasoning':
354
873
  return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantThinkingMessage, { thinking: text, addMargin: addMargin, streaming: streaming, preview: streaming &&
874
+ !streamFolded &&
355
875
  thinkingFold === 'preview' &&
356
- !expanded &&
357
876
  !isExpanded,
358
- // Streaming reasoning shows expanded live, then folds
359
- // automatically once the turn settles (unless Ctrl+O or a
360
- // single-row expansion keeps it open).
361
- verbose: isExpanded || expanded || streaming, durationMs: durationMs, isSelected: isSelected, onClick: onClick }) }));
877
+ // Streaming reasoning shows expanded live (click collapses to the
878
+ // ticker/header via streamFolded); settled rows keep the
879
+ // fold-on-settle default and expand via expandedRows/Ctrl+O.
880
+ verbose: isExpanded || expanded || (streaming && !streamFolded), durationMs: durationMs, isSelected: isSelected, onClick: streaming ? streamFoldOnClick : foldOnClick }) }));
362
881
  case 'tool': {
363
882
  if (toolCallId === undefined ||
364
883
  toolName === undefined ||
@@ -383,7 +902,7 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
383
902
  startedAt: toolStartedAt,
384
903
  durationMs: toolDurationMs,
385
904
  };
386
- return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantToolUseMessage, { tool: tool, addMargin: addMargin, verbose: isExpanded || expanded, isSelected: isSelected, isExpanded: isExpanded, footnote: toolFootnote, diffLayout: diffLayout, toolBackground: toolBackground }) }));
905
+ return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantToolUseMessage, { tool: tool, addMargin: addMargin, verbose: isExpanded || expanded, isSelected: isSelected, isExpanded: isExpanded, footnote: toolFootnote, diffLayout: diffLayout, toolBackground: toolBackground, onClick: foldOnClick, onOpenFile: onOpenFile }) }));
387
906
  }
388
907
  case 'notice':
389
908
  return (_jsx(Box, { marginTop: 1, ref: ref, children: _jsx(Divider, { title: ` ${text} ` }) }));
@@ -396,13 +915,13 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
396
915
  return (_jsx(Box, { paddingLeft: 2, backgroundColor: background, ref: ref, children: _jsx(Text, { dimColor: true, children: text }) }));
397
916
  case 'compact':
398
917
  // The post-compaction summary defaults to a folded one-liner with a
399
- // text preview; Ctrl+O (global) or message-selection Enter reveals
400
- // the full summary.
401
- return (_jsx(Box, { marginTop: addMargin ? 1 : 0, paddingLeft: 2, backgroundColor: background, ref: ref, onClick: onClick, children: expanded || isExpanded ? (_jsx(Text, { dimColor: true, children: text })) : (_jsxs(Text, { dimColor: true, italic: true, children: ["\u2234 ", t('compact-summary-folded'), " \u00B7 ", compactPreview(text), ' ', t('hint-expand-ctrl-o')] })) }));
918
+ // text preview; Ctrl+O (global), message-selection Enter, or a click
919
+ // reveals the full summary.
920
+ return (_jsx(Box, { marginTop: addMargin ? 1 : 0, paddingLeft: 2, backgroundColor: background, ref: ref, onClick: foldOnClick, onMouseEnter: () => setCompactHovered(true), onMouseLeave: () => setCompactHovered(false), children: expanded || isExpanded ? (_jsx(Text, { dimColor: true, children: text })) : (_jsxs(Text, { dimColor: true, italic: true, color: compactHovered ? 'text' : undefined, children: [_jsx(Text, { color: compactHovered ? 'text' : undefined, children: "\u2234" }), ' ', t('compact-summary-folded'), " \u00B7 ", compactPreview(text), ' ', t('hint-expand-ctrl-o')] })) }));
402
921
  case 'subagent':
403
922
  if (!subagent)
404
923
  return null;
405
- return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(SubagentMessage, { subagent: subagent, addMargin: addMargin, activityFrames: activityFrames, isExpanded: isExpanded, onClick: () => onToggleRow(rowId) }) }));
924
+ return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(SubagentMessage, { subagent: subagent, addMargin: addMargin, activityFrames: activityFrames, isExpanded: isExpanded, onClick: openSubagent }) }));
406
925
  }
407
926
  }
408
927
  /** Folded compact-summary preview: whitespace flattened, capped with an
@@ -419,10 +938,10 @@ const MemoRow = React.memo(TranscriptRow);
419
938
  * welcome line. It scrolls away with the transcript once the conversation
420
939
  * fills the viewport (Claude Code shows its ✦ logo in the same slot).
421
940
  */
422
- export function LogoHeader({ model, effort, cwd, whale = true, }) {
941
+ export function LogoHeader({ model, effort, cwd, whale = true, skipIntro = false, }) {
423
942
  // Minimal mode drops the whole splash (whale art AND wordmark) — only the
424
943
  // transcript and a bare status bar remain.
425
944
  if (isMinimalMode())
426
945
  return null;
427
- return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(LogoV2, { model: model, effort: effort, cwd: cwd, whale: whale }) }));
946
+ return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(LogoV2, { model: model, effort: effort, cwd: cwd, whale: whale, skipIntro: skipIntro }) }));
428
947
  }