@deepseek-harness-tui/dsh-tui 0.11.1 → 0.11.2

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 (427) hide show
  1. package/README.md +50 -20
  2. package/assets/whale-girl/whale-girl-dark.png +0 -0
  3. package/assets/whale-girl/whale-girl-happy.png +0 -0
  4. package/assets/whale-girl/whale-girl.png +0 -0
  5. package/bin/dsh-tui.js +63 -3
  6. package/cordis.patch.yml +39 -17
  7. package/cordis.yml +3 -2
  8. package/lib/settings.json +1272 -0
  9. package/lib/types/adapter/channel/ui-policy.d.ts +9 -2
  10. package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
  11. package/lib/types/adapter/channel/ui-policy.js +13 -0
  12. package/lib/types/adapter/kernel/replay.d.ts.map +1 -1
  13. package/lib/types/adapter/kernel/replay.js +2 -0
  14. package/lib/types/adapter/ports/channel-display.d.ts +12 -0
  15. package/lib/types/adapter/ports/channel-display.d.ts.map +1 -1
  16. package/lib/types/adapter/ports/channel-session.d.ts +38 -0
  17. package/lib/types/adapter/ports/channel-session.d.ts.map +1 -1
  18. package/lib/types/adapter/ports/channel-settings.d.ts +12 -0
  19. package/lib/types/adapter/ports/channel-settings.d.ts.map +1 -1
  20. package/lib/types/adapter/ports/channel-ui.d.ts +46 -4
  21. package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
  22. package/lib/types/adapter/ports/channel-view.d.ts +43 -0
  23. package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
  24. package/lib/types/commands.d.ts.map +1 -1
  25. package/lib/types/commands.js +2 -0
  26. package/lib/types/components/BalanceReportRow.d.ts +10 -2
  27. package/lib/types/components/BalanceReportRow.d.ts.map +1 -1
  28. package/lib/types/components/BalanceReportRow.js +18 -4
  29. package/lib/types/components/CompactionStatusRow.d.ts +24 -0
  30. package/lib/types/components/CompactionStatusRow.d.ts.map +1 -0
  31. package/lib/types/components/CompactionStatusRow.js +72 -0
  32. package/lib/types/components/GoalTodoPanel.js +2 -2
  33. package/lib/types/components/HelpMenu.d.ts +2 -2
  34. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  35. package/lib/types/components/HelpMenu.js +5 -4
  36. package/lib/types/components/InlineMathParagraph.d.ts +22 -0
  37. package/lib/types/components/InlineMathParagraph.d.ts.map +1 -0
  38. package/lib/types/components/InlineMathParagraph.js +178 -0
  39. package/lib/types/components/LogoV2.d.ts +41 -3
  40. package/lib/types/components/LogoV2.d.ts.map +1 -1
  41. package/lib/types/components/LogoV2.js +148 -24
  42. package/lib/types/components/Markdown.d.ts +10 -3
  43. package/lib/types/components/Markdown.d.ts.map +1 -1
  44. package/lib/types/components/Markdown.js +47 -14
  45. package/lib/types/components/MathBlock.d.ts +13 -0
  46. package/lib/types/components/MathBlock.d.ts.map +1 -0
  47. package/lib/types/components/MathBlock.js +136 -0
  48. package/lib/types/components/MessageList.d.ts +10 -1
  49. package/lib/types/components/MessageList.d.ts.map +1 -1
  50. package/lib/types/components/MessageList.js +13 -4
  51. package/lib/types/components/MigratePicker.d.ts +30 -0
  52. package/lib/types/components/MigratePicker.d.ts.map +1 -0
  53. package/lib/types/components/MigratePicker.js +38 -0
  54. package/lib/types/components/PromptInput.d.ts +17 -0
  55. package/lib/types/components/PromptInput.d.ts.map +1 -1
  56. package/lib/types/components/PromptInput.js +48 -7
  57. package/lib/types/components/SplitDiffView.d.ts.map +1 -1
  58. package/lib/types/components/SplitDiffView.js +2 -1
  59. package/lib/types/components/StarPrompt.d.ts +53 -0
  60. package/lib/types/components/StarPrompt.d.ts.map +1 -0
  61. package/lib/types/components/StarPrompt.js +281 -0
  62. package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
  63. package/lib/types/components/StreamingMarkdown.js +20 -3
  64. package/lib/types/components/TimelineRail.d.ts.map +1 -1
  65. package/lib/types/components/TimelineRail.js +9 -1
  66. package/lib/types/components/Whale.d.ts +17 -8
  67. package/lib/types/components/Whale.d.ts.map +1 -1
  68. package/lib/types/components/Whale.js +25 -15
  69. package/lib/types/components/WhaleGirl.d.ts +29 -0
  70. package/lib/types/components/WhaleGirl.d.ts.map +1 -0
  71. package/lib/types/components/WhaleGirl.js +32 -0
  72. package/lib/types/components/WorkingSpinner.d.ts +4 -1
  73. package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
  74. package/lib/types/components/WorkingSpinner.js +2 -2
  75. package/lib/types/components/bigfont.d.ts +47 -18
  76. package/lib/types/components/bigfont.d.ts.map +1 -1
  77. package/lib/types/components/bigfont.js +55 -40
  78. package/lib/types/components/design-system/ThemeProvider.d.ts +6 -0
  79. package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
  80. package/lib/types/components/design-system/ThemeProvider.js +40 -1
  81. package/lib/types/components/maidPortrait.d.ts +44 -0
  82. package/lib/types/components/maidPortrait.d.ts.map +1 -0
  83. package/lib/types/components/maidPortrait.js +178 -0
  84. package/lib/types/components/mathPreview.d.ts +25 -0
  85. package/lib/types/components/mathPreview.d.ts.map +1 -0
  86. package/lib/types/components/mathPreview.js +48 -0
  87. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  88. package/lib/types/components/messages/AssistantThinkingMessage.js +2 -1
  89. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  90. package/lib/types/components/messages/AssistantToolUseMessage.js +3 -2
  91. package/lib/types/components/messages/TranscriptImages.d.ts.map +1 -1
  92. package/lib/types/components/messages/TranscriptImages.js +11 -1
  93. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +14 -2
  94. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  95. package/lib/types/components/questions/AskUserQuestionPanel.js +80 -38
  96. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  97. package/lib/types/components/questions/PlanReviewPanel.js +3 -3
  98. package/lib/types/components/sessions/ForeignSessionRow.d.ts +34 -0
  99. package/lib/types/components/sessions/ForeignSessionRow.d.ts.map +1 -0
  100. package/lib/types/components/sessions/ForeignSessionRow.js +43 -0
  101. package/lib/types/components/sessions/SourceTabs.d.ts +52 -0
  102. package/lib/types/components/sessions/SourceTabs.d.ts.map +1 -0
  103. package/lib/types/components/sessions/SourceTabs.js +70 -0
  104. package/lib/types/components/splashEggs.d.ts +56 -0
  105. package/lib/types/components/splashEggs.d.ts.map +1 -0
  106. package/lib/types/components/splashEggs.js +67 -0
  107. package/lib/types/components/splashFonts.d.ts +119 -0
  108. package/lib/types/components/splashFonts.d.ts.map +1 -0
  109. package/lib/types/components/splashFonts.js +267 -0
  110. package/lib/types/components/splashLayout.d.ts +41 -0
  111. package/lib/types/components/splashLayout.d.ts.map +1 -0
  112. package/lib/types/components/splashLayout.js +40 -0
  113. package/lib/types/components/whaleGirlSprite.d.ts +17 -0
  114. package/lib/types/components/whaleGirlSprite.d.ts.map +1 -0
  115. package/lib/types/components/whaleGirlSprite.js +47 -0
  116. package/lib/types/deepseekPricing.d.ts +74 -0
  117. package/lib/types/deepseekPricing.d.ts.map +1 -1
  118. package/lib/types/deepseekPricing.js +110 -11
  119. package/lib/types/dsh-adapter/channel/action-readiness.d.ts +1 -1
  120. package/lib/types/dsh-adapter/channel/action-readiness.d.ts.map +1 -1
  121. package/lib/types/dsh-adapter/channel/action-readiness.js +6 -1
  122. package/lib/types/dsh-adapter/channel/background-action.d.ts +1 -0
  123. package/lib/types/dsh-adapter/channel/background-action.d.ts.map +1 -1
  124. package/lib/types/dsh-adapter/channel/background-action.js +1 -0
  125. package/lib/types/dsh-adapter/channel/binding-events.d.ts +8 -9
  126. package/lib/types/dsh-adapter/channel/binding-events.d.ts.map +1 -1
  127. package/lib/types/dsh-adapter/channel/binding-events.js +69 -21
  128. package/lib/types/dsh-adapter/channel/compaction.d.ts +2 -1
  129. package/lib/types/dsh-adapter/channel/compaction.d.ts.map +1 -1
  130. package/lib/types/dsh-adapter/channel/compaction.js +35 -2
  131. package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
  132. package/lib/types/dsh-adapter/channel/local-actions.js +6 -1
  133. package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
  134. package/lib/types/dsh-adapter/channel/model-switch.js +5 -1
  135. package/lib/types/dsh-adapter/channel/paths.d.ts.map +1 -1
  136. package/lib/types/dsh-adapter/channel/paths.js +7 -1
  137. package/lib/types/dsh-adapter/channel/preferences.d.ts +1 -1
  138. package/lib/types/dsh-adapter/channel/preferences.d.ts.map +1 -1
  139. package/lib/types/dsh-adapter/channel/preferences.js +16 -0
  140. package/lib/types/dsh-adapter/channel/projection.d.ts +1 -2
  141. package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
  142. package/lib/types/dsh-adapter/channel/projection.js +127 -17
  143. package/lib/types/dsh-adapter/channel/question-record.d.ts +76 -0
  144. package/lib/types/dsh-adapter/channel/question-record.d.ts.map +1 -0
  145. package/lib/types/dsh-adapter/channel/question-record.js +107 -0
  146. package/lib/types/dsh-adapter/channel/session-lineage.d.ts +42 -0
  147. package/lib/types/dsh-adapter/channel/session-lineage.d.ts.map +1 -0
  148. package/lib/types/dsh-adapter/channel/session-lineage.js +51 -0
  149. package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts +2 -0
  150. package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts.map +1 -1
  151. package/lib/types/dsh-adapter/channel/session-live-adoption.js +9 -3
  152. package/lib/types/dsh-adapter/channel/session-metadata.d.ts +2 -1
  153. package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
  154. package/lib/types/dsh-adapter/channel/session-metadata.js +39 -7
  155. package/lib/types/dsh-adapter/channel/session-reset.d.ts +1 -1
  156. package/lib/types/dsh-adapter/channel/session-reset.d.ts.map +1 -1
  157. package/lib/types/dsh-adapter/channel/session-reset.js +7 -0
  158. package/lib/types/dsh-adapter/channel/session-resume.d.ts +1 -0
  159. package/lib/types/dsh-adapter/channel/session-resume.d.ts.map +1 -1
  160. package/lib/types/dsh-adapter/channel/session-resume.js +4 -2
  161. package/lib/types/dsh-adapter/channel/settings-host.d.ts.map +1 -1
  162. package/lib/types/dsh-adapter/channel/settings-host.js +8 -0
  163. package/lib/types/dsh-adapter/channel/state.d.ts +14 -1
  164. package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
  165. package/lib/types/dsh-adapter/channel/state.js +6 -1
  166. package/lib/types/dsh-adapter/channel/subagent-projection.d.ts +30 -35
  167. package/lib/types/dsh-adapter/channel/subagent-projection.d.ts.map +1 -1
  168. package/lib/types/dsh-adapter/channel/subagent-projection.js +138 -4
  169. package/lib/types/dsh-adapter/channel/types.d.ts +1 -1
  170. package/lib/types/dsh-adapter/channel/types.d.ts.map +1 -1
  171. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  172. package/lib/types/dsh-adapter/channel.js +19 -4
  173. package/lib/types/dsh-adapter/compat/messages.d.ts +6 -0
  174. package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -1
  175. package/lib/types/dsh-adapter/compat/messages.js +9 -0
  176. package/lib/types/dsh-adapter/contract.d.ts +5 -5
  177. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  178. package/lib/types/dsh-adapter/contract.js +8 -6
  179. package/lib/types/dsh-adapter/index.d.ts +36 -1
  180. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  181. package/lib/types/dsh-adapter/index.js +22 -6
  182. package/lib/types/dsh-adapter/migrate/adapters/claude-code.d.ts +3 -0
  183. package/lib/types/dsh-adapter/migrate/adapters/claude-code.d.ts.map +1 -0
  184. package/lib/types/dsh-adapter/migrate/adapters/claude-code.js +131 -0
  185. package/lib/types/dsh-adapter/migrate/adapters/claude-code.parse.d.ts +17 -0
  186. package/lib/types/dsh-adapter/migrate/adapters/claude-code.parse.d.ts.map +1 -0
  187. package/lib/types/dsh-adapter/migrate/adapters/claude-code.parse.js +329 -0
  188. package/lib/types/dsh-adapter/migrate/adapters/codex.d.ts +3 -0
  189. package/lib/types/dsh-adapter/migrate/adapters/codex.d.ts.map +1 -0
  190. package/lib/types/dsh-adapter/migrate/adapters/codex.js +102 -0
  191. package/lib/types/dsh-adapter/migrate/adapters/codex.parse.d.ts +16 -0
  192. package/lib/types/dsh-adapter/migrate/adapters/codex.parse.d.ts.map +1 -0
  193. package/lib/types/dsh-adapter/migrate/adapters/codex.parse.js +289 -0
  194. package/lib/types/dsh-adapter/migrate/adapters/grok-build.d.ts +3 -0
  195. package/lib/types/dsh-adapter/migrate/adapters/grok-build.d.ts.map +1 -0
  196. package/lib/types/dsh-adapter/migrate/adapters/grok-build.js +117 -0
  197. package/lib/types/dsh-adapter/migrate/adapters/grok-build.parse.d.ts +13 -0
  198. package/lib/types/dsh-adapter/migrate/adapters/grok-build.parse.d.ts.map +1 -0
  199. package/lib/types/dsh-adapter/migrate/adapters/grok-build.parse.js +178 -0
  200. package/lib/types/dsh-adapter/migrate/adapters/omp.d.ts +3 -0
  201. package/lib/types/dsh-adapter/migrate/adapters/omp.d.ts.map +1 -0
  202. package/lib/types/dsh-adapter/migrate/adapters/omp.js +133 -0
  203. package/lib/types/dsh-adapter/migrate/adapters/scan-fs.d.ts +60 -0
  204. package/lib/types/dsh-adapter/migrate/adapters/scan-fs.d.ts.map +1 -0
  205. package/lib/types/dsh-adapter/migrate/adapters/scan-fs.js +178 -0
  206. package/lib/types/dsh-adapter/migrate/adapters/scan.d.ts +8 -0
  207. package/lib/types/dsh-adapter/migrate/adapters/scan.d.ts.map +1 -0
  208. package/lib/types/dsh-adapter/migrate/adapters/scan.js +40 -0
  209. package/lib/types/dsh-adapter/migrate/adapters/zcode.d.ts +3 -0
  210. package/lib/types/dsh-adapter/migrate/adapters/zcode.d.ts.map +1 -0
  211. package/lib/types/dsh-adapter/migrate/adapters/zcode.js +93 -0
  212. package/lib/types/dsh-adapter/migrate/adapters/zcode.parse.d.ts +9 -0
  213. package/lib/types/dsh-adapter/migrate/adapters/zcode.parse.d.ts.map +1 -0
  214. package/lib/types/dsh-adapter/migrate/adapters/zcode.parse.js +89 -0
  215. package/lib/types/dsh-adapter/migrate/bin-path.d.ts +6 -0
  216. package/lib/types/dsh-adapter/migrate/bin-path.d.ts.map +1 -0
  217. package/lib/types/dsh-adapter/migrate/bin-path.js +30 -0
  218. package/lib/types/dsh-adapter/migrate/browse.d.ts +42 -0
  219. package/lib/types/dsh-adapter/migrate/browse.d.ts.map +1 -0
  220. package/lib/types/dsh-adapter/migrate/browse.js +90 -0
  221. package/lib/types/dsh-adapter/migrate/cli.d.ts +9 -0
  222. package/lib/types/dsh-adapter/migrate/cli.d.ts.map +1 -0
  223. package/lib/types/dsh-adapter/migrate/cli.js +106 -0
  224. package/lib/types/dsh-adapter/migrate/import-one.d.ts +89 -0
  225. package/lib/types/dsh-adapter/migrate/import-one.d.ts.map +1 -0
  226. package/lib/types/dsh-adapter/migrate/import-one.js +133 -0
  227. package/lib/types/dsh-adapter/migrate/index.d.ts +35 -0
  228. package/lib/types/dsh-adapter/migrate/index.d.ts.map +1 -0
  229. package/lib/types/dsh-adapter/migrate/index.js +107 -0
  230. package/lib/types/dsh-adapter/migrate/parse/injection.d.ts +40 -0
  231. package/lib/types/dsh-adapter/migrate/parse/injection.d.ts.map +1 -0
  232. package/lib/types/dsh-adapter/migrate/parse/injection.js +127 -0
  233. package/lib/types/dsh-adapter/migrate/parse/jsonl.d.ts +28 -0
  234. package/lib/types/dsh-adapter/migrate/parse/jsonl.d.ts.map +1 -0
  235. package/lib/types/dsh-adapter/migrate/parse/jsonl.js +40 -0
  236. package/lib/types/dsh-adapter/migrate/parse/role-turns.d.ts +28 -0
  237. package/lib/types/dsh-adapter/migrate/parse/role-turns.d.ts.map +1 -0
  238. package/lib/types/dsh-adapter/migrate/parse/role-turns.js +50 -0
  239. package/lib/types/dsh-adapter/migrate/parse/title.d.ts +18 -0
  240. package/lib/types/dsh-adapter/migrate/parse/title.d.ts.map +1 -0
  241. package/lib/types/dsh-adapter/migrate/parse/title.js +29 -0
  242. package/lib/types/dsh-adapter/migrate/parse/tools.d.ts +62 -0
  243. package/lib/types/dsh-adapter/migrate/parse/tools.d.ts.map +1 -0
  244. package/lib/types/dsh-adapter/migrate/parse/tools.js +110 -0
  245. package/lib/types/dsh-adapter/migrate/picker.d.ts +64 -0
  246. package/lib/types/dsh-adapter/migrate/picker.d.ts.map +1 -0
  247. package/lib/types/dsh-adapter/migrate/picker.js +86 -0
  248. package/lib/types/dsh-adapter/migrate/recent-agents.d.ts +40 -0
  249. package/lib/types/dsh-adapter/migrate/recent-agents.d.ts.map +1 -0
  250. package/lib/types/dsh-adapter/migrate/recent-agents.js +100 -0
  251. package/lib/types/dsh-adapter/migrate/sessionize.d.ts +39 -0
  252. package/lib/types/dsh-adapter/migrate/sessionize.d.ts.map +1 -0
  253. package/lib/types/dsh-adapter/migrate/sessionize.js +271 -0
  254. package/lib/types/dsh-adapter/migrate/types.d.ts +176 -0
  255. package/lib/types/dsh-adapter/migrate/types.d.ts.map +1 -0
  256. package/lib/types/dsh-adapter/migrate/types.js +11 -0
  257. package/lib/types/dsh-adapter/migrate/uuid.d.ts +7 -0
  258. package/lib/types/dsh-adapter/migrate/uuid.d.ts.map +1 -0
  259. package/lib/types/dsh-adapter/migrate/uuid.js +25 -0
  260. package/lib/types/dsh-adapter/plugin.d.ts +4 -2
  261. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  262. package/lib/types/dsh-adapter/plugin.js +143 -364
  263. package/lib/types/dsh-adapter/providerWizard.d.ts +17 -0
  264. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  265. package/lib/types/dsh-adapter/providerWizard.js +254 -66
  266. package/lib/types/dsh-adapter/questions.d.ts +22 -15
  267. package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
  268. package/lib/types/dsh-adapter/questions.js +47 -45
  269. package/lib/types/dsh-adapter/sessions/digest.d.ts +11 -6
  270. package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -1
  271. package/lib/types/dsh-adapter/sessions/digest.js +20 -13
  272. package/lib/types/dsh-adapter/sessions/frames.d.ts +3 -1
  273. package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -1
  274. package/lib/types/dsh-adapter/sessions/frames.js +5 -4
  275. package/lib/types/dsh-adapter/sessions/list.d.ts +6 -1
  276. package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -1
  277. package/lib/types/dsh-adapter/sessions/list.js +188 -164
  278. package/lib/types/dsh-adapter/sessions/recovery.d.ts +16 -0
  279. package/lib/types/dsh-adapter/sessions/recovery.d.ts.map +1 -0
  280. package/lib/types/dsh-adapter/sessions/recovery.js +108 -0
  281. package/lib/types/dsh-adapter/sessions/snapshot.d.ts +7 -0
  282. package/lib/types/dsh-adapter/sessions/snapshot.d.ts.map +1 -0
  283. package/lib/types/dsh-adapter/sessions/snapshot.js +90 -0
  284. package/lib/types/dsh-adapter/sessions/store.d.ts +10 -4
  285. package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -1
  286. package/lib/types/dsh-adapter/sessions/store.js +62 -25
  287. package/lib/types/dsh-adapter/sideQuestion.d.ts +41 -2
  288. package/lib/types/dsh-adapter/sideQuestion.d.ts.map +1 -1
  289. package/lib/types/dsh-adapter/sideQuestion.js +98 -2
  290. package/lib/types/dsh-adapter/subagents.d.ts +28 -3
  291. package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
  292. package/lib/types/dsh-adapter/subagents.js +77 -2
  293. package/lib/types/herdr.d.ts +2 -0
  294. package/lib/types/herdr.d.ts.map +1 -1
  295. package/lib/types/herdr.js +9 -3
  296. package/lib/types/i18n.d.ts +424 -42
  297. package/lib/types/i18n.d.ts.map +1 -1
  298. package/lib/types/i18n.js +145 -20
  299. package/lib/types/ink/components/App.d.ts +2 -2
  300. package/lib/types/ink/components/App.d.ts.map +1 -1
  301. package/lib/types/ink/components/App.js +5 -1
  302. package/lib/types/ink/components/Image.d.ts +14 -1
  303. package/lib/types/ink/components/Image.d.ts.map +1 -1
  304. package/lib/types/ink/components/Image.js +2 -2
  305. package/lib/types/ink/events/input-event.js +3 -3
  306. package/lib/types/ink/hooks/use-terminal-images.d.ts +12 -0
  307. package/lib/types/ink/hooks/use-terminal-images.d.ts.map +1 -1
  308. package/lib/types/ink/hooks/use-terminal-images.js +13 -0
  309. package/lib/types/ink/ink.d.ts +6 -0
  310. package/lib/types/ink/ink.d.ts.map +1 -1
  311. package/lib/types/ink/ink.js +16 -1
  312. package/lib/types/ink/kitty-graphics.d.ts +44 -3
  313. package/lib/types/ink/kitty-graphics.d.ts.map +1 -1
  314. package/lib/types/ink/kitty-graphics.js +184 -23
  315. package/lib/types/ink/output.d.ts +24 -2
  316. package/lib/types/ink/output.d.ts.map +1 -1
  317. package/lib/types/ink/output.js +83 -5
  318. package/lib/types/ink/parse-keypress.d.ts +26 -1
  319. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  320. package/lib/types/ink/parse-keypress.js +156 -13
  321. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  322. package/lib/types/ink/render-node-to-output.js +8 -1
  323. package/lib/types/ink/screen.d.ts +24 -0
  324. package/lib/types/ink/screen.d.ts.map +1 -1
  325. package/lib/types/ink/screen.js +89 -0
  326. package/lib/types/ink/selection.d.ts +40 -18
  327. package/lib/types/ink/selection.d.ts.map +1 -1
  328. package/lib/types/ink/selection.js +142 -30
  329. package/lib/types/ink/sixel-codec.d.ts +12 -1
  330. package/lib/types/ink/sixel-codec.d.ts.map +1 -1
  331. package/lib/types/ink/sixel-codec.js +29 -5
  332. package/lib/types/ink/sixel-graphics.d.ts.map +1 -1
  333. package/lib/types/ink/sixel-graphics.js +5 -3
  334. package/lib/types/ink/styles.d.ts +9 -0
  335. package/lib/types/ink/styles.d.ts.map +1 -1
  336. package/lib/types/ink/terminal-image.d.ts +8 -0
  337. package/lib/types/ink/terminal-image.d.ts.map +1 -1
  338. package/lib/types/ink/terminal-querier.d.ts +7 -1
  339. package/lib/types/ink/terminal-querier.d.ts.map +1 -1
  340. package/lib/types/ink/terminal-querier.js +12 -2
  341. package/lib/types/ink/timeline-rail.d.ts +5 -0
  342. package/lib/types/ink/timeline-rail.d.ts.map +1 -1
  343. package/lib/types/math/inline-layout.d.ts +29 -0
  344. package/lib/types/math/inline-layout.d.ts.map +1 -0
  345. package/lib/types/math/inline-layout.js +106 -0
  346. package/lib/types/math/layout.d.ts +58 -0
  347. package/lib/types/math/layout.d.ts.map +1 -0
  348. package/lib/types/math/layout.js +59 -0
  349. package/lib/types/math/renderer.d.ts +64 -0
  350. package/lib/types/math/renderer.d.ts.map +1 -0
  351. package/lib/types/math/renderer.js +256 -0
  352. package/lib/types/screens/Chat.d.ts +14 -1
  353. package/lib/types/screens/Chat.d.ts.map +1 -1
  354. package/lib/types/screens/Chat.js +606 -53
  355. package/lib/types/screens/SessionSupervisor.d.ts +2 -1
  356. package/lib/types/screens/SessionSupervisor.d.ts.map +1 -1
  357. package/lib/types/screens/SessionSupervisor.js +156 -19
  358. package/lib/types/screens/Settings.d.ts.map +1 -1
  359. package/lib/types/screens/Settings.js +49 -11
  360. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  361. package/lib/types/screens/StatusLine.js +45 -15
  362. package/lib/types/screens/chatOverlay.d.ts +5 -0
  363. package/lib/types/screens/chatOverlay.d.ts.map +1 -1
  364. package/lib/types/screens/chatOverlay.js +1 -0
  365. package/lib/types/screens/sessionSupervisor/ForeignSessionPanes.d.ts +47 -0
  366. package/lib/types/screens/sessionSupervisor/ForeignSessionPanes.d.ts.map +1 -0
  367. package/lib/types/screens/sessionSupervisor/ForeignSessionPanes.js +65 -0
  368. package/lib/types/screens/sessionSupervisor/model.d.ts +12 -0
  369. package/lib/types/screens/sessionSupervisor/model.d.ts.map +1 -1
  370. package/lib/types/screens/sessionSupervisor/model.js +26 -0
  371. package/lib/types/screens/sessionSupervisor/useForeignSessions.d.ts +107 -0
  372. package/lib/types/screens/sessionSupervisor/useForeignSessions.d.ts.map +1 -0
  373. package/lib/types/screens/sessionSupervisor/useForeignSessions.js +312 -0
  374. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts +4 -1
  375. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts.map +1 -1
  376. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.js +97 -14
  377. package/lib/types/sessionHistory.d.ts +2 -2
  378. package/lib/types/sessionHistory.d.ts.map +1 -1
  379. package/lib/types/sessionHistory.js +31 -5
  380. package/lib/types/settings/definitions.d.ts +681 -0
  381. package/lib/types/settings/definitions.d.ts.map +1 -0
  382. package/lib/types/settings/definitions.js +511 -0
  383. package/lib/types/starAction.d.ts +44 -0
  384. package/lib/types/starAction.d.ts.map +1 -0
  385. package/lib/types/starAction.js +52 -0
  386. package/lib/types/terminal-utils/latex.d.ts +10 -0
  387. package/lib/types/terminal-utils/latex.d.ts.map +1 -0
  388. package/lib/types/terminal-utils/latex.js +1417 -0
  389. package/lib/types/terminal-utils/markdown.d.ts +3 -1
  390. package/lib/types/terminal-utils/markdown.d.ts.map +1 -1
  391. package/lib/types/terminal-utils/markdown.js +26 -1
  392. package/lib/types/terminal-utils/math.d.ts +66 -0
  393. package/lib/types/terminal-utils/math.d.ts.map +1 -0
  394. package/lib/types/terminal-utils/math.js +337 -0
  395. package/lib/types/tips.d.ts.map +1 -1
  396. package/lib/types/tips.js +6 -0
  397. package/lib/types/tuiDisplayPrefs.d.ts +67 -0
  398. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  399. package/lib/types/tuiDisplayPrefs.js +59 -0
  400. package/lib/types/ui.d.ts +1 -1
  401. package/lib/types/ui.d.ts.map +1 -1
  402. package/lib/types/ui.js +1 -1
  403. package/lib/types/usageStats.d.ts +98 -0
  404. package/lib/types/usageStats.d.ts.map +1 -0
  405. package/lib/types/usageStats.js +169 -0
  406. package/lib/types/utils/bmp.d.ts +17 -0
  407. package/lib/types/utils/bmp.d.ts.map +1 -0
  408. package/lib/types/utils/bmp.js +154 -0
  409. package/lib/types/utils/clipboard.d.ts +14 -2
  410. package/lib/types/utils/clipboard.d.ts.map +1 -1
  411. package/lib/types/utils/clipboard.js +143 -8
  412. package/lib/types/utils/fold-long-lines.d.ts.map +1 -1
  413. package/lib/types/utils/fold-long-lines.js +2 -1
  414. package/lib/types/utils/keymap.d.ts +4 -1
  415. package/lib/types/utils/keymap.d.ts.map +1 -1
  416. package/lib/types/utils/keymap.js +8 -0
  417. package/lib/types/utils/modifiers.d.ts +4 -3
  418. package/lib/types/utils/modifiers.d.ts.map +1 -1
  419. package/lib/types/utils/modifiers.js +4 -3
  420. package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/LICENSE +202 -0
  421. package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/NOTICE +20 -0
  422. package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/index.d.ts +16 -0
  423. package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/lib/bundled-packages.json +4 -0
  424. package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/lib/index.mjs +20 -0
  425. package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/lib/index.mjs.LEGAL.txt +35 -0
  426. package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/package.json +29 -0
  427. package/package.json +111 -97
