@deepseek-harness-tui/dsh-tui 0.9.0 → 0.9.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 (307) hide show
  1. package/README.md +69 -256
  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 +6 -0
  26. package/lib/types/components/AutoRecapRow.d.ts +23 -0
  27. package/lib/types/components/AutoRecapRow.d.ts.map +1 -0
  28. package/lib/types/components/AutoRecapRow.js +23 -0
  29. package/lib/types/components/ColorPicker.d.ts +17 -0
  30. package/lib/types/components/ColorPicker.d.ts.map +1 -0
  31. package/lib/types/components/ColorPicker.js +20 -0
  32. package/lib/types/components/CommandSuggestions.d.ts +3 -1
  33. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  34. package/lib/types/components/ContextBarView.d.ts +33 -0
  35. package/lib/types/components/ContextBarView.d.ts.map +1 -0
  36. package/lib/types/components/ContextBarView.js +46 -0
  37. package/lib/types/components/EffortInputBorder.d.ts +12 -1
  38. package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
  39. package/lib/types/components/EffortInputBorder.js +22 -5
  40. package/lib/types/components/FileActionsPanel.d.ts +22 -0
  41. package/lib/types/components/FileActionsPanel.d.ts.map +1 -0
  42. package/lib/types/components/FileActionsPanel.js +28 -0
  43. package/lib/types/components/FileSuggestions.d.ts +3 -1
  44. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  45. package/lib/types/components/MessageList.d.ts +3 -1
  46. package/lib/types/components/MessageList.d.ts.map +1 -1
  47. package/lib/types/components/MessageList.js +13 -6
  48. package/lib/types/components/ModelPicker.d.ts +26 -4
  49. package/lib/types/components/ModelPicker.d.ts.map +1 -1
  50. package/lib/types/components/ModelPicker.js +26 -6
  51. package/lib/types/components/PromptInput.d.ts.map +1 -1
  52. package/lib/types/components/PromptInput.js +365 -57
  53. package/lib/types/components/RecapPanel.d.ts +25 -0
  54. package/lib/types/components/RecapPanel.d.ts.map +1 -0
  55. package/lib/types/components/RecapPanel.js +55 -0
  56. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
  57. package/lib/types/components/ScrollbarGutter.js +55 -4
  58. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  59. package/lib/types/components/SubagentCard.js +6 -4
  60. package/lib/types/components/SuggestionCard.d.ts +4 -2
  61. package/lib/types/components/SuggestionCard.d.ts.map +1 -1
  62. package/lib/types/components/TimelineRail.d.ts.map +1 -1
  63. package/lib/types/components/TimelineRail.js +9 -2
  64. package/lib/types/components/design-system/ThemeProvider.d.ts +7 -0
  65. package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
  66. package/lib/types/components/design-system/ThemeProvider.js +2 -1
  67. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
  68. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  69. package/lib/types/components/messages/AssistantToolUseMessage.js +36 -8
  70. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +7 -3
  71. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  72. package/lib/types/components/questions/AskUserQuestionPanel.js +41 -9
  73. package/lib/types/dsh-adapter/channel.d.ts +101 -2
  74. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  75. package/lib/types/dsh-adapter/channel.js +969 -93
  76. package/lib/types/dsh-adapter/compat/index.d.ts +8 -6
  77. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  78. package/lib/types/dsh-adapter/compat/index.js +8 -6
  79. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +150 -0
  80. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  81. package/lib/types/dsh-adapter/compat/sessionLog.js +518 -41
  82. package/lib/types/dsh-adapter/contract.d.ts +1 -1
  83. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  84. package/lib/types/dsh-adapter/contract.js +1 -0
  85. package/lib/types/dsh-adapter/extension-events.d.ts +1 -1
  86. package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
  87. package/lib/types/dsh-adapter/index.d.ts +10 -0
  88. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  89. package/lib/types/dsh-adapter/index.js +5 -0
  90. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  91. package/lib/types/dsh-adapter/plugin.js +317 -8
  92. package/lib/types/dsh-adapter/presets.d.ts +18 -0
  93. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  94. package/lib/types/dsh-adapter/presets.js +31 -0
  95. package/lib/types/dsh-adapter/providerWizard.d.ts +29 -1
  96. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  97. package/lib/types/dsh-adapter/providerWizard.js +89 -5
  98. package/lib/types/dsh-adapter/questions.d.ts +19 -2
  99. package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
  100. package/lib/types/dsh-adapter/questions.js +53 -10
  101. package/lib/types/dsh-adapter/recap.d.ts +49 -0
  102. package/lib/types/dsh-adapter/recap.d.ts.map +1 -0
  103. package/lib/types/dsh-adapter/recap.js +121 -0
  104. package/lib/types/dsh-adapter/sessionTree.d.ts +314 -0
  105. package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -0
  106. package/lib/types/dsh-adapter/sessionTree.js +954 -0
  107. package/lib/types/dsh-adapter/shortcuts.d.ts +15 -44
  108. package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
  109. package/lib/types/dsh-adapter/shortcuts.js +28 -165
  110. package/lib/types/dsh-adapter/types.d.ts +1 -1
  111. package/lib/types/dsh-adapter/types.d.ts.map +1 -1
  112. package/lib/types/herdr.js +1 -1
  113. package/lib/types/i18n.d.ts +604 -176
  114. package/lib/types/i18n.d.ts.map +1 -1
  115. package/lib/types/i18n.js +206 -72
  116. package/lib/types/ink/log-update.d.ts.map +1 -1
  117. package/lib/types/ink/log-update.js +21 -0
  118. package/lib/types/migrationPrefs.d.ts +60 -0
  119. package/lib/types/migrationPrefs.d.ts.map +1 -0
  120. package/lib/types/migrationPrefs.js +97 -0
  121. package/lib/types/modelGroups.d.ts +72 -0
  122. package/lib/types/modelGroups.d.ts.map +1 -0
  123. package/lib/types/modelGroups.js +97 -0
  124. package/lib/types/modelRecents.d.ts +39 -0
  125. package/lib/types/modelRecents.d.ts.map +1 -0
  126. package/lib/types/modelRecents.js +97 -0
  127. package/lib/types/native-ts/yoga-layout/index.d.ts +11 -0
  128. package/lib/types/native-ts/yoga-layout/index.d.ts.map +1 -1
  129. package/lib/types/native-ts/yoga-layout/index.js +31 -1
  130. package/lib/types/oauth.d.ts +15 -0
  131. package/lib/types/oauth.d.ts.map +1 -0
  132. package/lib/types/oauth.js +14 -0
  133. package/lib/types/plugin-spec/registry.d.ts +1 -1
  134. package/lib/types/plugin-spec/registry.js +1 -1
  135. package/lib/types/reload.d.ts +109 -0
  136. package/lib/types/reload.d.ts.map +1 -0
  137. package/lib/types/reload.js +118 -0
  138. package/lib/types/screens/Chat.d.ts +3 -1
  139. package/lib/types/screens/Chat.d.ts.map +1 -1
  140. package/lib/types/screens/Chat.js +588 -64
  141. package/lib/types/screens/SessionTree.d.ts +27 -0
  142. package/lib/types/screens/SessionTree.d.ts.map +1 -0
  143. package/lib/types/screens/SessionTree.js +555 -0
  144. package/lib/types/screens/StatusLine.d.ts +0 -9
  145. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  146. package/lib/types/screens/StatusLine.js +215 -52
  147. package/lib/types/screens/StatusMetrics.d.ts +35 -3
  148. package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
  149. package/lib/types/screens/StatusMetrics.js +63 -8
  150. package/lib/types/screens/chatOverlay.d.ts +17 -1
  151. package/lib/types/screens/chatOverlay.d.ts.map +1 -1
  152. package/lib/types/screens/chatOverlay.js +3 -1
  153. package/lib/types/tips.d.ts.map +1 -1
  154. package/lib/types/tips.js +22 -4
  155. package/lib/types/update.d.ts +159 -0
  156. package/lib/types/update.d.ts.map +1 -1
  157. package/lib/types/update.js +496 -30
  158. package/lib/types/utils/debug.d.ts +1 -1
  159. package/lib/types/utils/debug.js +3 -3
  160. package/lib/types/utils/fileTarget.d.ts +46 -0
  161. package/lib/types/utils/fileTarget.d.ts.map +1 -0
  162. package/lib/types/utils/fileTarget.js +178 -0
  163. package/lib/types/utils/keymap.d.ts +134 -0
  164. package/lib/types/utils/keymap.d.ts.map +1 -0
  165. package/lib/types/utils/keymap.js +311 -0
  166. package/lib/types/utils/mentions.d.ts +25 -2
  167. package/lib/types/utils/mentions.d.ts.map +1 -1
  168. package/lib/types/utils/mentions.js +69 -10
  169. package/lib/types/utils/openExternal.d.ts +50 -0
  170. package/lib/types/utils/openExternal.d.ts.map +1 -0
  171. package/lib/types/utils/openExternal.js +170 -0
  172. package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -0
  173. package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +140 -0
  174. package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +11 -0
  175. package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -0
  176. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.d.ts +14 -0
  177. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.js +67 -0
  178. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +64 -0
  179. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.js +170 -0
  180. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +79 -0
  181. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +196 -0
  182. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +68 -0
  183. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +193 -0
  184. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.d.ts +32 -0
  185. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.js +101 -0
  186. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +31 -0
  187. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +68 -0
  188. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.d.ts +69 -0
  189. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.js +129 -0
  190. package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +70 -0
  191. package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
  192. package/node_modules/@dsh-std/command/LICENSE +21 -21
  193. package/node_modules/@dsh-std/command/README.md +26 -26
  194. package/node_modules/@dsh-std/command/README.zh.md +28 -28
  195. package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
  196. package/node_modules/@dsh-std/command/package.json +22 -22
  197. package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  198. package/node_modules/@dsh-std/connection/LICENSE +21 -21
  199. package/node_modules/@dsh-std/connection/README.md +42 -42
  200. package/node_modules/@dsh-std/connection/README.zh.md +42 -42
  201. package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  202. package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  203. package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  204. package/node_modules/@dsh-std/connection/package.json +20 -20
  205. package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  206. package/node_modules/@dsh-std/core/LICENSE +21 -21
  207. package/node_modules/@dsh-std/core/README.md +13 -13
  208. package/node_modules/@dsh-std/core/README.zh.md +13 -13
  209. package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  210. package/node_modules/@dsh-std/core/package.json +21 -21
  211. package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
  212. package/node_modules/@dsh-std/manifest/LICENSE +21 -21
  213. package/node_modules/@dsh-std/manifest/README.md +5 -5
  214. package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
  215. package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
  216. package/node_modules/@dsh-std/manifest/package.json +20 -20
  217. package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
  218. package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
  219. package/node_modules/@dsh-std/messages/LICENSE +21 -21
  220. package/node_modules/@dsh-std/messages/README.md +5 -5
  221. package/node_modules/@dsh-std/messages/README.zh.md +7 -7
  222. package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
  223. package/node_modules/@dsh-std/messages/package.json +20 -20
  224. package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
  225. package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
  226. package/node_modules/@dsh-std/presentation/LICENSE +21 -21
  227. package/node_modules/@dsh-std/presentation/README.md +27 -27
  228. package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
  229. package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
  230. package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
  231. package/node_modules/@dsh-std/presentation/package.json +25 -25
  232. package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
  233. package/node_modules/@dsh-std/storage/LICENSE +21 -21
  234. package/node_modules/@dsh-std/storage/README.md +5 -5
  235. package/node_modules/@dsh-std/storage/README.zh.md +7 -7
  236. package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
  237. package/node_modules/@dsh-std/storage/package.json +20 -20
  238. package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
  239. package/package.json +17 -5
  240. package/presets/liangshen/.dsh-tui-managed.json +1 -1
  241. package/presets/liangshen/instruction-hint.mjs +8 -0
  242. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  243. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
  244. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
  245. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
  246. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  247. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  248. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  249. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
  250. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  251. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
  252. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
  253. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
  254. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  255. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
  256. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
  257. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
  258. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
  259. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
  260. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
  261. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
  262. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
  263. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
  264. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
  265. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
  266. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
  267. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
  268. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
  269. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
  270. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  271. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
  272. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
  273. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
  274. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  275. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
  276. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  277. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
  278. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
  279. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
  280. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  281. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
  282. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  283. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
  284. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
  285. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
  286. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  287. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
  288. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  289. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
  290. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
  291. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
  292. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  293. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  294. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  295. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
  296. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  297. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
  298. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
  299. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
  300. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  301. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
  302. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  303. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
  304. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
  305. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
  306. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  307. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