@@ -11,16 +11,25 @@ import { planReload } from '../reload.js';
11
11
  import { AlternateScreen, Box, Image, Text, useInput, ScrollBox, useTheme, useTerminalSize } from '../ui.js';
12
12
  import { usePageInset } from '../components/PageMargin.js';
13
13
  import { POINTER } from '../terminal-utils/figures.js';
14
- import { isPlainReturnInput, modLabel } from '../utils/modifiers.js';
15
- import { actionMatches } from '../utils/keymap.js';
14
+ import { isPlainReturnInput } from '../utils/modifiers.js';
15
+ import { actionMatches, effectiveComboDisplay, primaryComboString } from '../utils/keymap.js';
16
16
  import { formatTokens } from '../terminal-utils/format.js';
17
17
  import { homeDir } from '../utils/paths.js';
18
+ import { execFileNoThrow } from '../utils/execFileNoThrow.js';
18
19
  import { cleanRenderText, cleanScalarText } from '../dsh-adapter/sanitize.js';
19
20
  import { deriveModelGroups, modelPickerLanding, recentCatalogModels, RECENTS_GROUP_PROVIDER, } from '../modelGroups.js';
20
21
  import { readModelRecents, recordModelUse } from '../modelRecents.js';
21
22
  import { TuiDialogStore } from '../dsh-adapter/dialogs.js';
22
23
  import { TuiStatusStore } from '../dsh-adapter/status.js';
23
24
  import { ActivityStore, useActivity } from '../dsh-adapter/activity-store.js';
25
+ import { rasterToPng, setMathPreviewOpener } from '../components/mathPreview.js';
26
+ import { renderMathRaster } from '../math/renderer.js';
27
+ /** Formula preview rasters are re-typeset at this cell scale (2× the
28
+ * transcript's) so the card's 100–800% zoom stays sharp instead of upscaling
29
+ * the inline pixels. Bounded like the other image budgets. */
30
+ const MATH_PREVIEW_SCALE = 2;
31
+ const MATH_PREVIEW_MAX_COLUMNS = 480;
32
+ const MATH_PREVIEW_MAX_ROWS = 64;
24
33
  import { runProviderWizard } from '../dsh-adapter/providerWizard.js';
25
34
  import { ApprovalStore } from '../dsh-adapter/approvals.js';
26
35
  import { AskUserQuestionPanel } from '../components/questions/AskUserQuestionPanel.js';
@@ -33,6 +42,9 @@ import { useCopyOnSelect } from '../ink/hooks/use-copy-on-select.js';
33
42
  import { useSelection } from '../ink/hooks/use-selection.js';
34
43
  import { NoSelect } from '../ink/components/NoSelect.js';
35
44
  import { LogoHeader, MessageList } from '../components/MessageList.js';
45
+ import { splashFontIdOf } from '../components/splashFonts.js';
46
+ import { StarPrompt } from '../components/StarPrompt.js';
47
+ import { dueStarModal, markStarAsked, STAR_MILESTONES } from '../usageStats.js';
36
48
  import { TimelineRail } from '../components/TimelineRail.js';
37
49
  import { ScrollbarGutter } from '../components/ScrollbarGutter.js';
38
50
  import { normalizeScrollGutter } from '../tuiDisplayPrefs.js';
@@ -42,7 +54,9 @@ import { PromptInput } from '../components/PromptInput.js';
42
54
  import { PromptEditorLayer, usePromptEditorOpen } from '../components/PromptEditor.js';
43
55
  import { GoalTodoPanel } from '../components/GoalTodoPanel.js';
44
56
  import { AutoRecapRow } from '../components/AutoRecapRow.js';
57
+ import { CompactionStatusRow } from '../components/CompactionStatusRow.js';
45
58
  import { BalanceReportRow } from '../components/BalanceReportRow.js';
59
+ import { estimateSessionCostSnapshotCny } from '../deepseekPricing.js';
46
60
  import { LoadedContextPanel } from '../components/LoadedContextPanel.js';
47
61
  import { StatusLine } from './StatusLine.js';
48
62
  import { WorkingSpinner, useThinkingStatus } from '../components/WorkingSpinner.js';
@@ -52,6 +66,10 @@ import { PluginSceneBoundary } from '../components/PluginSceneBoundary.js';
52
66
  import { PluginStatusViewBoundary } from '../components/PluginStatusViewBoundary.js';
53
67
  import { ImagePreviewOverlay } from '../components/ImagePreviewOverlay.js';
54
68
  import { SkillsPicker, SkillsPickerLoading } from '../components/SkillsPicker.js';
69
+ import { MigrateConfirm, MigratePicker } from '../components/MigratePicker.js';
70
+ import { collectMigratePickerRows, MIGRATE_SCAN_SPECS, parseImportSummary, resolveMigrateCommand } from '../dsh-adapter/migrate/picker.js';
71
+ import { collectActivitySamples, recentAgentsFrom } from '../dsh-adapter/migrate/recent-agents.js';
72
+ import { MIGRATION_ADAPTERS } from '../dsh-adapter/migrate/index.js';
55
73
  import { SessionSupervisor } from './SessionSupervisor.js';
56
74
  import { SessionTree } from './SessionTree.js';
57
75
  import { Settings } from './Settings.js';
@@ -89,7 +107,6 @@ import instances from '../ink/instances.js';
89
107
  import { useAnimationFrame } from '../ink/hooks/use-animation-frame.js';
90
108
  import { useExternalVersion } from '../hooks/useExternalVersion.js';
91
109
  import { TrajectoryScene } from './TrajectoryScene.js';
92
- import { resumeFailureText } from '../sessions/resumeFailure.js';
93
110
  import { markHomeSeen } from '../homePrefs.js';
94
111
  import { extendTrajectory, projectWave } from '../dsh-adapter/trajectory/index.js';