@@ -3,21 +3,25 @@ import { assembleContextFor, installModelSelection } from '@deepseek-ai/dsh-agen
3
3
  import { isUserInvocable, renderSkillContent } from '@deepseek-ai/dsh-skill';
4
4
  import { createUserMessage, isTokenDelta, MessageId, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
5
5
  import { runSideQuestion, wrapSideQuestion } from './sideQuestion.js';
6
+ import { collectRecentActivity, parseRecapResponse, RECAP_RECENT_CHARS, wrapRecapPrompt } from './recap.js';
7
+ import { SESSION_COLOR_NAMES } from '../cc/sessionColors.js';
6
8
  import { SessionId } from '@deepseek-ai/dsh-session';
7
9
  import { renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt';
8
10
  import { loadBaselineInstructions } from '@deepseek-ai/dsh-agent-instructions';
9
11
  import { extname, isAbsolute, join } from 'node:path';
10
12
  import { completeCommands, HIDDEN_COMMAND_NAMES, isLocalCommandName, LOCAL_COMMANDS, parseCommandName } from '../commands.js';
11
13
  import { clearResumeTarget, forgetSession, readResumeTarget, touchSession, writeResumeTarget } from '../sessionHistory.js';
12
- import { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, sessionsRoots } from './compat/index.js';
13
- import { listSummaries, locateSession, noteBranch, previewSession, } from './sessions/index.js';
14
+ import { appendSessionTitle, defaultMaxScanned, deleteSessionLog, ensureLegacySessionEventTypes, readSessionEventsFromFile, readSessionEventsFromLog, sessionsRoots } from './compat/index.js';
15
+ import { buildSessionTree, forkTarget, liveTailWindow, rewindTarget, turnUserText, } from './sessionTree.js';
16
+ import { resolveDshProfileName } from '../update.js';
17
+ import { listSummaries, locateSession, noteBranch, previewSession, readHeader, } from './sessions/index.js';
14
18
  import { writeActivityFrames } from '../activityPrefs.js';
15
19
  import { isPathLikeQuery, rankFileCandidates } from '../utils/fileSuggestions.js';
16
20
  import { readEffortPref, writeEffortPref } from '../effortPrefs.js';
17
21
  import { readModelPref, writeModelPref } from '../modelPrefs.js';
18
22
  import { explicitModelRoute, recordedModelRoute, resolveModelRoute, validateModelRoute } from '../modelRoute.js';
19
23
  import { readPresetPref, writePresetPref } from '../presetPrefs.js';
20
- import { composePreset, resolvePersistedPreset, rosterOf, runningPresetOf, serviceForAgent } from './presets.js';
24
+ import { composePreset, resolvePersistedPreset, resolvePersistedRoute, rosterOf, runningPresetOf, serviceForAgent } from './presets.js';
21
25
  import { isPresetName, PRESET_NAMES } from '../components/activityFrames.js';
22
26
  import { existsSync, statSync, writeFileSync } from 'node:fs';
23
27
  import { logForDebugging } from '../utils/debug.js';
@@ -866,6 +870,79 @@ export function createChannel(ctx, initialAgent, options) {
866
870
  ctx.logger.warn('dsh-tui: tui/session-switched dispatch failed: %o', error);
867
871
  }
868
872
  };
873
+ /**
874
+ * Swap the live agent for a freshly created fork (rewindTo and the session
875
+ * tree's rewindToNode share this tail): reset every session-scoped
876
+ * projection, replay the fork's seed into a fresh transcript (tokens/
877
+ * spinner counters land back at the rewind point, matching the fork),
878
+ * rebind subscriptions to the new agent, and free the replaced handle.
879
+ * Returns the source session's id (for the session-switched notification).
880
+ */
881
+ const adoptForkedAgent = (handle, seed, agentPreset, childId) => {
882
+ // Replay the forked history into a fresh transcript (tokens/spinner
883
+ // counters land back at the rewind point, matching the fork).
884
+ streaming = undefined;
885
+ reasoning = undefined;
886
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
887
+ // keep it out of the next turn's settle logs and revive cache.
888
+ sealedReasoning.length = 0;
889
+ lastReasoningRow = undefined;
890
+ toolCards.clear();
891
+ nextRowId = 0;
892
+ state.rows.length = 0;
893
+ // Goal/todo/title are session-scoped; the replay re-derives them for
894
+ // the session being entered (or leaves them empty).
895
+ state.todos = [];
896
+ // Queued-but-undelivered messages live in the OLD agent's inbox; the
897
+ // swap must drop their previews or they linger forever (unretirable —
898
+ // retire events are filtered to the new agent, unwithdrawable — the
899
+ // new inbox never heard of them).
900
+ state.pending = [];
901
+ state.goal = undefined;
902
+ state.sessionTitle = '';
903
+ state.sessionColor = '';
904
+ state.tokens = { input: 0, output: 0 };
905
+ state.responseChars = 0;
906
+ state.activeToolCount = 0;
907
+ state.lastUserText = '';
908
+ state.working = false;
909
+ state.spinnerMode = 'requesting';
910
+ state.status = handle.agent.status;
911
+ state.agentId = handle.agent.id;
912
+ state.agentPreset = agentPreset;
913
+ state.tps = undefined;
914
+ state.tpsSamples = [];
915
+ state.lastUsage = undefined;
916
+ state.workingActivity = undefined;
917
+ state.contextSegments = {
918
+ system: 0,
919
+ prompt: 0,
920
+ assistant: 0,
921
+ thinking: 0,
922
+ tools: 0,
923
+ };
924
+ replayEvents(seed);
925
+ settleStreaming();
926
+ // A seed ending mid-turn replays a turn/start that set working=true;
927
+ // the boot path resets this after replay — mirror it here so an idle
928
+ // rewound agent doesn't sit with a live spinner (a still-running
929
+ // agent re-asserts on its next event).
930
+ state.working = handle.agent.status === 'running';
931
+ // Rebind subscriptions to the new agent, then free the old one.
932
+ const oldHandle = currentHandle;
933
+ const sourceSessionId = String(agent.session.id);
934
+ agent = handle.agent;
935
+ currentHandle = handle;
936
+ bindAgent();
937
+ refreshCommandList();
938
+ void refreshLoadedContext();
939
+ void refreshSkillCommands();
940
+ // The forked session (rewind) becomes the most recently used.
941
+ touchSession(childId);
942
+ state.emit();
943
+ void oldHandle?.dispose().catch(() => { });
944
+ return sourceSessionId;
945
+ };
869
946
  /** Monotonic token: only the latest `interruptAndDeliver` re-queues, so a
870
947
  * second interrupt while the abort settles cannot double-deliver. */
871
948
  let interruptSeq = 0;
@@ -1301,6 +1378,17 @@ export function createChannel(ctx, initialAgent, options) {
1301
1378
  rows: [],
1302
1379
  status: 'starting',
1303
1380
  sessionTitle: '',
1381
+ sessionColor: '',
1382
+ get autoRecapOnOpen() {
1383
+ // Live read (not a boot snapshot): a /settings change applies on the
1384
+ // next session switch. No settings service → off (framework absent,
1385
+ // e.g. headless fixtures — nothing to configure and no llm route).
1386
+ const settings = ctx.get('settings');
1387
+ if (settings === undefined)
1388
+ return false;
1389
+ const ns = settings.describe({ redactSecrets: true }).find(entry => entry.ns === 'dsh-tui');
1390
+ return ns?.value?.recapOnOpen !== false;
1391
+ },
1304
1392
  agentId: agent.id,
1305
1393
  model: options.model,
1306
1394
  provider: options.provider,
@@ -1325,10 +1413,16 @@ export function createChannel(ctx, initialAgent, options) {
1325
1413
  modeIndex: 0,
1326
1414
  workingActivity: undefined,
1327
1415
  activityFrames: options.activityFrames,
1416
+ configuredProvider: options.configuredProvider,
1417
+ configuredModel: options.configuredModel,
1418
+ configuredPreset: options.configuredPreset,
1419
+ configuredActivityFrames: options.configuredActivityFrames,
1420
+ configuredLang: options.configuredLang,
1328
1421
  diffLayout: options.diffLayout ?? 'auto',
1329
1422
  thinkingFold: options.thinkingFold ?? 'preview',
1330
1423
  toolBackground: normalizeToolBackground(options.toolBackground),
1331
1424
  scrollGutter: normalizeScrollGutter(options.scrollGutter),
1425
+ promptSessionLabel: options.promptSessionLabel === true,
1332
1426
  statusBar: normalizeStatusBar(options.statusBar),
1333
1427
  whale: options.whale !== false,
1334
1428
  minimal: options.minimal === true,
@@ -1388,6 +1482,18 @@ export function createChannel(ctx, initialAgent, options) {
1388
1482
  })),
1389
1483
  ];