95
112
  import { miniWakeWidth } from '../components/trajectory/MiniWake.js';
@@ -119,6 +136,10 @@ const STATUS_VIEW_UI = Object.freeze({
119
136
  /** Shared empty snapshot for hosts whose channel has no event log. */
120
137
  const NO_EVENTS = [];
121
138
  const COMMAND_RESULT_CELLS = 200;
139
+ /** Ceiling for one `dsh-tui migrate` child run. Discovery parses every source
140
+ * file, but a healthy import of thousands of conversations finishes well
141
+ * inside this; without a cap a wedged child would hang the loop forever. */
142
+ const MIGRATE_CHILD_TIMEOUT_MS = 30 * 60 * 1000;
122
143
  function cleanCommandError(error) {
123
144
  try {
124
145
  if (error instanceof Error) {
@@ -207,7 +228,7 @@ let fallbackActivityStore;
207
228
  function peekKey(image, title) {
208
229
  return `${title ?? ''} ${image.id}`;
209
230
  }
210
- export function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, activityStore, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen = false, trajectorySeen: trajectorySeenProp, injectControllerRef, promptControllerRef: promptControllerRefProp, renderScene, openHomeOnBoot, }) {
231
+ export function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, activityStore, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen = false, trajectorySeen: trajectorySeenProp, injectControllerRef, promptControllerRef: promptControllerRefProp, renderScene, openHomeOnBoot, starPrompt, }) {
211
232
  const writeRaw = React.useContext(TerminalWriteContext);
212
233
  // Re-render whenever the channel mutates; rows/status are read fresh below.
213
234
  // DEFAULT lane on purpose (useExternalVersion): the channel version bumps
@@ -258,18 +279,6 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
258
279
  channel.notify(t('ext-shortcut-failed', { combo }), { color: 'error', timeoutMs: 4000 });
259
280
  });
260
281
  }, [extensionShortcuts, channel]);
261
- // When a questionnaire batch completes, fold a Q&A summary into the
262
- // transcript (the tool card itself is hidden from the message list).
263
- const questionOpenRef = React.useRef(questionSnapshot !== null);
264
- React.useEffect(() => {
265
- const wasOpen = questionOpenRef.current;
266
- questionOpenRef.current = questionSnapshot !== null;
267
- if (wasOpen && questionSnapshot === null) {
268
- for (const summary of questionStore.takeSummaries()) {
269
- channel.pushLocal(summary.title, summary.lines);
270
- }
271
- }
272
- }, [channel, questionSnapshot, questionStore]);
273
282
  const [expanded, setExpanded] = React.useState(false);
274
283
  const [helpOpen, setHelpOpen] = React.useState(false);
275
284
  const [handle, setHandle] = React.useState(null);
@@ -285,6 +294,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
285
294
  const [timeline, setTimeline] = React.useState({
286
295
  turns: [],
287
296
  activeId: null,
297
+ pinnedId: null,
288
298
  upId: null,
289
299
  downId: null,
290
300
  });
@@ -306,6 +316,17 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
306
316
  * list while the fresh one loads, exactly as the boolean era did.
307
317
  */
308
318
  const [overlay, dispatchOverlay] = React.useReducer(chatOverlayReducer, NO_OVERLAY);
319
+ // `/migrate` picker rows (null = collecting in the background; the picker
320
+ // shows its empty state until the sub-second scan lands).
321
+ const [migrateRows, setMigrateRows] = React.useState(null);
322
+ // Multi-select state (PRD): checked agent ids + the confirmation layer's
323
+ // frozen snapshot of the checked rows.
324
+ const [migrateChecked, setMigrateChecked] = React.useState(new Set());
325
+ const [migratePending, setMigratePending] = React.useState([]);
326
+ // Smart-hint arming: while the migration hint notification is up, a bare
327
+ // Enter (empty prompt, no overlay) jumps straight into the picker with
328
+ // that source pre-checked (PRD #4). Any other key disarms.
329
+ const [migrateHintAgent, setMigrateHintAgent] = React.useState(null);
309
330
  // Chat and PromptInput both receive one parsed stdin batch. Keep the
310
331
  // permission focus synchronous so arrow+Enter in the same batch uses the
311
332
  // post-arrow row rather than the previous render's index.
@@ -418,6 +439,152 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
418
439
  * browser, a screen rather than a panel: it owns its own focus, staged
419
440
  * drafts and keyboard; Chat only opens it. */
420
441
  const [settingsOpen, setSettingsOpen] = React.useState(false);
442
+ /** 99h / 999 次的"求 star"开屏弹窗(`usageStats` 记账,一档只弹一次):
443
+ * 只在启动时判定一次——回合进行中、或已有整屏界面在开(如开机首页),
444
+ * 这一轮不弹也**不记账**,留给下一次启动。`starPrompt` 是测试缝:传
445
+ * `null` 显式关闭,传 actions 覆写两个按钮(不跑真 gh、不开真浏览器)。 */
446
+ /** 本次会话是否已经 star 成功(开屏彩蛋标题切「捡到小星星啦」)。 */
447
+ const [starred, setStarred] = React.useState(false);
448
+ const [starModal, setStarModal] = React.useState(null);
449
+ // 单发闩:只在第一个"安静的开屏视口"上武装定时器。700ms 窗口内整屏
450
+ // 界面打开 → cleanup 掐掉定时器且**不再重臂**(记账只发生在回调里,
451
+ // 所以这一档完好留给下一次启动);整屏界面随后关闭也不追到聊天视图
452
+ // 上补弹——开屏求星不追人。
453
+ const starModalArmedRef = React.useRef(false);
454
+ /** 预览缝(`DSH_TUI_STAR_MODAL=1`):启动即弹一次 99h 档的弹窗,**既不
455
+ * 读账本也不记账**——给作者看效果、给回归夹具用;生产不设这个变量。 */
456
+ const starModalPreview = process.env.DSH_TUI_STAR_MODAL === '1';
457
+ React.useEffect(() => {
458
+ if (starModalArmedRef.current)
459
+ return;
460
+ if (starPrompt === null)
461
+ return;
462
+ if (supervisorOpen || treeOpen || settingsOpen || channel.working)
463
+ return;
464
+ starModalArmedRef.current = true;
465
+ // 让开屏先画半秒:弹窗压在介绍动画之上,而不是同抢第一帧。
466
+ const timer = setTimeout(() => {
467
+ // 到点时回合已经开始的仍不弹(channel 是活对象,读到的是当前值)。
468
+ if (channel.working)
469
+ return;
470
+ if (starModalPreview) {
471
+ const preview = STAR_MILESTONES.findIndex(milestone => milestone.hours === 99);
472
+ if (preview >= 0)
473
+ setStarModal({ index: preview, phase: 'ask' });
474
+ return;
475
+ }
476
+ const index = dueStarModal(starPrompt?.dir);
477
+ if (index === null)
478
+ return;
479
+ markStarAsked(index, starPrompt?.dir);
480
+ setStarModal({ index, phase: 'ask' });
481
+ }, 700);
482
+ return () => { clearTimeout(timer); };
483
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- 只在整屏界面开合时重判;闩保证只武装一次
484
+ }, [supervisorOpen, treeOpen, settingsOpen]);
485
+ /** `/star` 命令、开屏标语的点击/`Alt+S` 共用的一键动作:异步跑 gh,界面
486
+ * 全程不阻塞,结果回来按四类各报一句(成功 / 没装 gh / 没登录 / 失败)。
487
+ * `starPrompt.onStar` 存在时走同一条测试缝(夹具因此不会真的去 star)。 */
488
+ /** 打开仓库页(走 `starPrompt.onOpen` 测试缝——夹具里不会真的拉起浏览器)。 */
489
+ const openStarPage = React.useCallback(() => {
490
+ const seam = starPrompt?.onOpen;
491
+ if (seam !== undefined) {
492
+ seam();
493
+ return;
494
+ }
495
+ void import('../starAction.js').then(({ STAR_REPO }) => {
496
+ openExternal(`https://github.com/${STAR_REPO}`);
497
+ });
498
+ }, [starPrompt]);
499
+ const runStarAction = React.useCallback(() => {
500
+ const seam = starPrompt?.onStar;
501
+ void (seam !== undefined
502
+ ? Promise.resolve(seam())
503
+ : import('../starAction.js').then(async ({ starRepo, STAR_REPO }) => {
504
+ const url = `https://github.com/${STAR_REPO}`;
505
+ const outcome = await starRepo();
506
+ if (outcome.kind === 'starred')
507
+ return { kind: 'starred' };
508
+ if (outcome.kind === 'no-gh')
509
+ return { kind: 'no-gh', url };
510
+ if (outcome.kind === 'not-authed')
511
+ return { kind: 'not-authed', url };
512
+ return { kind: 'failed', detail: outcome.detail, url };
513
+ })).then(attempt => {
514
+ if (attempt.kind === 'starred') {
515
+ setStarred(true);
516
+ // 成功就演一段庆祝(女仆娘接住星星)——`/star`、`Alt+S`、标语点击
517
+ // 都是这一条路。整屏界面开着或回合进行中时弹窗放不下,退回一句
518
+ // 通知,用户至少知道 star 点上了。
519
+ const blocked = channel.working || supervisorOpen || treeOpen || settingsOpen;
520
+ const index = STAR_MILESTONES.findIndex(milestone => milestone.hours === 99);
521
+ if (!blocked && index >= 0) {
522
+ setStarModal({ index, phase: 'done' });
523
+ return;
524
+ }
525
+ channel.notify(t('star-ok'), { color: 'success' });
526
+ return;
527
+ }
528
+ if (attempt.kind === 'no-gh') {
529
+ // 本机没法一键(没装 gh / 没登录)→ **自动**打开仓库页让用户自己点,
530
+ // 通知里说明原因(浏览器没拉起来时 URL 也还在文案里)。
531
+ openStarPage();
532
+ channel.notify(t('star-no-gh', { url: attempt.url }), { color: 'warning' });
533
+ return;
534
+ }
535
+ if (attempt.kind === 'not-authed') {
536
+ openStarPage();
537
+ channel.notify(t('star-not-authed', { url: attempt.url }), { color: 'warning' });
538
+ return;
539
+ }
540
+ channel.notify(t('star-failed', { detail: attempt.detail, url: attempt.url }), { color: 'error' });
541
+ });
542
+ }, [channel, starPrompt, openStarPage, supervisorOpen, treeOpen, settingsOpen]);
543
+ const starModalActions = React.useMemo(() => ({
544
+ // 弹窗自己演结果(成功→庆祝、失败→留在卡里说明原因),所以这里把
545
+ // 结局**回传**给它;`/star` 命令那条路仍走 runStarAction 的 notify。
546
+ onStar: () => {
547
+ const seam = starPrompt?.onStar;
548
+ // 成功把开屏彩蛋切成「捡到星星」版;gh 缺失/未登录**自动**打开仓库页
549
+ // (与一键路径同一套兜底)。注意**不要**给返回值再包一层 `.then()`——
550
+ // 多一个微任务会让弹窗"庆祝那一帧"被紧随其后的 Enter 关窗批掉
551
+ //(夹具 C8/C9 实测)。这里只挂副作用、原样返回。
552
+ const afterAttempt = (attempt) => {
553
+ if (attempt.kind === 'starred')
554
+ setStarred(true);
555
+ else if (attempt.kind === 'no-gh' || attempt.kind === 'not-authed')
556
+ openStarPage();
557
+ return attempt;
558
+ };
559
+ if (seam !== undefined) {
560
+ const result = seam();
561
+ if (result instanceof Promise) {
562
+ void result.then(afterAttempt);
563
+ return result;
564
+ }
565
+ return afterAttempt(result);
566
+ }
567
+ const run = import('../starAction.js').then(async ({ starRepo, STAR_REPO }) => {
568
+ const url = `https://github.com/${STAR_REPO}`;
569
+ const outcome = await starRepo();
570
+ if (outcome.kind === 'starred')
571
+ return { kind: 'starred' };
572
+ if (outcome.kind === 'no-gh')
573
+ return { kind: 'no-gh', url };
574
+ if (outcome.kind === 'not-authed')
575
+ return { kind: 'not-authed', url };
576
+ return { kind: 'failed', detail: outcome.detail, url };
577
+ });
578
+ void run.then(afterAttempt);
579
+ return run;
580
+ },
581
+ onOpen: () => {
582
+ setStarModal(null);
583
+ openStarPage();
584
+ },
585
+ }), [starPrompt, openStarPage]);
586
+ /** 弹窗关闭回调:稳定引用(见渲染处的注释)。 */
587
+ const closeStarModal = React.useCallback(() => { setStarModal(null); }, []);
421
588
  const [workspaceTargets, setWorkspaceTargets] = React.useState([]);
422
589
  const workspaceFlowRequestRef = React.useRef(0);
423
590
  const workspaceFlowAbortRef = React.useRef(null);
@@ -554,14 +721,14 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
554
721
  * renders with stale folds/expansion/selection — the "entered a freshly
555
722
  * dispatched session and it renders wrong" bug. Reset the same set `/new`
556
723
  * resets, plus the search overlay and the side question, and repaint the
557
- * transcript from the top.
724
+ * transcript pinned to the bottom so rewinds and model switches can continue.
558
725
  */
559
726
  const repaintTranscript = () => {
560
727
  const ink = instances.get(process.stdout) ?? instances.values().next().value;
561
728
  // Wait one task so React commits the new session's tree before the
562
729
  // scrollback clear repaints (same pattern as `/new`).
563
730
  setTimeout(() => {
564
- handle?.scrollTo(0);
731
+ handle?.scrollToBottom();
565
732
  ink?.clearScrollbackAndRedraw();
566
733
  }, 0);
567
734
  };
@@ -741,6 +908,39 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
741
908
  overlay: { kind: 'image-preview', image, gallery, index, ...(title === undefined ? {} : { title }) },
742
909
  });
743
910
  }, [channel]);
911
+ /** A clicked formula opens the same card as a transcript image. The raster
912
+ * is re-typeset at double the cell size, so 100% is a sharper formula
913
+ * rather than an upscaled one; when that re-render fails (too wide, TeX
914
+ * rejected) the pixels already on screen stand in. */
915
+ const openMathPreview = React.useCallback((preview) => {
916
+ const scaled = {
917
+ ...preview.request,
918
+ cellSize: {
919
+ width: preview.request.cellSize.width * MATH_PREVIEW_SCALE,
920
+ height: preview.request.cellSize.height * MATH_PREVIEW_SCALE,
921
+ },
922
+ maxColumns: Math.min(preview.request.maxColumns * MATH_PREVIEW_SCALE, MATH_PREVIEW_MAX_COLUMNS),
923
+ maxRows: Math.min(preview.request.maxRows * MATH_PREVIEW_SCALE, MATH_PREVIEW_MAX_ROWS),
924
+ };
925
+ const image = {
926
+ id: `math:${preview.tex}`,
927
+ width: preview.source.width,
928
+ height: preview.source.height,
929
+ name: preview.tex,
930
+ mediaType: 'image/png',
931
+ read: async () => {
932
+ const rendered = await renderMathRaster(scaled);
933
+ return rasterToPng(rendered.ok ? rendered.raster.source : preview.source);
934
+ },
935
+ };
936
+ openImagePreview(image, preview.tex);
937
+ }, [openImagePreview]);
938
+ // The math components live deep inside the transcript, so the opener is
939
+ // published rather than threaded through every message row's props.
940
+ React.useEffect(() => {
941
+ setMathPreviewOpener(openMathPreview);
942
+ return () => setMathPreviewOpener(undefined);
943
+ }, [openMathPreview]);
744
944
  // Agent-binding generation is monotonic across every agent replacement
745
945
  // and bumps before the replacement emit, closing the ABA hole where a
746
946
  // resumed session reuses the same id. Partial test/embed channels fall
@@ -1107,6 +1307,34 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
1107
1307
  // fullscreen (<AlternateScreen> supplies mouse tracking); a no-op
1108
1308
  // subscription in inline mode, where selection belongs to the terminal.
1109
1309
  // The copy clears the highlight and posts a transient notification.
1310
+ // Smart migration hint (product ask): ~12s after mount, one background
1311
+ // pass over the foreign-agent stores; when a source was active inside the
1312
+ // 20-minute window, surface the user's own wording once per session. The
1313
+ // file-level mtime scan is the counter's walk shape (sub-second) and runs
1314
+ // off the render path; failures read as "no data" and stay silent.
1315
+ const migrateHintShownRef = React.useRef(false);
1316
+ React.useEffect(() => {
1317
+ if (migrateHintShownRef.current)
1318
+ return;
1319
+ const timer = setTimeout(() => {
1320
+ migrateHintShownRef.current = true;
1321
+ void (async () => {
1322
+ const newest = await new Promise(resolve => {
1323
+ setImmediate(() => resolve(collectActivitySamples(MIGRATION_ADAPTERS, adapter => MIGRATE_SCAN_SPECS[adapter.id])));
1324
+ });
1325
+ const top = recentAgentsFrom(newest, Date.now())[0];
1326
+ if (top !== undefined) {
1327
+ channel.notify(t('migrate-hint-notify', { agent: top.label }), { timeoutMs: 10000 });
1328
+ // PRD #4: while the hint is up, a bare Enter (empty prompt, no
1329
+ // overlay) jumps into the picker with this source pre-checked;
1330
+ // the global key layer below consumes it, anything else disarms.
1331
+ setMigrateHintAgent(top.agentId);
1332
+ setTimeout(() => setMigrateHintAgent(current => current === top.agentId ? null : current), 10_000);
1333
+ }
1334
+ })();
1335
+ }, 12_000);
1336
+ return () => clearTimeout(timer);
1337
+ }, [channel]);
1110
1338
  useCopyOnSelect(text => channel.notify(t('copied-chars', { n: text.length }), { timeoutMs: 1500 }),
1111
1339
  // Stale-selection refusal: the highlighted rows were replaced in place
1112
1340
  // (streaming overwrite), so nothing was copied — say why instead of
@@ -1307,10 +1535,12 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
1307
1535
  const ok = writeLangPref(lang);
1308
1536
  setLang(lang);
1309
1537
  const settingsHost = channel.settingsHost();
1310
- const tuiView = settingsHost?.listNamespaces().find(entry => entry.ns === 'dsh-tui');
1538
+ // This mount's own namespace (custom Loader ids exist): looking up the
1539
+ // literal 'dsh-tui' skipped the mirror entirely on such mounts.
1540
+ const tuiView = settingsHost?.listNamespaces().find(entry => entry.ns === channel.settingsNamespace);
1311
1541
  if (settingsHost !== undefined && tuiView !== undefined) {
1312
1542
  void settingsHost
1313
- .write('dsh-tui', [{ op: 'set', path: ['lang'], value: lang }], tuiView.revision)
1543
+ .write(channel.settingsNamespace, [{ op: 'set', path: ['lang'], value: lang }], tuiView.revision)
1314
1544
  .catch(() => { });
1315
1545
  }
1316
1546
  channel.notify(ok ? t('lang-switched', { lang }) : t('lang-switch-failed', { lang }), { color: ok ? 'success' : 'error' });
@@ -1325,6 +1555,82 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
1325
1555
  case 'activity': return t('reload-kind-activity');
1326
1556
  }
1327
1557
  };