1390
1484
  }
1485
+ if (path.length === 1 && path[0] === 'color') {
1486
+ return [
1487
+ { name: 'status', description: 'Show the current session color', descriptionKey: 'sugg-status-desc' },
1488
+ { name: 'reset', description: 'Clear the session color', descriptionKey: 'sugg-color-reset-desc' },
1489
+ ...SESSION_COLOR_NAMES.map((name) => ({
1490
+ name,
1491
+ description: 'Session accent color',
1492
+ descriptionKey: 'sugg-color-name-desc',
1493
+ ...(state.sessionColor === name ? { tag: 'current' } : {}),
1494
+ })),
1495
+ ];
1496
+ }
1391
1497
  if (path.length === 1 && path[0] === 'effort') {
1392
1498
  warmEffortLevels();
1393
1499
  return [
@@ -1774,67 +1880,9 @@ export function createChannel(ctx, initialAgent, options) {
1774
1880
  catch (error) {
1775
1881
  state.notify(t('rewind-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
1776
1882
  }
1777
- // Replay the forked history into a fresh transcript (tokens/spinner
1778
- // counters land back at the rewind point, matching the fork).
1779
- streaming = undefined;
1780
- reasoning = undefined;
1781
- // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1782
- // keep it out of the next turn's settle logs and revive cache.
1783
- sealedReasoning.length = 0;
1784
- lastReasoningRow = undefined;
1785
- toolCards.clear();
1786
- nextRowId = 0;
1787
- state.rows.length = 0;
1788
- // Goal/todo/title are session-scoped; the replay re-derives them for
1789
- // the session being entered (or leaves them empty).
1790
- state.todos = [];
1791
- // Queued-but-undelivered messages live in the OLD agent's inbox; the
1792
- // swap must drop their previews or they linger forever (unretirable —
1793
- // retire events are filtered to the new agent, unwithdrawable — the
1794
- // new inbox never heard of them).
1795
- state.pending = [];
1796
- state.goal = undefined;
1797
- state.sessionTitle = '';
1798
- state.tokens = { input: 0, output: 0 };
1799
- state.responseChars = 0;
1800
- state.activeToolCount = 0;
1801
- state.lastUserText = '';
1802
- state.working = false;
1803
- state.spinnerMode = 'requesting';
1804
- state.status = handle.agent.status;
1805
- state.agentId = handle.agent.id;
1806
- state.agentPreset = rewindComposed.agentPreset;
1807
- state.tps = undefined;
1808
- state.tpsSamples = [];
1809
- state.lastUsage = undefined;
1810
- state.workingActivity = undefined;
1811
- state.contextSegments = {
1812
- system: 0,
1813
- prompt: 0,
1814
- assistant: 0,
1815
- thinking: 0,
1816
- tools: 0,
1817
- };
1818
- replayEvents(seed);
1819
- settleStreaming();
1820
- // A seed ending mid-turn replays a turn/start that set working=true;
1821
- // the boot path resets this after replay — mirror it here so an idle
1822
- // rewound agent doesn't sit with a live spinner (a still-running
1823
- // agent re-asserts on its next event).
1824
- state.working = handle.agent.status === 'running';
1825
- // Rebind subscriptions to the new agent, then free the old one.
1826
- const oldHandle = currentHandle;
1827
- const sourceSessionId = String(agent.session.id);
1828
- agent = handle.agent;
1829
- currentHandle = handle;
1830
- bindAgent();
1831
- refreshCommandList();
1832
- void refreshLoadedContext();
1833
- void refreshSkillCommands();
1834
- // The forked session (rewind) becomes the most recently used.
1835
- touchSession(childId);
1836
- state.emit();
1837
- void oldHandle?.dispose().catch(() => { });
1883
+ // Swap the live agent for the fork (shared with rewindToNode): replay
1884
+ // the seed, rebind, and free the replaced handle.
1885
+ const sourceSessionId = adoptForkedAgent(handle, seed, rewindComposed.agentPreset, childId);
1838
1886
  // Decision-event pair around the completed rewind: `tui/rewind-done`
1839
1887
  // (the first non-empty string is toasted as the post-rewind summary,
1840
1888
  // e.g. a plugin reporting restored files) and the generic
@@ -1874,6 +1922,685 @@ export function createChannel(ctx, initialAgent, options) {
1874
1922
  notifySessionSwitched('rewind', String(childId), sourceSessionId);
1875
1923
  return row.text;
1876
1924
  },
1925
+ async buildSessionTree() {
1926
+ const persistence = ctx.get('sessionPersistence');
1927
+ if (!persistence) {
1928
+ state.notify(t('tree-unavailable'), { color: 'error' });
1929
+ return null;
1930
+ }
1931
+ // Pin the live session snapshot NOW: every await below (list/inspect)
1932
+ // is a window in which a fire-and-forget switch (/new, /resume,
1933
+ // /model) can swap `agent`. Reading agent.session piecemeal would
1934
+ // stitch the NEW session's events under the OLD session's id — a
1935
+ // confirm would then rewind from the wrong persisted log. Everything
1936
+ // below reads this snapshot, and the result is discarded if the live
1937
+ // session moved on before the build finished.
1938
+ const liveSession = agent.session;
1939
+ const currentId = String(liveSession.id);
1940
+ // Same enumerate as the /resume listing (snapshots when the backend
1941
+ // offers revisions, plain list otherwise), each header narrowed through
1942
+ // the sessions reader — one malformed header costs that session its
1943
+ // metadata, never the whole tree. `raw` stays the backend's own header
1944
+ // object for locate() below.
1945
+ let listed = [];
1946
+ try {
1947
+ if (typeof persistence.listSnapshots === 'function') {
1948
+ const snapshots = await persistence.listSnapshots();
1949
+ listed = snapshots.flatMap(snapshot => {
1950
+ const raw = snapshot?.header;
1951
+ const header = readHeader(raw);
1952
+ return header === undefined ? [] : [{ header, raw }];
1953
+ });
1954
+ }
1955
+ else if (typeof persistence.list === 'function') {
1956
+ const headers = await persistence.list();
1957
+ listed = headers.flatMap(raw => {
1958
+ const header = readHeader(raw);
1959
+ return header === undefined ? [] : [{ header, raw }];
1960
+ });
1961
+ }
1962
+ }
1963
+ catch {
1964
+ // A listing failure degrades the tree to the live session only.
1965
+ }
1966
+ // Same cwd scoping as /resume (Claude Code's project dimension): forks
1967
+ // inherit cwd, so the family never crosses projects — and the match is
1968
+ // the project-aware one /resume uses, so a pre-upgrade subdirectory
1969
+ // path, Windows separators, or a case variant on one header cannot
1970
+ // quietly amputate the ancestors and siblings it records. Subagent
1971
+ // child sessions carry parentSession too, but they are delegation
1972
+ // artifacts, not rewind branches — exclude them from the family.
1973
+ const local = listed.filter(entry => sessionCwdMatches(state.cwd, entry.header.cwd ?? '') &&
1974
+ entry.header.origin !== 'subagent' &&
1975
+ (entry.header.delegationDepth ?? 0) === 0);
1976
+ const headerById = new Map(local.map(entry => [entry.header.id, entry]));
1977
+ // The live session's header may not be materialized in list() yet
1978
+ // (the jsonl backend writes on first append) — overlay the in-memory
1979
+ // header so the ancestor walk below still finds a fresh fork's parent.
1980
+ const liveMeta = liveSession.header;
1981
+ if (!headerById.has(currentId) && liveMeta !== undefined) {
1982
+ headerById.set(currentId, { header: readHeader(liveMeta) ?? { id: currentId, cwd: undefined, createdAt: undefined, parentSession: undefined, origin: undefined, delegationDepth: undefined, seedLength: undefined, agentPreset: undefined }, raw: liveMeta });
1983
+ }
1984
+ // Family = the live session's ancestor chain PLUS every descendant of
1985
+ // its topmost known ancestor (siblings and cousins included).
1986
+ const childrenByParent = new Map();
1987
+ for (const entry of local) {
1988
+ if (entry.header.parentSession === undefined)
1989
+ continue;
1990
+ const list = childrenByParent.get(entry.header.parentSession);
1991
+ if (list === undefined)
1992
+ childrenByParent.set(entry.header.parentSession, [entry.header.id]);
1993
+ else
1994
+ list.push(entry.header.id);
1995
+ }
1996
+ const ancestorIds = [];
1997
+ {
1998
+ const visited = new Set([currentId]);
1999
+ let cursor = headerById.get(currentId);
2000
+ while (cursor?.header.parentSession !== undefined) {
2001
+ const parentId = cursor.header.parentSession;
2002
+ if (visited.has(parentId))
2003
+ break;
2004
+ visited.add(parentId);
2005
+ if (!headerById.has(parentId))
2006
+ break;
2007
+ ancestorIds.push(parentId);
2008
+ cursor = headerById.get(parentId);
2009
+ }
2010
+ }
2011
+ // BFS from the topmost ancestor. The scan must NOT be gated by family
2012
+ // membership: ancestor-chain nodes are already in the family, and
2013
+ // skipping them here would never enumerate their other children —
2014
+ // siblings/cousins forking off a MIDDLE ancestor would be lost.
2015
+ const family = new Set([currentId, ...ancestorIds]);
2016
+ {
2017
+ const scanned = new Set();
2018
+ const queue = [ancestorIds.at(-1) ?? currentId];
2019
+ while (queue.length > 0) {
2020
+ const id = queue.shift();
2021
+ if (scanned.has(id))
2022
+ continue;
2023
+ scanned.add(id);
2024
+ family.add(id);
2025
+ for (const child of childrenByParent.get(id) ?? []) {
2026
+ queue.push(child);
2027
+ }
2028
+ }
2029
+ }
2030
+ // Processing order is TOPOLOGICAL (a parent before its children): the
2031
+ // coverage bookkeeping below — which seq range each chain already
2032
+ // shows — feeds the next read's inherited-prefix skip, so a parent
2033
+ // must be read before its forks. Within each sibling group the live
2034
+ // chain wins, then newest first (the same priority the read budget
2035
+ // always had).
2036
+ const ancestorSet = new Set(ancestorIds);
2037
+ const priorityOf = (a, b) => {
2038
+ const aChain = a === currentId || ancestorSet.has(a);
2039
+ const bChain = b === currentId || ancestorSet.has(b);
2040
+ if (aChain !== bChain)
2041
+ return aChain ? -1 : 1;
2042
+ return (headerById.get(b)?.header.createdAt ?? 0) - (headerById.get(a)?.header.createdAt ?? 0);
2043
+ };
2044
+ const kidsOf = new Map();
2045
+ const familyRoots = [];
2046
+ for (const id of family) {
2047
+ const parentId = headerById.get(id)?.header.parentSession;
2048
+ if (parentId !== undefined && parentId !== id && family.has(parentId)) {
2049
+ const list = kidsOf.get(parentId);
2050
+ if (list === undefined)
2051
+ kidsOf.set(parentId, [id]);
2052
+ else
2053
+ list.push(id);
2054
+ }
2055
+ else {
2056
+ familyRoots.push(id);
2057
+ }
2058
+ }
2059
+ familyRoots.sort(priorityOf);
2060
+ for (const list of kidsOf.values())
2061
+ list.sort(priorityOf);
2062
+ const ordered = [];
2063
+ {
2064
+ const seen = new Set();
2065
+ const stack = [...familyRoots].reverse();
2066
+ while (stack.length > 0) {
2067
+ const id = stack.pop();
2068
+ if (seen.has(id))
2069
+ continue;
2070
+ seen.add(id);
2071
+ ordered.push(id);
2072
+ const kids = kidsOf.get(id);
2073
+ if (kids !== undefined) {
2074
+ for (let i = kids.length - 1; i >= 0; i--)
2075
+ stack.push(kids[i]);
2076
+ }
2077
+ }
2078
+ // Cycle-broken leftovers (corrupt parent headers) — never drop one.
2079
+ for (const id of [...family].sort(priorityOf)) {
2080
+ if (!seen.has(id))
2081
+ ordered.push(id);
2082
+ }
2083
+ }
2084
+ // Caps: the ancestor chain + live session ALWAYS stay selected — the
2085
+ // structural invariant (the live branch must reach the family root)
2086
+ // outranks the session cap, which therefore evicts only non-ancestors.
2087
+ // The event budget bounds the READ cost too: non-live logs decode
2088
+ // lazily and stop at the remaining budget (see below), the live
2089
+ // session keeps only its tail.
2090
+ const MAX_TREE_SESSIONS = 24;
2091
+ const MAX_TREE_EVENTS = 200_000;
2092
+ // The event budget alone does NOT bound read cost: skipped envelopes
2093
+ // (ignorable frames, headers) are paid for but never collected, so a
2094
+ // noisy log can return ZERO events and leave the next log a full scan
2095
+ // allowance — 23 logs × ~800k envelopes would block the TUI for
2096
+ // seconds. scanBudget caps the TOTAL envelopes inspected across all
2097
+ // logs (the reader reports its real scanned count); per-log caps
2098
+ // derive from the event budget as before, and the smaller of the two
2099
+ // applies, so one flood cannot starve every later sibling either.
2100
+ const MAX_TREE_SCANNED = defaultMaxScanned(MAX_TREE_EVENTS);
2101
+ let scanBudget = MAX_TREE_SCANNED;
2102
+ const selected = new Set();
2103
+ let slots = MAX_TREE_SESSIONS;
2104
+ for (const id of ordered) {
2105
+ const chain = id === currentId || ancestorSet.has(id);
2106
+ if (chain || slots > 0) {
2107
+ selected.add(id);
2108
+ if (!chain)
2109
+ slots -= 1;
2110
+ }
2111
+ }
2112
+ // The live session's events come from memory (its header may not be
2113
+ // materialized yet — the jsonl backend writes on first append).
2114
+ const liveHeader = headerById.get(currentId);
2115
+ const familySessions = [];
2116
+ let truncated = selected.size < family.size;
2117
+ let eventBudget = 0;
2118
+ // Coverage bookkeeping: coveredThrough(S) = the highest K such that
2119
+ // [0..K] is already displayed by S's chain or an ancestor's. A fork's
2120
+ // inherited seed prefix duplicates that range, so non-live reads SKIP
2121
+ // it (the reader's skipBelowSeq): the prefix still costs scan budget
2122
+ // (its bytes are read and parsed) but NOT the event budget — a fork
2123
+ // of a huge parent pays only for its OWN events, so two small forks
2124
+ // of a 70k-event parent both stay visible. Unreadable/unloaded
2125
+ // sessions are transparent: they claim nothing beyond what their own
2126
+ // ancestors covered, so a fork of a dead branch dedups against the
2127
+ // grandparent instead of hiding its self-contained history.
2128
+ const coveredThrough = new Map();
2129
+ for (const id of ordered) {
2130
+ if (!selected.has(id))
2131
+ continue;
2132
+ const entry = headerById.get(id);
2133
+ if (id === currentId) {
2134
+ const liveParentId = liveHeader?.header.parentSession ?? liveMeta?.parentSession;
2135
+ const liveParent = liveParentId !== undefined ? String(liveParentId) : undefined;
2136
+ const parentCovered = liveParent !== undefined
2137
+ ? (coveredThrough.get(liveParent) ?? -1)
2138
+ : -1;
2139
+ const liveEvents = liveSession.events;
2140
+ const remaining = Math.max(0, MAX_TREE_EVENTS - eventBudget);
2141
+ // The live session's in-memory log is SELF-CONTAINED: a fork's
2142
+ // events still carry the inherited seed prefix, which the parent's
2143
+ // chain already displays (and already charged to the budget).
2144
+ // Skipping it exactly like the non-live reads do keeps a live fork
2145
+ // of a huge parent from spending the whole family budget on
2146
+ // duplicated history and evicting its own siblings.
2147
+ const liveSeed = liveHeader?.header.seedLength ?? liveMeta?.seedLength;
2148
+ const skipBelow = liveParent !== undefined && liveSeed !== undefined
2149
+ ? Math.min(liveSeed, parentCovered + 1)
2150
+ : 0;
2151
+ const own = skipBelow > 0 ? liveEvents.filter(event => event.seq >= skipBelow) : liveEvents;
2152
+ // A live session larger than the remaining budget keeps its TAIL,
2153
+ // aligned to whole turns (sessionTree.liveTailWindow): leftover
2154
+ // entries of a turn whose turn/start was cut away render as
2155
+ // selectable rows that can never rewind; a window holding no
2156
+ // turn/start at all (one oversized LAST turn spans the budget)
2157
+ // retries over the earlier complete turns instead of blacking the
2158
+ // session out. Rewind itself never reads this copy (rewindToNode
2159
+ // forks the real session), so the slice only narrows what the tree
2160
+ // can display.
2161
+ const events = liveTailWindow(own, remaining);
2162
+ // Charge the KEPT tail, not the in-memory length: extraction only
2163
+ // ever touches `events`, and charging the full log would black out
2164
+ // every other family member's budget behind a discarded prefix.
2165
+ eventBudget += events.length;
2166
+ if (events.length !== own.length)
2167
+ truncated = true;
2168
+ familySessions.push({
2169
+ id,
2170
+ createdAt: liveHeader?.header.createdAt ?? liveMeta?.createdAt ?? Date.now(),
2171
+ ...(liveParent !== undefined ? { parentSession: liveParent } : {}),
2172
+ ...(liveHeader?.header.seedLength !== undefined || liveMeta?.seedLength !== undefined
2173
+ ? { seedLength: liveHeader?.header.seedLength ?? liveMeta.seedLength }
2174
+ : {}),
2175
+ events,
2176
+ live: true,
2177
+ // The in-memory log always reaches the tip (liveTailWindow trims
2178
+ // the head only), so the adopt/warning UX facts are derivable.
2179
+ tailComplete: true,
2180
+ });
2181
+ // A kept tail cut off the front connects to nothing — coverage
2182
+ // stays at the parent's (a fork of the live session re-reads the
2183
+ // hidden prefix from its own log).
2184
+ const firstKept = events.length > 0 ? events[0].seq : Number.POSITIVE_INFINITY;
2185
+ const lastKept = events.length > 0 ? events[events.length - 1].seq : -1;
2186
+ coveredThrough.set(id, firstKept <= parentCovered + 1 ? Math.max(parentCovered, lastKept) : parentCovered);
2187
+ continue;
2188
+ }
2189
+ const header = entry?.header;
2190
+ const parentId = header?.parentSession;
2191
+ const parentCovered = parentId !== undefined ? (coveredThrough.get(parentId) ?? -1) : -1;
2192
+ // Never skip past the seed prefix: events beyond it are this
2193
+ // session's OWN — no ancestor can show them. A parent that was never
2194
+ // read (evicted, or outside the family) covers nothing (skip 0).
2195
+ const skipBelow = parentId !== undefined && header?.seedLength !== undefined
2196
+ ? Math.min(header.seedLength, parentCovered + 1)
2197
+ : 0;
2198
+ const facts = {
2199
+ id,
2200
+ createdAt: header?.createdAt ?? 0,
2201
+ ...(parentId !== undefined ? { parentSession: parentId } : {}),
2202
+ ...(header?.seedLength !== undefined ? { seedLength: header.seedLength } : {}),
2203
+ };
2204
+ if (eventBudget >= MAX_TREE_EVENTS || scanBudget <= 0) {
2205
+ // Budget spent: keep the STRUCTURE — the session degrades to an
2206
+ // unloaded placeholder so its branch (and any ancestor chain
2207
+ // through it) stays visible instead of vanishing from the tree.
2208
+ truncated = true;
2209
+ familySessions.push({ ...facts, events: [], live: false, unloaded: true });
2210
+ coveredThrough.set(id, parentCovered);
2211
+ continue;
2212
+ }
2213
+ // Read-only, tolerant, bounded: the compat reader decodes frames
2214
+ // lazily and stops at the remaining event budget. Browsing the tree
2215
+ // must never REWRITE history logs (the ignorable-marking repair
2216
+ // stays on the explicit resume/rewind path), and the strict backend
2217
+ // inspect would both reject third-party event types wholesale and
2218
+ // parse chunk-heavy logs whole. Header facts come from list().
2219
+ const remaining = MAX_TREE_EVENTS - eventBudget;
2220
+ // Source precedence, all read-only:
2221
+ // 1. persistence.locate — the backend's OWN artifact resolution is
2222
+ // authoritative (custom root, workspace-key scheme). When it
2223
+ // names a path, ONLY that file is read: falling back to a
2224
+ // same-id copy under the stock root could surface a STALE log
2225
+ // from another backend configuration. A locate miss or an ABSENT
2226
+ // file falls through to inspect, never to the stock scan.
2227
+ // 2. Stock root scan — only for backends WITHOUT locate (fakes,
2228
+ // older custom implementations).
2229
+ // 3. inspect — the backend's strict read (non-file backends), with
2230
+ // the same budget enforced on what we keep — and ONLY when the
2231
+ // file read found NOTHING (undefined). A read that failed on a
2232
+ // safety cap or corruption (failed) must never escalate here:
2233
+ // inspect parses the WHOLE log up front, so falling through
2234
+ // would re-read unboundedly exactly the logs the caps exist to
2235
+ // bound (64 MiB frames, decode bombs) — degrade to a placeholder
2236
+ // instead.
2237
+ let events;
2238
+ let complete = true;
2239
+ let failed = false;
2240
+ // First seq the chosen source actually covers: the file readers start
2241
+ // at the inherited-prefix skip, inspect always hands the whole log.
2242
+ let readFrom = 0;
2243
+ // Per-log scan allowance: the usual 4×-of-remaining derivation,
2244
+ // clamped to what the tree-level scan budget still has.
2245
+ const scanAllowance = Math.min(defaultMaxScanned(remaining), scanBudget);
2246
+ const locate = persistence.locate;
2247
+ const hasLocate = typeof locate === 'function';
2248
+ if (hasLocate && entry !== undefined) {
2249
+ let locatedPath;
2250
+ try {
2251
+ const location = locate.call(persistence, entry.raw);
2252
+ // Only the jsonl kind enters the compat file layer — a foreign
2253
+ // kind's artifact is the backend's own format (inspect below).
2254
+ if (location !== null && typeof location === 'object') {
2255
+ const record = location;
2256
+ if (record.kind === 'jsonl' && typeof record.path === 'string') {
2257
+ locatedPath = record.path;
2258
+ }
2259
+ }
2260
+ }
2261
+ catch {
2262
+ // Best effort — a locate hiccup falls through to inspect.
2263
+ }
2264
+ if (locatedPath !== undefined) {
2265
+ const viaPath = readSessionEventsFromFile(locatedPath, remaining, scanAllowance, skipBelow);
2266
+ if (viaPath !== undefined) {
2267
+ scanBudget -= viaPath.scanned;
2268
+ if (viaPath.failed === true)
2269
+ failed = true;
2270
+ else {
2271
+ events = viaPath.events;
2272
+ complete = viaPath.complete;
2273
+ readFrom = skipBelow;
2274
+ }
2275
+ }
2276
+ }
2277
+ }
2278
+ else if (!hasLocate) {
2279
+ const read = readSessionEventsFromLog(id, remaining, scanAllowance, skipBelow);
2280
+ if (read !== undefined) {
2281
+ scanBudget -= read.scanned;
2282
+ if (read.failed === true)
2283
+ failed = true;
2284
+ else {
2285
+ events = read.events;
2286
+ complete = read.complete;
2287
+ readFrom = skipBelow;
2288
+ }
2289
+ }
2290
+ }
2291
+ if (!failed && events === undefined && typeof persistence.inspect === 'function') {
2292
+ try {
2293
+ const inspection = await persistence.inspect(SessionId(id));
2294
+ // inspect parses the WHOLE log up front: charge the full length
2295
+ // to the scan budget (may overdraw; the next iterations skip).
2296
+ scanBudget -= inspection.events.length;
2297
+ // Non-file backends hand back the self-contained log from seq 0:
2298
+ // the inherited-prefix skip the file readers got must apply here
2299
+ // too, or a long prefix would fill the slice and the branch's OWN
2300
+ // events — the only ones nobody else displays — would be cut.
2301
+ const all = skipBelow > 0 ? inspection.events.filter(event => event.seq >= skipBelow) : inspection.events;
2302
+ readFrom = skipBelow;
2303
+ events = all;
2304
+ if (events.length > remaining) {
2305
+ events = events.slice(0, remaining);
2306
+ complete = false;
2307
+ }
2308
+ }
2309
+ catch {
2310
+ events = undefined;
2311
+ }
2312
+ }
2313
+ if (failed || events === undefined) {
2314
+ // An unreadable log keeps the branch structure, no entries — and
2315
+ // stays transparent for coverage, so a fork of this branch dedups
2316
+ // against the grandparent instead of hiding its own history.
2317
+ familySessions.push({ ...facts, events: [], live: false, unreadable: true });
2318
+ coveredThrough.set(id, parentCovered);
2319
+ continue;
2320
+ }
2321
+ eventBudget += events.length;
2322
+ if (!complete)
2323
+ truncated = true;
2324
+ // tailComplete gates the branch-adopt target and the drop-turn
2325
+ // warning: a budget-sliced read lost the tail, and a tip computed
2326
+ // from it would fork mid-branch while claiming to keep everything.
2327
+ familySessions.push({ ...facts, events, live: false, ...(complete ? { tailComplete: true } : {}) });
2328
+ const lastRead = events.length > 0 ? events[events.length - 1].seq : -1;
2329
+ coveredThrough.set(id, readFrom <= parentCovered + 1 ? Math.max(parentCovered, lastRead) : parentCovered);
2330
+ }
2331
+ // A session swap mid-build invalidates the whole assembly (it mixes
2332
+ // the snapshot's lineage with headers listed for the OLD cwd state):
2333
+ // drop it silently — the reopened tree rebuilds on the new session.
2334
+ if (agent.session !== liveSession)
2335
+ return null;
2336
+ return buildSessionTree(familySessions, currentId, truncated);
2337
+ },
2338
+ async rewindToNode(sessionId, seq, mode = 'rewind') {
2339
+ const agents = ctx.get('agents');
2340
+ if (!agents) {
2341
+ state.notify(t('rewind-unavailable'), { color: 'error' });
2342
+ return null;
2343
+ }
2344
+ // Pin the entry-time session: the awaits below (log load, preset
2345
+ // compose, agent create) are windows in which a queued switch
2346
+ // (/new, /resume, /model) can swap `agent` — the mutation queue only
2347
+ // serializes the REPLACING entries, so the boundary and restored text
2348
+ // derive from THIS session's log and any swap along the way aborts
2349
+ // the rewind (forking or disposing whatever agent happens to be
2350
+ // current at the end would rewind the wrong session).
2351
+ const entrySession = agent.session;
2352
+ const currentId = String(entrySession.id);
2353
+ const childId = SessionId(randomUUID());
2354
+ // Source events: the live session from memory; any other family member
2355
+ // from its durable log (legacy event types registered first — the same
2356
+ // in-process compat seam as resumeTo, since load validates known types).
2357
+ let sourceEvents;
2358
+ let sourceCwd = state.cwd;
2359
+ let forkFromLive = true;
2360
+ if (sessionId === currentId) {
2361
+ sourceEvents = entrySession.events;
2362
+ }
2363
+ else {
2364
+ forkFromLive = false;
2365
+ const persistence = ctx.get('sessionPersistence');
2366
+ if (!persistence || typeof persistence.load !== 'function') {
2367
+ state.notify(t('rewind-no-persistence'), { color: 'error' });
2368
+ return null;
2369
+ }
2370
+ try {
2371
+ ensureLegacySessionEventTypes();
2372
+ const loaded = await persistence.load(SessionId(sessionId));
2373
+ sourceEvents = loaded.events;
2374
+ sourceCwd = loaded.meta.cwd ?? state.cwd;
2375
+ }
2376
+ catch (error) {
2377
+ const message = error instanceof Error ? error.message : String(error);
2378
+ state.notify(t('rewind-load-failed', { err: message }), { color: 'error' });
2379
+ return null;
2380
+ }
2381
+ }
2382
+ // DSH event order is `turn/start → user/message → … → turn/end`, and a
2383
+ // fork seed must not end inside an open turn. pi's navigateTree
2384
+ // semantics mapped onto that constraint (sessionTree.rewindTarget): a
2385
+ // USER message drops its turn — the boundary sits just before the
2386
+ // turn/start and the prompt comes back into the input for re-editing;
2387
+ // any OTHER entry keeps through its enclosing STEP — a mid-turn cut at
2388
+ // the step/end with the turn closed synthetically (DSH agentic turns
2389
+ // span thousands of events, so turn-granular keeping would barely move
2390
+ // the visible history). Fork mode (pi's /fork) instead KEEPS the
2391
+ // picked entry: a user message cuts right after itself (the turn's
2392
+ // reply drops) and never returns text to the input.
2393
+ const target = mode === 'fork'
2394
+ ? forkTarget(sourceEvents, seq)
2395
+ : rewindTarget(sourceEvents, seq);
2396
+ const boundary = target.boundary;
2397
+ if (boundary < 0) {
2398
+ state.notify(t('rewind-first-message'), { color: 'error' });
2399
+ return null;
2400
+ }
2401
+ // Keeping the entry can still be a NO-OP: when nothing message-bearing
2402
+ // follows the boundary (only a turn/end, or nothing at all), the fork's
2403
+ // transcript would be identical to the live one. pi truncates to right
2404
+ // after the entry; DSH's step/turn-closed seed cannot always express
2405
+ // that, so the honest answer is to say there is nothing to rewind. A
2406
+ // DEAD session's tip still forks: that adopts the branch, a real
2407
+ // switch.
2408
+ if (forkFromLive && !sourceEvents.some(event => event.seq > boundary &&
2409
+ (event.type === 'user/message' || event.type === 'assistant/message' ||
2410
+ event.type === 'tool/call' || event.type === 'tool/result'))) {
2411
+ state.notify(t('rewind-noop'), { color: 'warning' });
2412
+ return null;
2413
+ }
2414
+ // The dropped turn's own prompt text, restored into the input after
2415
+ // the swap ('' whenever the entry was kept — fork mode included — or
2416
+ // the turn had no human-typed text to restore).
2417
+ const restoredText = mode === 'fork' ? '' : turnUserText(sourceEvents, seq);
2418
+ // The fork continues under the source session's own preset: switches
2419
+ // are blank-only, so every `agent-preset/selected` event predates any
2420
+ // rewind boundary and the source log resolves the exact composition.
2421
+ // The route likewise stays the live one — a rewind continues the same
2422
+ // conversation, so a `/model` switch must survive it (issue #30).
2423
+ const sourcePreset = forkFromLive
2424
+ ? runningPresetOf(entrySession)
2425
+ : ((await resolvePersistedPreset(ctx, SessionId(sessionId))) ?? runningPresetOf(entrySession));
2426
+ const rewindComposed = await composePreset(ctx, sourcePreset);
2427
+ // Everything fallible is done — only NOW stop a running turn (a load
2428
+ // or preset failure above must not kill it). But bail first when the
2429
+ // live session was swapped during those awaits: cancelling/forking
2430
+ // now would hit the NEW session with THIS session's boundary.
2431
+ if (agent.session !== entrySession) {
2432
+ state.notify(t('rewind-session-changed'), { color: 'error' });
2433
+ return null;
2434
+ }
2435
+ // Stop a running turn first and WAIT for its turn/end to land: fork
2436
+ // rejects boundaries inside open turns, and Agent.cancel() closes the
2437
+ // turn asynchronously (a long thinking turn can take seconds to
2438
+ // settle). Cross-session rewinds need this too: the live agent is
2439
+ // about to be disposed, and its turn must close cleanly.
2440
+ const wasWorking = state.working;
2441
+ const cancelSeq = agent.session.seq;
2442
+ if (wasWorking)
2443
+ agent.cancel({ kind: 'user' });
2444
+ if (wasWorking) {
2445
+ const turnSettled = await waitForTurnEnd(agent.session, cancelSeq, 30000);
2446
+ if (!turnSettled) {
2447
+ state.notify(t('rewind-settling'), { color: 'error' });
2448
+ return null;
2449
+ }
2450
+ }
2451
+ // Slice the seed from the PINNED event snapshot. Never sessions.fork
2452
+ // here: fork() rejects a boundary inside an open turn, which is
2453
+ // exactly where a keep-style cut lands (closeTurn set) — close it
2454
+ // with the exact event a real user interrupt writes instead (the
2455
+ // persistence layer closes crash-orphaned turns the same way).
2456
+ // agents.create validates the result itself (contiguous from seq 0,
2457
+ // no open turns).
2458
+ const seed = sourceEvents.filter(event => event.seq <= boundary);
2459
+ if (target.closeTurn !== undefined) {
2460
+ const last = seed[seed.length - 1];
2461
+ if (last !== undefined) {
2462
+ seed.push({
2463
+ type: 'turn/end',
2464
+ seq: last.seq + 1,
2465
+ time: last.time + 1,
2466
+ data: { turn: target.closeTurn, reason: { kind: 'aborted', reason: { kind: 'user' } } },
2467
+ });
2468
+ }
2469
+ }
2470
+ let handle;
2471
+ try {
2472
+ handle = await agents.create({
2473
+ sessionId: childId,
2474
+ seed,
2475
+ meta: {
2476
+ cwd: sourceCwd,
2477
+ parentSession: SessionId(sessionId),
2478
+ seedLength: seed.length,
2479
+ ...(rewindComposed.agentPreset === undefined
2480
+ ? {}
2481
+ : { agentPreset: rewindComposed.agentPreset }),
2482
+ },
2483
+ agentOptions: { provider: state.provider, model: state.model },
2484
+ ...(rewindComposed.setup === undefined ? {} : { setup: rewindComposed.setup }),
2485
+ });
2486
+ }
2487
+ catch {
2488
+ state.notify(t('rewind-create-failed'), { color: 'error' });
2489
+ return null;
2490
+ }
2491
+ try {
2492
+ await attachSessionToWorkspace(ctx, sourceCwd, childId);
2493
+ }
2494
+ catch (error) {
2495
+ state.notify(t('rewind-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
2496
+ }
2497
+ // The create await was another swap window: adopting now would dispose
2498
+ // the NEW session's agent. Free the fork we just made and bail.
2499
+ if (agent.session !== entrySession) {
2500
+ void handle.dispose().catch(() => { });
2501
+ state.notify(t('rewind-session-changed'), { color: 'error' });
2502
+ return null;
2503
+ }
2504
+ // Replay the forked history into a fresh transcript (the same swap
2505
+ // tail rewindTo runs), then announce the session switch.
2506
+ const sourceSessionId = adoptForkedAgent(handle, seed, rewindComposed.agentPreset, childId);
2507
+ notifySessionSwitched(mode === 'fork' ? 'fork' : 'rewind', String(childId), sourceSessionId);
2508
+ return restoredText;
2509
+ },
2510
+ async forkSession() {
2511
+ const sessions = ctx.get('sessions');
2512
+ const agents = ctx.get('agents');
2513
+ if (!sessions || !agents) {
2514
+ state.notify(t('fork-unavailable'), { color: 'error' });
2515
+ return false;
2516
+ }
2517
+ // kimi-code /fork semantics: refuse mid-turn instead of cancelling —
2518
+ // the fork must not surprise the user by killing their running turn,
2519
+ // and sessions.fork rejects an open-turn log anyway.
2520
+ if (state.working) {
2521
+ state.notify(t('fork-while-working'), { color: 'warning' });
2522
+ return false;
2523
+ }
2524
+ const source = agent.session;
2525
+ const childId = SessionId(randomUUID());
2526
+ // No boundary: the whole (turn-closed) log. Slice via sessions.fork for
2527
+ // the same validation the rewind path gets, never sessions.fork's
2528
+ // session-storing sibling — agents.create must own the new session.
2529
+ let seed;
2530
+ try {
2531
+ seed = sessions.fork(source).events;
2532
+ }
2533
+ catch (error) {
2534
+ const message = error instanceof Error ? error.message : String(error);
2535
+ state.notify(t('fork-failed', { err: message }), { color: 'error' });
2536
+ return false;
2537
+ }
2538
+ // Same preset/route rule as a rewind fork: the source log's own
2539
+ // composition, the live route (a /model switch survives forking).
2540
+ const forkComposed = await composePreset(ctx, runningPresetOf(source));
2541
+ let handle;
2542
+ try {
2543
+ handle = await agents.create({
2544
+ sessionId: childId,
2545
+ seed,
2546
+ meta: {
2547
+ cwd: state.cwd,
2548
+ // NO parentSession: a /fork copy is an independent conversation
2549
+ // (kimi-code semantics — a copy of the message list under a new
2550
+ // root session, like /new plus the history), not a rewind branch.
2551
+ // Recording lineage would fold it into the source's family in
2552
+ // /resume and the user would never find it.
2553
+ seedLength: seed.length,
2554
+ ...(forkComposed.agentPreset === undefined
2555
+ ? {}
2556
+ : { agentPreset: forkComposed.agentPreset }),
2557
+ },
2558
+ agentOptions: { provider: state.provider, model: state.model },
2559
+ ...(forkComposed.setup === undefined ? {} : { setup: forkComposed.setup }),
2560
+ });
2561
+ }
2562
+ catch {
2563
+ state.notify(t('fork-create-failed'), { color: 'error' });
2564
+ return false;
2565
+ }
2566
+ // STAY in the source session: adopting the fork would dispose the live
2567
+ // agent (killing its in-flight turn and background tasks) — the fork is
2568
+ // an independent copy the user enters via /resume or the printed resume
2569
+ // command. The teardown order matters:
2570
+ // 1. attach while the fork's agent is still LIVE — the workspace's
2571
+ // header read resolves live sessions from the registry, so attaching
2572
+ // after dispose races the persistence index and can fail with
2573
+ // "cannot validate session".
2574
+ // 2. await the dispose so the seed log finishes flushing…
2575
+ // 3. …then append the Fork: title — appending mid-flush races the
2576
+ // writer and the frame is silently dropped.
2577
+ try {
2578
+ await attachSessionToWorkspace(ctx, state.cwd, childId);
2579
+ }
2580
+ catch (error) {
2581
+ state.notify(t('fork-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
2582
+ }
2583
+ try {
2584
+ await handle.dispose();
2585
+ }
2586
+ catch (error) {
2587
+ ctx.logger.warn('dsh-tui: forked session dispose failed: %o', error);
2588
+ }
2589
+ // kimi's naming convention: the fork wears `Fork: <source title>` (the
2590
+ // prefix stays English in both locales). Best effort — a backend whose
2591
+ // log the compat layer cannot reach just leaves the fork untitled.
2592
+ const sourceTitle = state.sessionTitle.trim();
2593
+ appendSessionTitle(String(childId), `Fork: ${sourceTitle === '' ? String(source.id).slice(0, 8) : sourceTitle}`);
2594
+ // The same resume-command shape the exit hint prints (plugin.ts
2595
+ // resumeCommand): DSH_TUI_RESUME_SESSION + the boot profile.
2596
+ const profile = resolveDshProfileName();
2597
+ const boot = profile === undefined ? 'dsh --config cordis.yml' : `dsh --profile ${profile}`;
2598
+ const command = process.platform === 'win32'
2599
+ ? `dsh-tui --resume ${childId}`
2600
+ : `DSH_TUI_RESUME_SESSION=${childId} ${boot}`;
2601
+ state.notify(t('fork-done', { id: String(childId), command }), { timeoutMs: 8000 });
2602
+ return true;
2603
+ },
1877
2604
  async resumeTo(sessionId) {
1878
2605
  // Switch the live agent to a persisted session: /resume picker Enter
1879
2606
  // loads the history immediately (the `--resume` launcher path keeps
@@ -1912,10 +2639,20 @@ export function createChannel(ctx, initialAgent, options) {
1912
2639
  provider: options.configuredProvider,
1913
2640
  model: options.configuredModel,
1914
2641
  });
2642
+ // The recorded route feeds back into agentOptions too — not just the
2643
+ // status line below: a provider-only cordis.yml pin (issue #67) leaves
2644
+ // agentOptions.model undefined on resume, which breaks the `{{model}}`
2645
+ // persona variable for the resumed agent's own assembly AND for every
2646
+ // subagent it spawns (dsh-subagent's resolveChildAgentOptions inherits
2647
+ // `parent.options.model`).
2648
+ const recordedRoute = await resolvePersistedRoute(ctx, SessionId(sessionId));
1915
2649
  try {
1916
2650
  handle = await agents.resume({
1917
2651
  resumeSessionId: SessionId(sessionId),
1918
- agentOptions: { provider: resumeRoute?.provider, model: resumeRoute?.model },
2652
+ agentOptions: {
2653
+ provider: resumeRoute?.provider ?? recordedRoute?.provider,
2654
+ model: resumeRoute?.model ?? recordedRoute?.model,
2655
+ },
1919
2656
  ...(resumeComposed.setup === undefined ? {} : { setup: resumeComposed.setup }),
1920
2657
  });
1921
2658
  }
@@ -1954,6 +2691,7 @@ export function createChannel(ctx, initialAgent, options) {
1954
2691
  state.pending = [];
1955
2692
  state.goal = undefined;
1956
2693
  state.sessionTitle = '';
2694
+ state.sessionColor = '';
1957
2695
  state.tokens = { input: 0, output: 0 };
1958
2696
  state.responseChars = 0;
1959
2697
  state.activeToolCount = 0;
@@ -2121,6 +2859,7 @@ export function createChannel(ctx, initialAgent, options) {
2121
2859
  state.pending = [];
2122
2860
  state.goal = undefined;
2123
2861
  state.sessionTitle = '';
2862
+ state.sessionColor = '';
2124
2863
  state.tokens = { input: 0, output: 0 };
2125
2864
  state.responseChars = 0;
2126
2865
  state.activeToolCount = 0;
@@ -2304,6 +3043,7 @@ export function createChannel(ctx, initialAgent, options) {
2304
3043
  state.pending = [];
2305
3044
  state.goal = undefined;
2306
3045
  state.sessionTitle = '';
3046
+ state.sessionColor = '';
2307
3047
  state.tokens = { input: 0, output: 0 };
2308
3048
  state.responseChars = 0;
2309
3049
  state.activeToolCount = 0;
@@ -2437,6 +3177,12 @@ export function createChannel(ctx, initialAgent, options) {
2437
3177
  state.scrollGutter = normalized;
2438
3178
  state.emit();
2439
3179
  },
3180
+ setPromptSessionLabel(enabled) {
3181
+ if (enabled === state.promptSessionLabel)
3182
+ return;
3183
+ state.promptSessionLabel = enabled;
3184
+ state.emit();
3185
+ },
2440
3186
  setStatusBar(config) {
2441
3187
  const next = normalizeStatusBar({ ...state.statusBar, ...config });
2442
3188
  const changed = Object.keys(next).some(key => next[key] !== state.statusBar[key]);
@@ -2567,6 +3313,12 @@ export function createChannel(ctx, initialAgent, options) {
2567
3313
  return Promise.all(providers.map(provider => llm.listModels(provider.id).catch(() => [])))
2568
3314
  .then(lists => lists.flat());
2569
3315
  },
3316
+ listProviders() {
3317
+ // Group labels for the two-level /model picker: the registry's own
3318
+ // display names, detached so a registry swap cannot leak through.
3319
+ const llm = ctx.get('llm');
3320
+ return Promise.resolve(llm === undefined ? [] : llm.listProviders().map(info => ({ ...info })));
3321
+ },
2570
3322
  async listSkills() {
2571
3323
  // snapshot() over list(): only a COMPLETE observation is authoritative
2572
3324
  // (same contract as the skill-command merge above) — a partial catalog
@@ -2663,7 +3415,12 @@ export function createChannel(ctx, initialAgent, options) {
2663
3415
  return undefined;
2664
3416
  }
2665
3417
  const revision = () => settings.describe().find(descriptor => descriptor.ns === 'llm-pi-ai')?.revision;
3418
+ // The OAuth sign-in surface (dsh-auth-style plugin), structural and
3419
+ // optional: mounting the plugin lights up the wizard's OAuth branch,
3420
+ // and without it the wizard is exactly what it was before.
3421
+ const oauthApi = ctx.get('dshAuth')?.api;
2666
3422
  return {
3423
+ ...(oauthApi === undefined ? {} : { oauth: oauthApi }),
2667
3424
  listCatalogProviders() {
2668
3425
  // declared === true marks routes the adapter knows only because a
2669
3426
  // stored profile names them (user-added); the rest are activatable
@@ -2709,6 +3466,12 @@ export function createChannel(ctx, initialAgent, options) {
2709
3466
  },
2710
3467
  };
2711
3468
  },
3469
+ async oauthProviderStatuses() {
3470
+ // Same optional seam the wizard's OAuth branch reads: absent plugin →
3471
+ // undefined, and `/login` renders exactly its pre-plugin lines.
3472
+ const api = ctx.get('dshAuth')?.api;
3473
+ return api === undefined ? undefined : api.providers();
3474
+ },
2712
3475
  async sideQuestion(question, options) {
2713
3476
  // CC /btw:无工具单轮辅助调用,重放 deriveMessages() 前缀 + 一条
2714
3477
  // 包装问题。tools 永不传(侧问无工具是核心语义);usage 不回收
@@ -2801,6 +3564,61 @@ export function createChannel(ctx, initialAgent, options) {
2801
3564
  state.sessionTitle = title;
2802
3565
  state.emit();
2803
3566
  },
3567
+ setSessionColor(color) {
3568
+ // `session/color` is a dsh-tui plugin event — not in dsh-session's
3569
+ // typed union, so appended through the same cast applyMode uses for
3570
+ // its sandbox/approval overrides. It replays on resume/rewind like
3571
+ // session/title, keeping each session's accent color its own.
3572
+ ;
3573
+ agent.session
3574
+ .append('session/color', { color });
3575
+ state.sessionColor = color;
3576
+ state.emit();
3577
+ },
3578
+ async recapRecent(options) {
3579
+ // `/recap` (pi-recap semantics): one tool-less LLM call over the
3580
+ // session's TAIL exchanges — unlike /btw it does not replay the full
3581
+ // derived history (the excerpt IS the payload), so it stays cheap.
3582
+ // The answer is pure UI state: never appended to the session log.
3583
+ const llm = ctx.get('llm');
3584
+ if (!llm)
3585
+ return { summary: null, error: t('recap-llm-unavailable') };
3586
+ const header = agent.session.requestHeader();
3587
+ const config = header?.config;
3588
+ const activity = collectRecentActivity(agent.session.events, RECAP_RECENT_CHARS);
3589
+ if (activity === '')
3590
+ return { summary: null, error: t('recap-no-activity') };
3591
+ const messages = [
3592
+ createUserMessage({
3593
+ content: [{ type: 'text', text: wrapRecapPrompt(activity) }],
3594
+ source: { kind: 'plugin', plugin: 'dsh-tui/recap' },
3595
+ }),
3596
+ ];
3597
+ const request = {
3598
+ provider: config?.provider ?? state.provider,
3599
+ model: config?.model ?? state.model,
3600
+ messages,
3601
+ ...(header?.system !== undefined && { system: header.system }),
3602
+ ...(config?.reasoningEffort !== undefined && { reasoningEffort: config.reasoningEffort }),
3603
+ ...(config?.temperature !== undefined && { temperature: config.temperature }),
3604
+ ...(config?.maxTokens !== undefined && { maxTokens: config.maxTokens }),
3605
+ ...(config?.stop !== undefined && { stop: [...config.stop] }),
3606
+ sessionId: agent.session.id,
3607
+ ...(options?.signal && { signal: options.signal }),
3608
+ };
3609
+ const outcome = await runSideQuestion({
3610
+ stream: llm.stream.bind(llm),
3611
+ options: request,
3612
+ onText: options?.onText,
3613
+ signal: options?.signal,
3614
+ });
3615
+ if (outcome.answer === null)
3616
+ return { summary: null, error: outcome.error };
3617
+ const parsed = parseRecapResponse(outcome.answer);
3618
+ return parsed.title === undefined
3619
+ ? { summary: parsed.summary }
3620
+ : { summary: parsed.summary, title: parsed.title };
3621
+ },
2804
3622
  async deleteSession(sessionId) {
2805
3623
  // The live session's log is still being appended by this process —
2806
3624
  // deleting it from under the writer is never offered in the picker
@@ -2896,7 +3714,13 @@ export function createChannel(ctx, initialAgent, options) {
2896
3714
  .catch((error) => {
2897
3715
  state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
2898
3716
  });
2899
- })();
3717
+ })().catch((error) => {
3718
+ // Sync throws from compactNow (e.g. runMaintenance rejecting a
3719
+ // non-idle agent right after /resume) reject this IIFE itself;
3720
+ // uncaught, that is an unhandled rejection and Node exits the
3721
+ // whole TUI. Surface it as the same failure notification.
3722
+ state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
3723
+ });
2900
3724
  },
2901
3725
  runExternalCommand(name, rawInput) {
2902
3726
  return executeRegistryCommand(name, rawInput);
@@ -4275,6 +5099,13 @@ ${output}
4275
5099
  nextRowId += 1;
4276
5100
  break;
4277
5101
  }
5102
+ // `/color` accent (dsh-tui plugin event, replayed on resume/rewind
5103
+ // like session/title): last write wins, '' clears to the default.
5104
+ if (event.type === 'session/color') {
5105
+ const data = event.data;
5106
+ state.sessionColor = typeof data.color === 'string' ? data.color : '';
5107
+ break;
5108
+ }
4278
5109
  // Custom plugin events (tuiRenderers seam): a registered renderer
4279
5110
  // maps the payload to text rows — title as a local row, body as
4280
5111
  // preview-clipped local-output rows, same shape pushLocal uses.
@@ -4822,6 +5653,20 @@ const MENTION_IMAGE_MEDIA_TYPES = {
4822
5653
  function mentionImageMediaType(path) {
4823
5654
  return MENTION_IMAGE_MEDIA_TYPES[extname(path).toLowerCase()];
4824
5655
  }
5656
+ /** Resolve+stat one candidate path; undefined when it throws OR stats
5657
+ * absent — both are a miss for strip-first mention resolution (issue #359). */
5658
+ async function tryResolveMention(fs, absolute) {
5659
+ try {
5660
+ const target = await fs.resolve(absolute);
5661
+ const info = await fs.stat(target);
5662
+ if (info === undefined)
5663
+ return undefined;
5664
+ return { target, info };
5665
+ }
5666
+ catch {
5667
+ return undefined;
5668
+ }
5669
+ }
4825
5670
  /**
4826
5671
  * Expand a submitted text's `@` mentions (issue #15) into model-facing
4827
5672
  * attachment blocks: supported image files become durable image blocks,
@@ -4840,68 +5685,99 @@ export async function expandMentions(fs, cwd, text, attachments, stagedImages) {
4840
5685
  let imageBytes = 0;
4841
5686
  if (fs !== undefined) {
4842
5687
  for (const mention of mentions) {
5688
+ const display = mention.literal ?? mention.path;
4843
5689
  const imageMediaType = mentionImageMediaType(mention.path);
4844
5690
  if (budget <= 0 && imageMediaType === undefined)
4845
5691
  break;
4846
5692
  // Mentions resolve against the session cwd, same as the model-facing fs
4847
- // tools; absolute paths pass through untouched.
5693
+ // tools; absolute paths pass through untouched. A `#L12-14` line suffix
5694
+ // (issue #359) is stripped before resolution; when the stripped path
5695
+ // misses, the typed literal (suffix intact) gets ONE fallback try so
5696
+ // filenames genuinely containing `#L…` still resolve as whole files.
4848
5697
  const absolute = isAbsolute(mention.path) ? mention.path : join(cwd, mention.path);
4849
- let target;
4850
- let info;
4851
- try {
4852
- target = await fs.resolve(absolute);
4853
- info = await fs.stat(target);
4854
- }
4855
- catch {
4856
- missing.push(mention.path);
5698
+ let resolved = await tryResolveMention(fs, absolute);
5699
+ let literalFallback = false;
5700
+ if (resolved === undefined && mention.literal !== undefined) {
5701
+ const literalPath = isAbsolute(mention.literal) ? mention.literal : join(cwd, mention.literal);
5702
+ resolved = await tryResolveMention(fs, literalPath);
5703
+ literalFallback = resolved !== undefined;
5704
+ }
5705
+ if (resolved === undefined) {
5706
+ missing.push(display);
4857
5707
  continue;
4858
5708
  }
5709
+ const { target, info } = resolved;
5710
+ // On a literal-fallback hit the attached file IS the typed name — the
5711
+ // model must see that path, not the suffix-stripped one.
5712
+ const shownPath = literalFallback ? display : mention.path;
5713
+ // …and judge image-ness by the typed extension in that case too.
5714
+ const imageType = literalFallback && mention.literal !== undefined
5715
+ ? mentionImageMediaType(mention.literal)
5716
+ : imageMediaType;
4859
5717
  if (info?.type === 'file') {
4860
- if (imageMediaType !== undefined && attachments !== undefined && fs.readBytes !== undefined) {
5718
+ if (imageType !== undefined && attachments !== undefined && fs.readBytes !== undefined) {
4861
5719
  const limits = attachments.imageLimits;
4862
- if (!limits.mediaTypes.includes(imageMediaType) || imageCount >= limits.maxImagesPerMessage) {
4863
- missing.push(mention.path);
5720
+ if (!limits.mediaTypes.includes(imageType) || imageCount >= limits.maxImagesPerMessage) {
5721
+ missing.push(display);
4864
5722
  continue;
4865
5723
  }
4866
5724
  try {
4867
5725
  const data = await fs.readBytes(target, undefined, limits.maxImageBytes);
4868
5726
  if (imageBytes + data.byteLength > limits.maxMessageImageBytes) {
4869
- missing.push(mention.path);
5727
+ missing.push(display);
4870
5728
  continue;
4871
5729
  }
4872
5730
  const attachment = await attachments.saveImage({
4873
5731
  data,
4874
- mediaType: imageMediaType,
5732
+ mediaType: imageType,
4875
5733
  name: basename(target.displayPath),
4876
5734
  });
4877
5735
  blocks.push({ type: 'image', attachment });
4878
5736
  imageCount += 1;
4879
5737
  imageBytes += data.byteLength;
4880
- attached.push(mention.path);
5738
+ attached.push(display);
4881
5739
  }
4882
5740
  catch {
4883
- missing.push(mention.path);
5741
+ missing.push(display);
4884
5742
  }
4885
5743
  continue;
4886
5744
  }
4887
5745
  try {
4888
5746
  const cap = Math.min(MENTION_MAX_FILE_CHARS, budget);
4889
- let content = await fs.readText(target);
5747
+ const content = await fs.readText(target);
5748
+ let body = content;
4890
5749
  let truncated = false;
4891
- if (content.length > cap) {
4892
- content = content.slice(0, cap);
5750
+ let header = `<attached-file path="${shownPath}">`;
5751
+ if (mention.startLine !== undefined && !literalFallback) {
5752
+ // Line-range slice (issue #359): 1-based inclusive. An endLine
5753
+ // past EOF clamps to the file; a startLine past EOF falls back
5754
+ // to the whole file with an in-band note — never a silent
5755
+ // empty attach. Line ranges never apply to literal-fallback
5756
+ // hits (those files really are named `…#L…`, no suffix typed).
5757
+ const lines = content.split('\n');
5758
+ if (mention.startLine > lines.length) {
5759
+ header = `<attached-file path="${shownPath}" lines="${mention.startLine}-${mention.endLine}" note="requested lines beyond EOF (file has ${lines.length} line${lines.length === 1 ? '' : 's'}); whole file attached">`;
5760
+ }
5761
+ else {
5762
+ const endLine = Math.min(mention.endLine ?? mention.startLine, lines.length);
5763
+ header = `<attached-file path="${shownPath}" lines="${mention.startLine}${endLine === mention.startLine ? '' : `-${endLine}`}">`;
5764
+ body = lines.slice(mention.startLine - 1, endLine).join('\n');
5765
+ }
5766
+ }
5767
+ if (body.length > cap) {
5768
+ body = body.slice(0, cap);
4893
5769
  truncated = true;
4894
5770
  }
4895
- budget -= content.length;
5771
+ budget -= body.length;
4896
5772
  blocks.push({
4897
5773
  type: 'text',
4898
- text: `<attached-file path="${mention.path}">\n${content}${truncated ? '\n[… truncated]' : ''}\n</attached-file>`,
5774
+ text: `${header}\n${body}${truncated ? '\n[… truncated]' : ''}\n</attached-file>`,
4899
5775
  });
4900
- attached.push(mention.path);
5776
+ attached.push(display);
4901
5777
  }
4902
5778
  catch {
4903
5779
  // Binary/undecodable or unreadable — report it like a miss.
4904
- missing.push(mention.path);
5780
+ missing.push(display);
4905
5781
  }
4906
5782
  continue;
4907
5783
  }
@@ -4918,17 +5794,17 @@ export async function expandMentions(fs, cwd, text, attachments, stagedImages) {
4918
5794
  budget -= body.length;
4919
5795
  blocks.push({
4920
5796
  type: 'text',
4921
- text: `<attached-directory path="${mention.path}">\n${body}\n</attached-directory>`,
5797
+ text: `<attached-directory path="${shownPath}">\n${body}\n</attached-directory>`,
4922
5798
  });
4923
- attached.push(mention.path);
5799
+ attached.push(display);
4924
5800
  }
4925
5801
  catch {
4926
- missing.push(mention.path);
5802
+ missing.push(display);
4927
5803
  }
4928
5804
  continue;
4929
5805
  }
4930
5806
  // Absent (stat → undefined) or a special file.
4931
- missing.push(mention.path);
5807
+ missing.push(display);
4932
5808
  }
4933
5809
  }
4934
5810
  if (attachments !== undefined && stagedImages !== undefined) {