1558
+ /** Collect picker rows off the current turn: the scan is synchronous FS
1559
+ * work (name-only walk + per-file stat), so it is deferred by one macrotask
1560
+ * to let the overlay paint its loading state first. */
1561
+ const collectMigrateRows = () => new Promise(resolve => {
1562
+ setImmediate(() => resolve(collectMigratePickerRows(Date.now())));
1563
+ });
1564
+ /** Run `dsh-tui migrate <args>` in a child process through the package
1565
+ * bin; resolves with the exit code and the combined output. Uses the
1566
+ * shared no-throw runner (bounded capture, timeout, windowsHide): a wedged
1567
+ * child would otherwise hang the sequential per-source loop forever. */
1568
+ const runMigrateChild = async (parts) => {
1569
+ const { dirname } = await import('node:path');
1570
+ const { fileURLToPath } = await import('node:url');
1571
+ const { resolveOwnBin } = await import('../dsh-adapter/migrate/bin-path.js');
1572
+ // This file sits at a different depth per layout (src/screens vs
1573
+ // lib/types/screens), so the bin resolves by upward probe — see
1574
+ // bin-path.ts; a fixed dirname count fails on real installs.
1575
+ const bin = resolveOwnBin(dirname(fileURLToPath(import.meta.url)));
1576
+ if (bin === undefined) {
1577
+ channel.notify(t('migrate-spawn-failed'), { color: 'error', timeoutMs: 8000 });
1578
+ return { code: -1, out: '' };
1579
+ }
1580
+ const result = await execFileNoThrow(process.execPath, [bin, 'migrate', ...parts], {
1581
+ timeout: MIGRATE_CHILD_TIMEOUT_MS,
1582
+ });
1583
+ // A killed child reports `code: null` and whatever it managed to print; put
1584
+ // the reason on the record so the transcript does not read as a silent
1585
+ // failure. Nothing at all (no code, no output) means it never really ran.
1586
+ if (result.code === null) {
1587
+ return {
1588
+ code: null,
1589
+ out: `${result.stdout}${result.stderr}${t('migrate-child-timeout', { minutes: MIGRATE_CHILD_TIMEOUT_MS / 60_000 })}\n`,
1590
+ };
1591
+ }
1592
+ if (result.code === 1 && result.stdout === '' && result.stderr === '') {
1593
+ channel.notify(t('migrate-spawn-failed'), { color: 'error', timeoutMs: 8000 });
1594
+ }
1595
+ // stdout carries the per-source report, stderr the usage/error lines;
1596
+ // both belong in the /migrate transcript row.
1597
+ return { code: result.code, out: `${result.stdout}${result.stderr}` };
1598
+ };
1599
+ /** Orchestrate the confirmation layer's confirmed rows (PRD #3): one child
1600
+ * per source, sequential; per-source progress notifications (throttled by
1601
+ * the source boundary — no intra-source spam), real per-source counters
1602
+ * parsed from each child's report, and a final summary that NEVER claims
1603
+ * success for a source that did not run (the P2 fix). */
1604
+ const spawnMigrateSources = (rows, dryRun) => {
1605
+ const allOut = [];
1606
+ let failures = 0;
1607
+ void (async () => {
1608
+ for (let i = 0; i < rows.length; i++) {
1609
+ const row = rows[i];
1610
+ channel.notify(t(dryRun ? 'migrate-previewing-source' : 'migrate-importing-source', { label: row.label, i: i + 1, n: rows.length }), { timeoutMs: 4000 });
1611
+ const { code, out } = await runMigrateChild(dryRun ? [row.agentId, '--dry-run'] : [row.agentId]);
1612
+ allOut.push(...out.split('\n').map(line => cleanRenderText(line, 400)).filter(Boolean));
1613
+ if (code !== 0)
1614
+ failures += 1;
1615
+ // Per-source real counters straight from the child's report line.
1616
+ const summary = parseImportSummary(out).find(entry => entry.agentId === row.agentId);
1617
+ if (!dryRun && summary !== undefined) {
1618
+ channel.notify(t('migrate-source-done', { label: row.label, imported: summary.imported, existing: summary.existing }), { timeoutMs: 6000 });
1619
+ }
1620
+ }
1621
+ // The transcript row is this run's record. When a source failed AND no
1622
+ // child output was captured at all (killed by the timeout, or never
1623
+ // spawned), the success wording would contradict the notification right
1624
+ // above it — report the failure here too.
1625
+ const fallbackLine = failures > 0
1626
+ ? t('migrate-failed', { n: failures })
1627
+ : t(dryRun ? 'migrate-all-previewed' : 'migrate-all-done', { n: rows.length });
1628
+ channel.pushLocal('/migrate', allOut.length > 0 ? allOut : [fallbackLine]);
1629
+ channel.notify(failures === 0
1630
+ ? t(dryRun ? 'migrate-all-previewed' : 'migrate-all-done', { n: rows.length })
1631
+ : t('migrate-failed', { n: failures }), failures === 0 ? { timeoutMs: 6000 } : { color: 'error', timeoutMs: 10000 });
1632
+ })();
1633
+ };
1328
1634
  const runCommand = (name, rawInput = '', images = []) => {
1329
1635
  switch (name) {
1330
1636
  case 'activity': {
@@ -1923,7 +2229,28 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
1923
2229
  const rate = total > 0 ? ((usage.cacheRead / total) * 100).toFixed(1) : '0.0';
1924
2230
  lines.push(t('cost-cache-hit-rate', { rate, read: formatTokens(usage.cacheRead), write: formatTokens(usage.cacheWrite) }));
1925
2231
  }
1926
- lines.push(t('cost-note'));
2232
+ // 金额与拆解:主会话按模型分桶 + 子代理按各自 (provider, model) 分桶;
2233
+ // 全部未计价时只报 token 并标注未计价,不显示 ¥0.00 金额行(DESIGN D4/D6)。
2234
+ const estimate = estimateSessionCostSnapshotCny({
2235
+ provider: channel.provider,
2236
+ main: channel.mainCost,
2237
+ subagents: channel.subagentCost,
2238
+ fallbackTokens: channel.tokens,
2239
+ fallbackModel: channel.model,
2240
+ });
2241
+ // 金额行与末尾口径共用同一判定:有已计价金额才显示金额行与"估算非账单"
2242
+ // 文案;无金额(无用量 / 全部未计价)只解释 token(#1089)。
2243
+ const hasAmount = estimate !== undefined && estimate.total > 0;
2244
+ if (estimate !== undefined) {
2245
+ if (hasAmount) {
2246
+ lines.push(t('cost-session-estimate', { cost: estimate.total.toFixed(2) }));
2247
+ lines.push(`${t('cost-split-main', { cost: estimate.main.toFixed(2) })} · ${t('cost-split-subagent', { cost: estimate.subagent.toFixed(2) })}`);
2248
+ }
2249
+ if (estimate.unpricedTokens > 0) {
2250
+ lines.push(t('cost-unpriced', { tokens: formatTokens(estimate.unpricedTokens) }));
2251
+ }
2252
+ }
2253
+ lines.push(t(hasAmount ? 'cost-note' : 'cost-note-no-amount'));
1927
2254
  setHelpOpen(false);
1928
2255
  channel.pushLocal('/cost', lines);
1929
2256
  return true;
@@ -1944,6 +2271,13 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
1944
2271
  setSettingsOpen(true);
1945
2272
  return true;
1946
2273
  }
2274
+ case 'star': {
2275
+ // 一键 star:**只有用户主动敲 /star 才会跑**(绝不自动)。动作用
2276
+ // runStarAction(与开屏弹窗共用),异步执行、结果用 notify 报。
2277
+ setHelpOpen(false);
2278
+ runStarAction();
2279
+ return true;
2280
+ }
1947
2281
  case 'config': {
1948
2282
  const userHome = process.env.USERPROFILE ?? '';
1949
2283
  const lines = [
@@ -1961,6 +2295,60 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
1961
2295
  setHelpOpen(false);
1962
2296
  channel.pushLocal('/doctor', channel.doctorInfo());
1963
2297
  return true;
2298
+ case 'migrate': {
2299
+ // Double entry points with the CLI. BARE `/migrate` opens the
2300
+ // multi-select picker; `/migrate <agent>` opens the CONFIRMATION
2301
+ // layer for that single source (PRD #2 — a bulk import is never one
2302
+ // keystroke away). Validity is decided against the adapter REGISTRY,
2303
+ // never the picker's row cache: on a fresh mount that cache is empty,
2304
+ // and deriving the answer from it made every `/migrate <agent>` report
2305
+ // an unknown source until the picker had been opened once.
2306
+ setHelpOpen(false);
2307
+ const command = resolveMigrateCommand(rawInput, MIGRATION_ADAPTERS.map(adapter => adapter.id));
2308
+ if (command.kind === 'unknown') {
2309
+ channel.notify(t('migrate-unknown-agent', { agent: command.agentId }), { color: 'error', timeoutMs: 8000 });
2310
+ return true;
2311
+ }
2312
+ if (command.kind === 'usage') {
2313
+ channel.notify(t('migrate-usage'), { color: 'error', timeoutMs: 8000 });
2314
+ return true;
2315
+ }
2316
+ if (command.kind === 'dry-run-needs-source') {
2317
+ channel.notify(t('migrate-dry-run-needs-source'), { color: 'error', timeoutMs: 8000 });
2318
+ return true;
2319
+ }
2320
+ if (command.kind === 'import') {
2321
+ void (async () => {
2322
+ // Rows carry the scannable count the confirmation line shows; a
2323
+ // warm cache from an earlier picker visit is reused as is.
2324
+ const rows = migrateRows ?? await collectMigrateRows();
2325
+ const row = rows.find(candidate => candidate.agentId === command.agentId);
2326
+ if (row === undefined) {
2327
+ channel.notify(t('migrate-unknown-agent', { agent: command.agentId }), { color: 'error', timeoutMs: 8000 });
2328
+ return;
2329
+ }
2330
+ if (command.dryRun) {
2331
+ spawnMigrateSources([row], true);
2332
+ return;
2333
+ }
2334
+ setMigratePending([row]);
2335
+ // Pin the checked set to the source this confirmation is about:
2336
+ // Esc returns to the picker, and a stale set from an earlier visit
2337
+ // would there contradict what the confirmation just showed.
2338
+ setMigrateChecked(new Set([row.agentId]));
2339
+ dispatchOverlay({ type: 'open', overlay: { kind: 'migrate-confirm' } });
2340
+ })();
2341
+ return true;
2342
+ }
2343
+ // Bare `/migrate` starts a NEW flow: drop the previous run's checks
2344
+ // (only Esc-out-of-confirm keeps them) and let the picker show its
2345
+ // scanning state until the rows land.
2346
+ setMigrateRows(null);
2347
+ setMigrateChecked(new Set());
2348
+ dispatchOverlay({ type: 'open', overlay: { kind: 'migrate', index: 0 } });
2349
+ void collectMigrateRows().then(setMigrateRows);
2350
+ return true;
2351
+ }
1964
2352
  case 'plugins':
1965
2353
  // Plugin diagnostics (C-070): trust banner first, then descriptor /
1966
2354
  // grant matrix / ledger tail — or validate+negotiate for
@@ -2172,7 +2560,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
2172
2560
  setHelpOpen(false);
2173
2561
  const tuiNamespace = channel.settingsHost()
2174
2562
  ?.listNamespaces()
2175
- .find(entry => entry.ns === 'dsh-tui');
2563
+ .find(entry => entry.ns === channel.settingsNamespace);
2176
2564
  const plan = planReload({
2177
2565
  envTheme: envThemeOverride(),
2178
2566
  envLang: isLang(process.env.DSH_TUI_LANG) ? process.env.DSH_TUI_LANG : undefined,
@@ -2267,7 +2655,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
2267
2655
  setHelpOpen(false);
2268
2656
  channel.pushLocal('/terminal-setup', [
2269
2657
  t('terminal-setup-hint'),
2270
- t('terminal-paste-hint', { mod: modLabel }),
2658
+ t('terminal-paste-hint', { keys: effectiveComboDisplay('paste') }),
2271
2659
  ]);
2272
2660
  return true;
2273
2661
  case 'recap': {
@@ -2627,6 +3015,11 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
2627
3015
  /** Deduplicate terminals that report one Enter as parsed Return then raw CR/LF. */
2628
3016
  const lastModalEnterAtRef = React.useRef(0);
2629
3017
  useInput((input, key, event) => {
3018
+ // 开屏"求 star"弹窗开着时键盘全归它(↑/↓/Enter/Esc 由它自己的
3019
+ // useInput 处理),滚轮也不许滚动它身后的转录——和下面的整屏界面
3020
+ // 同一套让位规则。
3021
+ if (starModal !== null)
3022
+ return;
2630
3023
  // Prompt-slot panels own the keyboard while visible. Their own useInput
2631
3024
  // handles the relevant keys; Chat registered first, so yielding here
2632
3025
  // still lets the panel receive them. PromptInput now stays mounted but
@@ -3058,6 +3451,81 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3058
3451
  }
3059
3452
  return;
3060
3453
  }
3454
+ // Armed migration hint (PRD #4): bare Enter while the hint notification
3455
+ // is up — no overlay and an EMPTY prompt — jumps into the picker with the
3456
+ // hinted source pre-checked; every other key disarms silently. The empty
3457
+ // check is explicit because the composer owns that state: an Enter that
3458
+ // submitted a written message must not also open the picker, and this
3459
+ // listener runs before PromptInput's, so the event is consumed here too.
3460
+ if (migrateHintAgent !== null && overlay.kind === 'none') {
3461
+ if (plainReturn && !(promptControllerRef.current?.hasText() ?? false)) {
3462
+ const agent = migrateHintAgent;
3463
+ event.stopImmediatePropagation();
3464
+ setMigrateHintAgent(null);
3465
+ setMigrateRows(null);
3466
+ setMigrateChecked(new Set([agent]));
3467
+ dispatchOverlay({ type: 'open', overlay: { kind: 'migrate', index: 0 } });
3468
+ void collectMigrateRows().then(setMigrateRows);
3469
+ return;
3470
+ }
3471
+ setMigrateHintAgent(null);
3472
+ }
3473
+ if (overlay.kind === 'migrate') {
3474
+ const rows = migrateRows ?? [];
3475
+ if (key.upArrow || key.downArrow) {
3476
+ dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: rows.length });
3477
+ }
3478
+ else if (key.escape) {
3479
+ dispatchOverlay({ type: 'close' });
3480
+ }
3481
+ else if (rows.length > 0) {
3482
+ const row = rows[overlay.index];
3483
+ if (input === ' ' && row !== undefined) {
3484
+ setMigrateChecked(current => {
3485
+ const next = new Set(current);
3486
+ if (next.has(row.agentId))
3487
+ next.delete(row.agentId);
3488
+ else
3489
+ next.add(row.agentId);
3490
+ return next;
3491
+ });
3492
+ }
3493
+ else if (input === 'a' && !key.ctrl && !key.meta) {
3494
+ // All/none toggle: a checked-everything state collapses to none.
3495
+ setMigrateChecked(current => current.size >= rows.length ? new Set() : new Set(rows.map(candidate => candidate.agentId)));
3496
+ }
3497
+ else if (plainReturn) {
3498
+ // Checked set wins; the focused row acts as a single selection
3499
+ // when nothing is checked (PRD #1).
3500
+ const chosen = migrateChecked.size > 0
3501
+ ? rows.filter(candidate => migrateChecked.has(candidate.agentId))
3502
+ : row !== undefined ? [row] : [];
3503
+ if (chosen.length > 0) {
3504
+ setMigratePending(chosen);
3505
+ dispatchOverlay({ type: 'close' });
3506
+ dispatchOverlay({ type: 'open', overlay: { kind: 'migrate-confirm' } });
3507
+ }
3508
+ }
3509
+ }
3510
+ return;
3511
+ }
3512
+ if (overlay.kind === 'migrate-confirm') {
3513
+ if (key.escape) {
3514
+ // Back to the picker with the checked set preserved (PRD #2's
3515
+ // "cancel" reads cheapest as "let me change the selection").
3516
+ dispatchOverlay({ type: 'close' });
3517
+ dispatchOverlay({ type: 'open', overlay: { kind: 'migrate', index: 0 } });
3518
+ }
3519
+ else if (plainReturn) {
3520
+ dispatchOverlay({ type: 'close' });
3521
+ spawnMigrateSources(migratePending, false);
3522
+ }
3523
+ else if (input === 'd' && !key.ctrl && !key.meta) {
3524
+ dispatchOverlay({ type: 'close' });
3525
+ spawnMigrateSources(migratePending, true);
3526
+ }
3527
+ return;
3528
+ }
3061
3529
  if (overlay.kind === 'activity') {
3062
3530
  if (key.upArrow || key.downArrow) {
3063
3531
  dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: PRESET_NAMES.length });
@@ -3410,6 +3878,17 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3410
3878
  }
3411
3879
  event.stopImmediatePropagation();
3412
3880
  }
3881
+ else if (key.escape
3882
+ && !helpOpen
3883
+ && !channel.working
3884
+ && channel.compaction?.cancellable === true
3885
+ && !promptControllerRef.current?.vimActive()) {
3886
+ // Idle Esc otherwise falls through to the prompt's double-tap-clear;
3887
+ // while a manual compaction runs, stopping it is what the status row
3888
+ // promises (and the host closes the bracket cleanly on abort).
3889
+ channel.cancelCompact();
3890
+ event.stopImmediatePropagation();
3891
+ }
3413
3892
  else if (actionMatches('transcript', input, key) && !helpOpen) {
3414
3893
  // Leaving transcript mode (default Ctrl+O) — search was already
3415
3894
  // handled above. Help is modal: toggling this state behind the
@@ -3446,7 +3925,17 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3446
3925
  // CLEARS a non-empty prompt (single press) and only arms the
3447
3926
  // double-press exit when the input is empty; ctrl+d keeps the
3448
3927
  // time-based double-press exit regardless.
3449
- if (channel.working) {
3928
+ if (input === 'c' && !channel.working && channel.compaction?.cancellable === true) {
3929
+ // Ctrl+C during a manual compaction stops the compaction instead of
3930
+ // arming the double-press exit: exiting the process mid-bracket is how
3931
+ // a session log ends up with an unmatched `compaction/start`. Ctrl+D
3932
+ // keeps its exit meaning, and the next Ctrl+C behaves normally again.
3933
+ channel.cancelCompact();
3934
+ exitPendingRef.current = false;
3935
+ if (exitTimerRef.current)
3936
+ clearTimeout(exitTimerRef.current);
3937
+ }
3938
+ else if (channel.working) {
3450
3939
  // First press while working only interrupts. If that abort is still
3451
3940
  // converging (cancelPending) the next press is the user insisting on
3452
3941
  // leaving: go straight to the exit funnel. Without this, a stuck turn
@@ -3505,6 +3994,12 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3505
3994
  // Consume: same readline-shadowing rule as dashboard/showAll above.
3506
3995
  event.stopImmediatePropagation();
3507
3996
  }
3997
+ else if (actionMatches('star', input, key)) {
3998
+ // 一键 star(默认 Alt+S)——与 `/star`、开屏标语点击同一个动作。
3999
+ // 消费事件:alt 组合不该再落进输入框当普通字符。
4000
+ runStarAction();
4001
+ event.stopImmediatePropagation();
4002
+ }
3508
4003
  else if (plainReturn && !isSticky) {
3509
4004
  // Enter while scrolled up returns to the bottom: the
3510
4005
  // affordance now exists whenever the view is off the bottom, not
@@ -3532,6 +4027,14 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3532
4027
  // Working-activity line (spinner slot): context-pressure prefix shares the
3533
4028
  // StatusLine thresholds (amber ≥ 80, red ≥ 95).
3534
4029
  const activityWarnPct = contextPressurePct(channel.lastUsage, channel.contextWindow);
4030
+ // Who owns the spinner slot: with the working-activity line on, that slot
4031
+ // draws the user's `/activity` preset, so the compaction row borrows the same
4032
+ // indicator instead of answering with the classic dot.
4033
+ const activitySlot = channel.activityEnabled && !channel.minimal;
4034
+ // An automatic compaction runs INSIDE the turn, so it rides whichever spinner
4035
+ // the slot shows as a badge instead of a second row (the spinner's timer is
4036
+ // the turn's, not the compaction's).
4037
+ const compactionBadge = channel.compaction === undefined ? undefined : t('compact-badge');
3535
4038
  // ── Interrupt lane ─────────────────────────────────────────────────────
3536
4039
  // The approval and ask_user_question panels park the agent until the user
3537
4040
  // answers, but they render inside the conversation layout — every screen
@@ -3545,8 +4048,38 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3545
4048
  // The panel elements are shared with the prompt-slot chain below so the
3546
4049
  // two mount sites cannot drift.
3547
4050
  const approvalPanelNode = approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, background: approvalSnapshot.agentId !== channel.agentId, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : null;
3548
- const questionPanelNode = questionSnapshot !== null ? (_jsx(AskUserQuestionPanel, { question: questionSnapshot.question, position: questionSnapshot.position, total: questionSnapshot.total, answered: questionSnapshot.answered, initialDraft: questionSnapshot.draft, onAnswer: selection => questionStore.answerCurrent(selection), onCancel: () => questionStore.cancelCurrent(), onBack: questionSnapshot.canGoBack
3549
- ? draft => questionStore.backCurrent(draft)
4051
+ const questionPanelNode = questionSnapshot !== null ? (_jsx(AskUserQuestionPanel, { question: questionSnapshot.question, position: questionSnapshot.position, total: questionSnapshot.total, answered: questionSnapshot.answered, initialDraft: questionSnapshot.draft, onAnswer: selection => {
4052
+ if (!questionStore.stillCurrent(questionSnapshot.key))
4053
+ return;
4054
+ questionStore.answerCurrent(selection);
4055
+ }, onCancel: () => questionStore.cancelCurrent(), onEscape: draft => {
4056
+ // Esc means "back" once a later question is showing, including when
4057
+ // → and Esc share one stdin batch and this panel was mounted for
4058
+ // question 1 (no onBack). Ctrl+C stays on onCancel: it cancels the
4059
+ // whole ask from any question, so a same-batch → must not swallow it.
4060
+ const live = questionStore.getSnapshot();
4061
+ if (live?.canGoBack) {
4062
+ // A same-batch → already saved this panel's draft on the question
4063
+ // it left. Passing that draft into backCurrent would write it onto
4064
+ // the question → just opened.
4065
+ questionStore.backCurrent(questionStore.stillCurrent(questionSnapshot.key) ? draft : undefined);
4066
+ return;
4067
+ }
4068
+ if (live !== null && questionStore.stillCurrent(questionSnapshot.key)) {
4069
+ questionStore.cancelCurrent();
4070
+ }
4071
+ }, onBack: questionSnapshot.canGoBack
4072
+ ? draft => {
4073
+ if (!questionStore.stillCurrent(questionSnapshot.key))
4074
+ return;
4075
+ questionStore.backCurrent(draft);
4076
+ }
4077
+ : undefined, onForward: questionSnapshot.canGoForward
4078
+ ? draft => {
4079
+ if (!questionStore.stillCurrent(questionSnapshot.key))
4080
+ return;
4081
+ questionStore.forwardCurrent(draft);
4082
+ }
3550
4083
  : undefined, collapsed: questionMinimized, onExpand: () => setMinimizedQuestionKey(null), onToggleFold: () => setMinimizedQuestionKey(previous => previous === questionSnapshot.key ? null : questionSnapshot.key), fullscreen: fullscreen }, questionSnapshot.key)) : null;
3551
4084
  const interruptPanel = approvalPanelNode ?? questionPanelNode;
3552
4085
  const screenOpen = channel.pluginScene !== undefined || supervisorOpen || settingsOpen
@@ -3601,19 +4134,17 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3601
4134
  */
3602
4135
  const agentRowOf = (sessionId) => agentViewRows.find(row => row.id === sessionId);
3603
4136
  const supervisorNode = (_jsx(SessionSupervisor, { channel: channel, home: homeDir(), onClose: closeHome, approval: approvalSnapshot, onApprove: outcome => approvals.decide(outcome), onOpenSession: async (sessionId) => {
4137
+ // A refusal is reported by the screen itself (see `openSession`):
4138
+ // the composer that draws channel notifications is not mounted here.
3604
4139
  const result = await channel.resumeTo(sessionId);
3605
- if (!result.ok) {
3606
- const text = resumeFailureText(result);
3607
- if (text !== undefined)
3608
- channel.notify(text, { color: 'error', timeoutMs: 8000 });
3609
- return false;
3610
- }
4140
+ if (!result.ok)
4141
+ return result;
3611
4142
  channel.notify(t('resume-resumed'));
3612
4143
  suppressLogoIntroRef.current = true;
3613
4144
  setAgentViewReturnId(undefined);
3614
4145
  setSupervisorOpen(false);
3615
4146
  repaintTranscript();
3616
- return true;
4147
+ return result;
3617
4148
  }, onNewSession: async (target) => {
3618
4149
  const ok = await channel.switchWorkspace(target);
3619
4150
  if (ok) {
@@ -3703,7 +4234,8 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3703
4234
  || overlay.kind === 'tips'
3704
4235
  || (recap !== null && (!recap.auto || recap.expanded))
3705
4236
  || btw !== null
3706
- || questionPanelNode !== null;
4237
+ || questionPanelNode !== null
4238
+ || starModal !== null;
3707
4239
  // The trajectory scene replaces the conversation for as long as it is open.
3708
4240
  // Rendering it INSTEAD of (not above) the transcript is what makes it a
3709
4241
  // screen rather than an overlay: it owns the full viewport, and the
@@ -3728,11 +4260,13 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3728
4260
  presetOptionCount: presetOptions.length,
3729
4261
  }) && !(overlay.kind === 'permission'
3730
4262
  && (approvalSnapshot !== null || questionSnapshot !== null || dialogSnapshot !== null));
3731
- // The sticky header pins the turn owning the viewport top row
3732
- // (timeline.activeId, reported by MessageList) — scrolled up to an old
3733
- // turn, it carries THAT turn's prompt, not the latest one.
4263
+ // The sticky header pins the turn owning the viewport top row once its
4264
+ // prompt has scrolled out above it (timeline.pinnedId, reported by
4265
+ // MessageList) — scrolled up to an old turn, it carries THAT turn's
4266
+ // prompt, not the latest one. The row stays while scrolled up and goes
4267
+ // blank when nothing is pinned, so the viewport never shifts under it.
3734
4268
  // channel.rows is a live in-place array, so the lookup is per-render.
3735
- const anchorUserRowId = timeline.activeId;
4269
+ const anchorUserRowId = timeline.pinnedId;
3736
4270
  const anchorUserText = anchorUserRowId === null
3737
4271
  ? null
3738
4272
  : channel.rows.find(row => row.id === anchorUserRowId)?.text ?? null;
@@ -3756,7 +4290,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3756
4290
  ? () => setPeekSuppressed(peekKey(activePreview.image, activePreview.title))
3757
4291
  : () => dispatchOverlay({ type: 'close-if', kind: 'image-preview' }), region: imagePreviewRegion }))
3758
4292
  : null;
3759
- return (_jsxs(Box, { ref: wakeTickRef, flexDirection: "column", flexGrow: 1, width: "100%", children: [!isSticky && anchorUserText && (_jsx(PinnedTurnHeader, { text: anchorUserText, onClick: () => {
4293
+ return (_jsxs(Box, { ref: wakeTickRef, flexDirection: "column", flexGrow: 1, width: "100%", children: [!isSticky && timeline.activeId !== null && (_jsx(PinnedTurnHeader, { text: anchorUserText || null, onClick: () => {
3760
4294
  // Click snaps the pinned prompt to the viewport top. Jump by the
3761
4295
  // SAME content coordinate the
3762
4296
  // rail's tick uses (timeline turn top = the prompt TEXT top):
@@ -3771,7 +4305,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3771
4305
  seekRow(anchorUserRowId);
3772
4306
  else
3773
4307
  handle?.scrollToBottom();
3774
- } })), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, marginRight: -pageInsetX, children: [_jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd, whale: channel.whale, whaleIdle: channel.whaleIdle && whaleArtVisible, working: channel.working,
4308
+ } })), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, marginRight: -pageInsetX, children: [_jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd,
4309
+ // 大字字面(设置项 `dsh-tui.splashFont`):`daily` 交回按天轮换
4310
+ // (`undefined`),其余 pin 住一款。
4311
+ fontId: splashFontIdOf(channel.splashFont), whale: channel.whale, whaleIdle: channel.whaleIdle && whaleArtVisible, whaleGirl: channel.whaleGirl, starred: starred, onStarClick: runStarAction, working: channel.working,
3775
4312
  // Resuming a long session skips the ~3.4s opening animation: it
3776
4313
  // keeps firing low-frequency React commits that compete with the
3777
4314
  // transcript mount batches (and the first wheel events) for the
@@ -3779,7 +4316,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3779
4316
  // sessions keep the full intro; restored ones settle instantly.
3780
4317
  // A remount after a whole screen closed also settles instantly
3781
4318
  // (see suppressLogoIntroRef).
3782
- skipIntro: suppressLogoIntroRef.current || channel.rows.length > 30 }, logoNonce), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: toggleLoadedContext })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, streamViewToggledRows: streamViewToggledRows, onToggleStreamView: toggleStreamView, model: channel.model, diffLayout: channel.diffLayout, thinkingFold: channel.thinkingFold, toolBackground: channel.toolBackground, foldTerminalCommand: channel.foldTerminalCommand, smoothStreaming: channel.smoothStreaming, activityFrames: channel.activityFrames, showAll: showAllMessages, thinkingVisible: thinkingVisible, historyPaintEnabled: !fullscreen, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount, onTimeline: setTimeline, onOpenSubagent: setSubagentDetailId, onOpenJobs: openJobsPanel, onOpenFile: openFileActions, sessionCwd: channel.cwd, onPreviewImage: openImagePreview, suppressImageGraphics: activePreview !== null })] }), (() => {
4319
+ skipIntro: suppressLogoIntroRef.current || channel.rows.length > 30 }, logoNonce), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: toggleLoadedContext })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: primaryComboString('trajectory') }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, streamViewToggledRows: streamViewToggledRows, onToggleStreamView: toggleStreamView, model: channel.model, diffLayout: channel.diffLayout, thinkingFold: channel.thinkingFold, toolBackground: channel.toolBackground, foldTerminalCommand: channel.foldTerminalCommand, smoothStreaming: channel.smoothStreaming, activityFrames: channel.activityFrames, showAll: showAllMessages, thinkingVisible: thinkingVisible, historyPaintEnabled: !fullscreen, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount, onTimeline: setTimeline, onOpenSubagent: setSubagentDetailId, onOpenJobs: openJobsPanel, onOpenFile: openFileActions, sessionCwd: channel.cwd, onPreviewImage: openImagePreview, suppressImageGraphics: activePreview !== null })] }), (() => {
3783
4320
  // Gutter mode (settings `dsh-tui.scrollGutter`): the timeline
3784
4321
  // rail (default), the proportional scrollbar, or nothing. The
3785
4322
  // slot keeps its 2 columns in both rendered modes (Qwen's
@@ -3793,16 +4330,17 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3793
4330
  }
3794
4331
  return (_jsx(TimelineRail, { handle: handle, turns: timeline.turns, activeId: timeline.activeId, upId: timeline.upId, downId: timeline.downId, terminalWidth: terminalColumns, hoverEnabled: !promptSelectionActive, onRevealTurn: revealAndSeekRow }));
3795
4332
  })(), !promptEditorOpen && imagePreviewNode] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [showPill && (_jsx(NewMessagesPill, { count: unseenCount, onClick: () => handle?.scrollToBottom() })), channel.working &&
3796
- (channel.activityEnabled &&
3797
- !channel.minimal &&
4333
+ (activitySlot &&
3798
4334
  workingActivity !== undefined &&
3799
4335
  workingActivity.line !== '' &&
3800
4336
  workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95,
3801
4337
  // Upload = real tokens of the last request; download =
3802
4338
  // the animated chars/4 estimate, matching the classic
3803
4339
  // spinner's counter (the suffix used raw chars before,
3804
- // inflating the reading next to a real upload number).
3805
- suffix: `${lastUploadTokens > 0 ? ` · ↑ ${formatTokens(lastUploadTokens)}` : ''} · ↓ ${formatTokens(Math.round(channel.responseChars / 4))} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, uploadTokensRef: uploadTokensRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), _jsx(GoalTodoPanel, { channel: channel, collapsed: todoCollapsed, onToggle: () => setTodoCollapsed(previous => !previous) }), recap !== null && recap.auto && !recap.expanded && (_jsx(AutoRecapRow, { summary: recap.summary, streaming: !recap.done, onExpand: () => setRecap(prev => (prev ? { ...prev, expanded: true } : prev)), onDismiss: () => closeRecap() })), balance !== null && (_jsx(BalanceReportRow, { result: balance.result, refreshing: balance.refreshing, tokens: channel.tokens, model: channel.model, onRefresh: runBalance, onDismiss: () => setBalance(null) })), statusEntries.length > 0 && (_jsx(Text, { dimColor: true, wrap: "truncate", children: statusEntries.map(entry => entry.text).join(' · ') })), activePreview === null && statusViews.map(view => (_jsx(PluginStatusViewBoundary, { viewKey: view.key, onError: (key, error) => statusContributions.reportViewError(key, error), children: _jsx(Box, { flexDirection: "column", flexShrink: 0, maxHeight: view.maxRows, overflow: "hidden", children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: React.createElement(view.component, {
4340
+ // inflating the reading next to a real upload number). An
4341
+ // automatic compaction mid-turn badges THIS line too — it is
4342
+ // the spinner slot whenever real activity data exists.
4343
+ suffix: `${lastUploadTokens > 0 ? ` · ↑ ${formatTokens(lastUploadTokens)}` : ''} · ↓ ${formatTokens(Math.round(channel.responseChars / 4))} tokens${compactionBadge === undefined ? '' : ` · ${compactionBadge}`}` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, uploadTokensRef: uploadTokensRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus, suffix: compactionBadge }))), !channel.working && channel.compaction !== undefined && (_jsx(CompactionStatusRow, { compaction: channel.compaction, activityPreset: activitySlot ? channel.activityFrames : undefined })), _jsx(GoalTodoPanel, { channel: channel, collapsed: todoCollapsed, onToggle: () => setTodoCollapsed(previous => !previous) }), recap !== null && recap.auto && !recap.expanded && (_jsx(AutoRecapRow, { summary: recap.summary, streaming: !recap.done, onExpand: () => setRecap(prev => (prev ? { ...prev, expanded: true } : prev)), onDismiss: () => closeRecap() })), balance !== null && (_jsx(BalanceReportRow, { result: balance.result, refreshing: balance.refreshing, tokens: channel.tokens, model: channel.model, provider: channel.provider, mainCost: channel.mainCost, subagentCost: channel.subagentCost, onRefresh: runBalance, onDismiss: () => setBalance(null) })), statusEntries.length > 0 && (_jsx(Text, { dimColor: true, wrap: "truncate", children: statusEntries.map(entry => entry.text).join(' · ') })), activePreview === null && statusViews.map(view => (_jsx(PluginStatusViewBoundary, { viewKey: view.key, onError: (key, error) => statusContributions.reportViewError(key, error), children: _jsx(Box, { flexDirection: "column", flexShrink: 0, maxHeight: view.maxRows, overflow: "hidden", children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: React.createElement(view.component, {
3806
4344
  React,
3807
4345
  ui: STATUS_VIEW_UI,
3808
4346
  }) }) }) }, `${view.key}:${view.registrationId}`))), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [approvalPanelNode !== null ? (approvalPanelNode) : dialogSnapshot !== null ? (_jsx(ExtensionDialog, { dialog: dialogSnapshot, onDecide: value => dialogs.decide(dialogSnapshot.key, value), onCancel: () => dialogs.cancel(dialogSnapshot.key) }, dialogSnapshot.key)) : overlay.kind === 'tips' ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(TipsPanel, { onClose: () => dispatchOverlay({ type: 'close-if', kind: 'tips' }) }) })) : recap !== null && (!recap.auto || recap.expanded) ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RecapPanel, { summary: recap.summary, title: recap.title, error: recap.error, streaming: !recap.done, titleApplied: recap.titleApplied, onClose: () => {
@@ -3852,7 +4390,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3852
4390
  ? undefined
3853
4391
  : {
3854
4392
  band: wakeBand,
3855
- hint: trajectorySeen ? undefined : `${modLabel}t`,
4393
+ hint: trajectorySeen ? undefined : primaryComboString('trajectory'),
3856
4394
  tick: Math.floor(wakeTime / 120),
3857
4395
  } }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8, 1), children: [overlay.kind === 'thinking' && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: overlay.focus, onPick: (index) => {
3858
4396
  // 点击行 = 设焦点 + 应用(与 Enter 同一条路径)
@@ -3898,7 +4436,19 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3898
4436
  return;
3899
4437
  dispatchOverlay({ type: 'close' });
3900
4438
  void switchModelRecorded(model.provider, model.id, model.name);
3901
- } })) })), overlay.kind === 'skills' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: skillsList === null ? (_jsx(SkillsPickerLoading, {})) : (_jsx(SkillsPicker, { skills: skillsList, focusIndex: overlay.index, onPick: (index) => {
4439
+ } })) })), overlay.kind === 'migrate' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(MigratePicker, { rows: migrateRows ?? [], focusIndex: overlay.index, checked: migrateChecked, loading: migrateRows === null, onPick: (index) => {
4440
+ const row = (migrateRows ?? [])[index];
4441
+ if (!row)
4442
+ return;
4443
+ setMigrateChecked(current => {
4444
+ const next = new Set(current);
4445
+ if (next.has(row.agentId))
4446
+ next.delete(row.agentId);
4447
+ else
4448
+ next.add(row.agentId);
4449
+ return next;
4450
+ });
4451
+ } }) })), overlay.kind === 'migrate-confirm' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(MigrateConfirm, { rows: migratePending }) })), overlay.kind === 'skills' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: skillsList === null ? (_jsx(SkillsPickerLoading, {})) : (_jsx(SkillsPicker, { skills: skillsList, focusIndex: overlay.index, onPick: (index) => {
3902
4452
  const skill = skillsList[index];
3903
4453
  if (!skill)
3904
4454
  return;
@@ -3984,18 +4534,21 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3984
4534
  const path = overlay.path;
3985
4535
  dispatchOverlay({ type: 'close' });
3986
4536
  runFileAction(index, path);
3987
- } }) })), overlay.kind === 'search' && _jsx(TranscriptSearch, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent })] }))] })] }), _jsx(TooltipLayer, { invalidationKey: `${overlay.kind}:${dialogOverlayOpen}:${btw !== null}`, subscribeInvalidation: subscribeTooltipInvalidation }), _jsx(PromptEditorLayer, {}), promptEditorOpen && imagePreviewNode] }));
4537
+ } }) })), overlay.kind === 'search' && _jsx(TranscriptSearch, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent })] }))] })] }), _jsx(TooltipLayer, { invalidationKey: `${overlay.kind}:${dialogOverlayOpen}:${btw !== null}`, subscribeInvalidation: subscribeTooltipInvalidation }), _jsx(PromptEditorLayer, {}), promptEditorOpen && imagePreviewNode, starModal !== null && STAR_MILESTONES[starModal.index] !== undefined && (_jsx(StarPrompt, { milestone: STAR_MILESTONES[starModal.index], actions: starModalActions, onClose: closeStarModal, initialPhase: starModal.phase }))] }));
3988
4538
  }
3989
4539
  /**
3990
4540
  * The pinned prompt header shown above the ScrollBox while the user has
3991
- * scrolled up. It pins the user message the transcript viewport is currently
3992
- * showing — the topmost visible user message, or the nearest one above when only assistant
3993
- * content fills the view — so it tracks which turn the user is reading
3994
- * instead of always carrying the latest prompt. Fixed at 1 row so the
3995
- * ScrollBox never shifts when the text changes.
4541
+ * scrolled up. It pins the prompt of the turn the viewport top is showing
4542
+ * once that prompt has scrolled out above it, so it tracks which turn the
4543
+ * user is reading instead of always carrying the latest prompt. With no
4544
+ * such prompt (it still sits on the top row, or the logo owns the top) the
4545
+ * row renders blank rather than repeat on-screen text. Fixed at 1 row so
4546
+ * the ScrollBox never shifts when the text changes or goes blank.
3996
4547
  */
3997
4548
  function PinnedTurnHeader({ text, onClick, }) {
3998
4549
  const { columns } = useTerminalSize();
4550
+ if (text === null)
4551
+ return _jsx(Box, { flexShrink: 0, width: "100%", height: 1 });
3999
4552
  // A one-row Box does not clip its children. Flatten hard line breaks before
4000
4553
  // truncating, otherwise later prompt lines paint down the transcript gutter.
4001
4554
  const label = cleanRenderText(`${POINTER} ${text}`, Math.max(1, columns - 1));