@deepseek-harness-tui/dsh-tui 0.8.0 → 0.8.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 (434) hide show
  1. package/README.md +32 -15
  2. package/cordis.patch.yml +21 -2
  3. package/cordis.yml +12 -1
  4. package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -0
  5. package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -0
  6. package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -0
  7. package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -0
  8. package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -0
  9. package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -0
  10. package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -0
  11. package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -0
  12. package/dsh-ecosystem-spec/registry/README.md +14 -0
  13. package/dsh-ecosystem-spec/registry/contracts/decision-events-v1alpha1.json +13 -0
  14. package/dsh-ecosystem-spec/registry/contracts/scene-v1alpha1.json +13 -0
  15. package/dsh-ecosystem-spec/registry/contracts/settings-section-v1alpha1.json +13 -0
  16. package/dsh-ecosystem-spec/registry/contracts/tui-channel-v1alpha1.json +30 -0
  17. package/dsh-ecosystem-spec/registry/contracts/workspace-provider-v1alpha1.json +6 -0
  18. package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -0
  19. package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -0
  20. package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -0
  21. package/dsh-ecosystem-spec/schemas/conformance-claim.schema.json +15 -0
  22. package/dsh-ecosystem-spec/schemas/effect-ledger-record.schema.json +16 -0
  23. package/dsh-ecosystem-spec/schemas/host-descriptor.schema.json +26 -0
  24. package/lib/types/cc/figures.d.ts +4 -0
  25. package/lib/types/cc/figures.d.ts.map +1 -1
  26. package/lib/types/cc/figures.js +4 -0
  27. package/lib/types/command-trees.d.ts +2 -1
  28. package/lib/types/command-trees.d.ts.map +1 -1
  29. package/lib/types/command-trees.js +1 -1
  30. package/lib/types/commands.d.ts.map +1 -1
  31. package/lib/types/commands.js +2 -0
  32. package/lib/types/components/ExtensionDialog.d.ts +40 -0
  33. package/lib/types/components/ExtensionDialog.d.ts.map +1 -0
  34. package/lib/types/components/ExtensionDialog.js +210 -0
  35. package/lib/types/components/HelpMenu.js +1 -1
  36. package/lib/types/components/LoadedContextPanel.d.ts +5 -5
  37. package/lib/types/components/LoadedContextPanel.js +6 -6
  38. package/lib/types/components/LogoV2.d.ts.map +1 -1
  39. package/lib/types/components/LogoV2.js +16 -8
  40. package/lib/types/components/Markdown.d.ts +13 -1
  41. package/lib/types/components/Markdown.d.ts.map +1 -1
  42. package/lib/types/components/Markdown.js +15 -1
  43. package/lib/types/components/MessageList.d.ts +3 -1
  44. package/lib/types/components/MessageList.d.ts.map +1 -1
  45. package/lib/types/components/MessageList.js +111 -6
  46. package/lib/types/components/PromptInput.d.ts.map +1 -1
  47. package/lib/types/components/PromptInput.js +26 -12
  48. package/lib/types/components/RewindPicker.d.ts +13 -1
  49. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  50. package/lib/types/components/RewindPicker.js +26 -2
  51. package/lib/types/components/StreamingMarkdown.d.ts +2 -8
  52. package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
  53. package/lib/types/components/StreamingMarkdown.js +51 -9
  54. package/lib/types/components/ToolUseLoader.d.ts +8 -3
  55. package/lib/types/components/ToolUseLoader.d.ts.map +1 -1
  56. package/lib/types/components/ToolUseLoader.js +43 -9
  57. package/lib/types/components/activityFrames.d.ts +1 -1
  58. package/lib/types/components/activityFrames.d.ts.map +1 -1
  59. package/lib/types/components/activityFrames.js +4 -1
  60. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +5 -1
  61. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  62. package/lib/types/components/messages/AssistantThinkingMessage.js +26 -3
  63. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  64. package/lib/types/components/messages/AssistantToolUseMessage.js +13 -2
  65. package/lib/types/dsh-adapter/channel.d.ts +61 -11
  66. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  67. package/lib/types/dsh-adapter/channel.js +767 -120
  68. package/lib/types/dsh-adapter/childStderr.d.ts.map +1 -1
  69. package/lib/types/dsh-adapter/childStderr.js +10 -0
  70. package/lib/types/dsh-adapter/command-attribution.d.ts +51 -0
  71. package/lib/types/dsh-adapter/command-attribution.d.ts.map +1 -0
  72. package/lib/types/dsh-adapter/command-attribution.js +93 -0
  73. package/lib/types/dsh-adapter/command-errors.d.ts +42 -0
  74. package/lib/types/dsh-adapter/command-errors.d.ts.map +1 -0
  75. package/lib/types/dsh-adapter/command-errors.js +63 -0
  76. package/lib/types/dsh-adapter/command-trees.d.ts +7 -1
  77. package/lib/types/dsh-adapter/command-trees.d.ts.map +1 -1
  78. package/lib/types/dsh-adapter/command-trees.js +70 -20
  79. package/lib/types/dsh-adapter/component-identity.d.ts +33 -0
  80. package/lib/types/dsh-adapter/component-identity.d.ts.map +1 -0
  81. package/lib/types/dsh-adapter/component-identity.js +108 -0
  82. package/lib/types/dsh-adapter/contract.d.ts +1 -1
  83. package/lib/types/dsh-adapter/contract.js +2 -2
  84. package/lib/types/dsh-adapter/decision-guard.d.ts +84 -0
  85. package/lib/types/dsh-adapter/decision-guard.d.ts.map +1 -0
  86. package/lib/types/dsh-adapter/decision-guard.js +348 -0
  87. package/lib/types/dsh-adapter/dialogs.d.ts +145 -0
  88. package/lib/types/dsh-adapter/dialogs.d.ts.map +1 -0
  89. package/lib/types/dsh-adapter/dialogs.js +316 -0
  90. package/lib/types/dsh-adapter/effect-ledger.d.ts +91 -0
  91. package/lib/types/dsh-adapter/effect-ledger.d.ts.map +1 -0
  92. package/lib/types/dsh-adapter/effect-ledger.js +251 -0
  93. package/lib/types/dsh-adapter/extension-events.d.ts +193 -0
  94. package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -0
  95. package/lib/types/dsh-adapter/extension-events.js +207 -0
  96. package/lib/types/dsh-adapter/extensions.d.ts +32 -0
  97. package/lib/types/dsh-adapter/extensions.d.ts.map +1 -0
  98. package/lib/types/dsh-adapter/extensions.js +42 -0
  99. package/lib/types/dsh-adapter/grants.d.ts +25 -0
  100. package/lib/types/dsh-adapter/grants.d.ts.map +1 -0
  101. package/lib/types/dsh-adapter/grants.js +213 -0
  102. package/lib/types/dsh-adapter/host-access.d.ts +37 -0
  103. package/lib/types/dsh-adapter/host-access.d.ts.map +1 -0
  104. package/lib/types/dsh-adapter/host-access.js +652 -0
  105. package/lib/types/dsh-adapter/host-descriptor.d.ts +24 -0
  106. package/lib/types/dsh-adapter/host-descriptor.d.ts.map +1 -0
  107. package/lib/types/dsh-adapter/host-descriptor.js +169 -0
  108. package/lib/types/dsh-adapter/index.d.ts +4 -0
  109. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  110. package/lib/types/dsh-adapter/index.js +1 -0
  111. package/lib/types/dsh-adapter/message-observer.d.ts +173 -0
  112. package/lib/types/dsh-adapter/message-observer.d.ts.map +1 -0
  113. package/lib/types/dsh-adapter/message-observer.js +553 -0
  114. package/lib/types/dsh-adapter/plugin-host.d.ts +136 -0
  115. package/lib/types/dsh-adapter/plugin-host.d.ts.map +1 -0
  116. package/lib/types/dsh-adapter/plugin-host.js +365 -0
  117. package/lib/types/dsh-adapter/plugin-storage.d.ts +108 -0
  118. package/lib/types/dsh-adapter/plugin-storage.d.ts.map +1 -0
  119. package/lib/types/dsh-adapter/plugin-storage.js +397 -0
  120. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  121. package/lib/types/dsh-adapter/plugin.js +124 -14
  122. package/lib/types/dsh-adapter/plugins-info.d.ts +43 -0
  123. package/lib/types/dsh-adapter/plugins-info.d.ts.map +1 -0
  124. package/lib/types/dsh-adapter/plugins-info.js +276 -0
  125. package/lib/types/dsh-adapter/promptDebug.d.ts +0 -6
  126. package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -1
  127. package/lib/types/dsh-adapter/promptDebug.js +13 -4
  128. package/lib/types/dsh-adapter/renderers.d.ts +57 -0
  129. package/lib/types/dsh-adapter/renderers.d.ts.map +1 -0
  130. package/lib/types/dsh-adapter/renderers.js +173 -0
  131. package/lib/types/dsh-adapter/sanitize.d.ts +36 -0
  132. package/lib/types/dsh-adapter/sanitize.d.ts.map +1 -0
  133. package/lib/types/dsh-adapter/sanitize.js +65 -0
  134. package/lib/types/dsh-adapter/scenes.d.ts +15 -5
  135. package/lib/types/dsh-adapter/scenes.d.ts.map +1 -1
  136. package/lib/types/dsh-adapter/scenes.js +149 -35
  137. package/lib/types/dsh-adapter/settings-sections.d.ts +7 -3
  138. package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -1
  139. package/lib/types/dsh-adapter/settings-sections.js +106 -26
  140. package/lib/types/dsh-adapter/shortcuts.d.ts +107 -0
  141. package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -0
  142. package/lib/types/dsh-adapter/shortcuts.js +340 -0
  143. package/lib/types/dsh-adapter/status.d.ts +74 -0
  144. package/lib/types/dsh-adapter/status.d.ts.map +1 -0
  145. package/lib/types/dsh-adapter/status.js +234 -0
  146. package/lib/types/dsh-adapter/workspaces.d.ts +16 -5
  147. package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
  148. package/lib/types/dsh-adapter/workspaces.js +287 -170
  149. package/lib/types/extensions.d.ts +13 -0
  150. package/lib/types/extensions.d.ts.map +1 -0
  151. package/lib/types/extensions.js +21 -0
  152. package/lib/types/force-production-react.d.ts +1 -0
  153. package/lib/types/force-production-react.d.ts.map +1 -0
  154. package/lib/types/force-production-react.js +12 -0
  155. package/lib/types/i18n.d.ts +174 -20
  156. package/lib/types/i18n.d.ts.map +1 -1
  157. package/lib/types/i18n.js +56 -6
  158. package/lib/types/index.d.ts +1 -0
  159. package/lib/types/index.d.ts.map +1 -1
  160. package/lib/types/index.js +1 -0
  161. package/lib/types/ink/components/App.d.ts +2 -0
  162. package/lib/types/ink/components/App.d.ts.map +1 -1
  163. package/lib/types/ink/components/App.js +4 -0
  164. package/lib/types/ink/ink.d.ts +31 -3
  165. package/lib/types/ink/ink.d.ts.map +1 -1
  166. package/lib/types/ink/ink.js +90 -5
  167. package/lib/types/ink/log-update.d.ts.map +1 -1
  168. package/lib/types/ink/log-update.js +225 -56
  169. package/lib/types/ink/output.d.ts +35 -2
  170. package/lib/types/ink/output.d.ts.map +1 -1
  171. package/lib/types/ink/output.js +281 -14
  172. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  173. package/lib/types/ink/render-node-to-output.js +14 -6
  174. package/lib/types/ink/screen.d.ts +39 -0
  175. package/lib/types/ink/screen.d.ts.map +1 -1
  176. package/lib/types/ink/screen.js +117 -0
  177. package/lib/types/ink/terminal.d.ts.map +1 -1
  178. package/lib/types/ink/terminal.js +26 -6
  179. package/lib/types/ink/wrapAnsi.d.ts +1 -1
  180. package/lib/types/ink/wrapAnsi.d.ts.map +1 -1
  181. package/lib/types/ink/wrapAnsi.js +29 -2
  182. package/lib/types/plugin-host.d.ts +15 -0
  183. package/lib/types/plugin-host.d.ts.map +1 -0
  184. package/lib/types/plugin-host.js +17 -0
  185. package/lib/types/plugin-spec/negotiate.d.ts +11 -0
  186. package/lib/types/plugin-spec/negotiate.d.ts.map +1 -0
  187. package/lib/types/plugin-spec/negotiate.js +97 -0
  188. package/lib/types/plugin-spec/permission-scope.d.ts +32 -0
  189. package/lib/types/plugin-spec/permission-scope.d.ts.map +1 -0
  190. package/lib/types/plugin-spec/permission-scope.js +90 -0
  191. package/lib/types/plugin-spec/registry.d.ts +23 -0
  192. package/lib/types/plugin-spec/registry.d.ts.map +1 -0
  193. package/lib/types/plugin-spec/registry.js +252 -0
  194. package/lib/types/plugin-spec/schema-check.d.ts +17 -0
  195. package/lib/types/plugin-spec/schema-check.d.ts.map +1 -0
  196. package/lib/types/plugin-spec/schema-check.js +165 -0
  197. package/lib/types/plugin-spec/tui-extension.d.ts +16 -0
  198. package/lib/types/plugin-spec/tui-extension.d.ts.map +1 -0
  199. package/lib/types/plugin-spec/tui-extension.js +79 -0
  200. package/lib/types/plugin-spec/types.d.ts +114 -0
  201. package/lib/types/plugin-spec/types.d.ts.map +1 -0
  202. package/lib/types/plugin-spec/types.js +10 -0
  203. package/lib/types/plugin-spec/validate.d.ts +21 -0
  204. package/lib/types/plugin-spec/validate.d.ts.map +1 -0
  205. package/lib/types/plugin-spec/validate.js +138 -0
  206. package/lib/types/scenes.d.ts +2 -1
  207. package/lib/types/scenes.d.ts.map +1 -1
  208. package/lib/types/scenes.js +1 -1
  209. package/lib/types/screens/Chat.d.ts +14 -1
  210. package/lib/types/screens/Chat.d.ts.map +1 -1
  211. package/lib/types/screens/Chat.js +173 -49
  212. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  213. package/lib/types/screens/SessionBrowser.js +10 -3
  214. package/lib/types/sessions/view.d.ts +3 -2
  215. package/lib/types/sessions/view.d.ts.map +1 -1
  216. package/lib/types/sessions/view.js +18 -0
  217. package/lib/types/settings-sections.d.ts +2 -1
  218. package/lib/types/settings-sections.d.ts.map +1 -1
  219. package/lib/types/settings-sections.js +1 -1
  220. package/lib/types/theme.d.ts +5 -0
  221. package/lib/types/theme.d.ts.map +1 -1
  222. package/lib/types/theme.js +15 -0
  223. package/lib/types/update.d.ts +20 -0
  224. package/lib/types/update.d.ts.map +1 -1
  225. package/lib/types/update.js +54 -7
  226. package/lib/types/utils/execFileNoThrow.d.ts.map +1 -1
  227. package/lib/types/utils/execFileNoThrow.js +3 -0
  228. package/lib/types/utils/loaded-context.d.ts +8 -3
  229. package/lib/types/utils/loaded-context.d.ts.map +1 -1
  230. package/lib/types/utils/loaded-context.js +39 -3
  231. package/lib/types/utils/modifiers.d.ts.map +1 -1
  232. package/lib/types/utils/modifiers.js +3 -2
  233. package/lib/types/working-activity.d.ts +12 -0
  234. package/lib/types/working-activity.d.ts.map +1 -1
  235. package/lib/types/working-activity.js +13 -0
  236. package/lib/types/workspaces.d.ts +2 -1
  237. package/lib/types/workspaces.d.ts.map +1 -1
  238. package/lib/types/workspaces.js +1 -1
  239. package/node_modules/@dsh-std/command/CHANGELOG.md +8 -0
  240. package/node_modules/@dsh-std/command/LICENSE +21 -0
  241. package/node_modules/@dsh-std/command/README.md +26 -0
  242. package/node_modules/@dsh-std/command/README.zh.md +28 -0
  243. package/node_modules/@dsh-std/command/lib/index.d.ts +144 -0
  244. package/node_modules/@dsh-std/command/lib/index.d.ts.map +1 -0
  245. package/node_modules/@dsh-std/command/lib/index.js +252 -0
  246. package/node_modules/@dsh-std/command/lib/index.js.map +1 -0
  247. package/node_modules/@dsh-std/command/package.json +22 -0
  248. package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
  249. package/node_modules/@dsh-std/connection/LICENSE +21 -0
  250. package/node_modules/@dsh-std/connection/README.md +42 -0
  251. package/node_modules/@dsh-std/connection/README.zh.md +42 -0
  252. package/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
  253. package/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
  254. package/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
  255. package/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
  256. package/node_modules/@dsh-std/connection/lib/index.js +158 -0
  257. package/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
  258. package/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
  259. package/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
  260. package/node_modules/@dsh-std/connection/lib/memory.js +258 -0
  261. package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
  262. package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
  263. package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
  264. package/node_modules/@dsh-std/connection/package.json +20 -0
  265. package/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  266. package/node_modules/@dsh-std/core/LICENSE +21 -0
  267. package/node_modules/@dsh-std/core/README.md +13 -0
  268. package/node_modules/@dsh-std/core/README.zh.md +13 -0
  269. package/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  270. package/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  271. package/node_modules/@dsh-std/core/lib/index.js +424 -0
  272. package/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  273. package/node_modules/@dsh-std/core/package.json +21 -0
  274. package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -0
  275. package/node_modules/@dsh-std/manifest/LICENSE +21 -0
  276. package/node_modules/@dsh-std/manifest/README.md +5 -0
  277. package/node_modules/@dsh-std/manifest/README.zh.md +5 -0
  278. package/node_modules/@dsh-std/manifest/lib/index.d.ts +182 -0
  279. package/node_modules/@dsh-std/manifest/lib/index.d.ts.map +1 -0
  280. package/node_modules/@dsh-std/manifest/lib/index.js +625 -0
  281. package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -0
  282. package/node_modules/@dsh-std/manifest/package.json +20 -0
  283. package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -0
  284. package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -0
  285. package/node_modules/@dsh-std/messages/LICENSE +21 -0
  286. package/node_modules/@dsh-std/messages/README.md +5 -0
  287. package/node_modules/@dsh-std/messages/README.zh.md +7 -0
  288. package/node_modules/@dsh-std/messages/lib/index.d.ts +50 -0
  289. package/node_modules/@dsh-std/messages/lib/index.d.ts.map +1 -0
  290. package/node_modules/@dsh-std/messages/lib/index.js +151 -0
  291. package/node_modules/@dsh-std/messages/lib/index.js.map +1 -0
  292. package/node_modules/@dsh-std/messages/package.json +20 -0
  293. package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -0
  294. package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -0
  295. package/node_modules/@dsh-std/presentation/LICENSE +21 -0
  296. package/node_modules/@dsh-std/presentation/README.md +27 -0
  297. package/node_modules/@dsh-std/presentation/README.zh.md +29 -0
  298. package/node_modules/@dsh-std/presentation/lib/callback.d.ts +2 -0
  299. package/node_modules/@dsh-std/presentation/lib/callback.js +179 -0
  300. package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -0
  301. package/node_modules/@dsh-std/presentation/lib/index.d.ts +262 -0
  302. package/node_modules/@dsh-std/presentation/lib/index.d.ts.map +1 -0
  303. package/node_modules/@dsh-std/presentation/lib/index.js +550 -0
  304. package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -0
  305. package/node_modules/@dsh-std/presentation/lib/interaction.d.ts +2 -0
  306. package/node_modules/@dsh-std/presentation/lib/interaction.js +2 -0
  307. package/node_modules/@dsh-std/presentation/lib/operations.d.ts +2 -0
  308. package/node_modules/@dsh-std/presentation/lib/operations.js +2 -0
  309. package/node_modules/@dsh-std/presentation/package.json +25 -0
  310. package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -0
  311. package/node_modules/@dsh-std/storage/LICENSE +21 -0
  312. package/node_modules/@dsh-std/storage/README.md +5 -0
  313. package/node_modules/@dsh-std/storage/README.zh.md +7 -0
  314. package/node_modules/@dsh-std/storage/lib/index.d.ts +48 -0
  315. package/node_modules/@dsh-std/storage/lib/index.d.ts.map +1 -0
  316. package/node_modules/@dsh-std/storage/lib/index.js +104 -0
  317. package/node_modules/@dsh-std/storage/lib/index.js.map +1 -0
  318. package/node_modules/@dsh-std/storage/package.json +20 -0
  319. package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -0
  320. package/package.json +110 -70
  321. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
  322. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -0
  323. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -0
  324. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -0
  325. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
  326. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
  327. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
  328. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
  329. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js +158 -0
  330. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
  331. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
  332. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
  333. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js +258 -0
  334. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
  335. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
  336. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
  337. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -0
  338. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  339. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -0
  340. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -0
  341. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -0
  342. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  343. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  344. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js +424 -0
  345. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  346. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -0
  347. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -0
  348. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -0
  349. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -0
  350. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -0
  351. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.d.ts +182 -0
  352. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.d.ts.map +1 -0
  353. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js +625 -0
  354. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -0
  355. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -0
  356. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -0
  357. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -0
  358. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -0
  359. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -0
  360. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -0
  361. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.d.ts +2 -0
  362. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js +179 -0
  363. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -0
  364. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.d.ts +262 -0
  365. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.d.ts.map +1 -0
  366. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js +550 -0
  367. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -0
  368. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/interaction.d.ts +2 -0
  369. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/interaction.js +2 -0
  370. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/operations.d.ts +2 -0
  371. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/operations.js +2 -0
  372. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -0
  373. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  374. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -0
  375. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -0
  376. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -0
  377. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  378. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  379. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js +424 -0
  380. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  381. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -0
  382. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  383. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -0
  384. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -0
  385. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -0
  386. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  387. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  388. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js +424 -0
  389. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  390. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -0
  391. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  392. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -0
  393. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -0
  394. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -0
  395. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  396. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  397. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js +424 -0
  398. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  399. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -0
  400. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
  401. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -0
  402. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -0
  403. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -0
  404. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
  405. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
  406. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
  407. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
  408. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js +158 -0
  409. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
  410. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
  411. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
  412. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js +258 -0
  413. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
  414. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
  415. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
  416. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -0
  417. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  418. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -0
  419. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -0
  420. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -0
  421. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  422. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  423. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js +424 -0
  424. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  425. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -0
  426. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  427. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -0
  428. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -0
  429. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -0
  430. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  431. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  432. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js +424 -0
  433. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  434. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -0
@@ -26,7 +26,108 @@ import { t } from '../i18n.js';
26
26
  import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
27
27
  import { ActivityTracker } from 'dsh-working-activity/status';
28
28
  import { attachSessionToWorkspace } from './workspace.js';
29
- import { createLocalWorkspaceRuntime } from './workspaces.js';
29
+ import { createLocalWorkspaceRuntime, getHostWorkspaceRuntime } from './workspaces.js';
30
+ import { getHostCommandTrees } from './command-trees.js';
31
+ import { getHostSettingsSections } from './settings-sections.js';
32
+ import { getHostSceneRuntime } from './scenes.js';
33
+ import { getHostRenderers } from './renderers.js';
34
+ import { getHostMessageObserver } from './message-observer.js';
35
+ import { dispatchTuiDecision, dispatchTuiNotification, normalizeCancelDecision } from './extension-events.js';
36
+ import { installDecisionGuard } from './decision-guard.js';
37
+ import { commandOwner } from './command-attribution.js';
38
+ import { readGrantStore } from './grants.js';
39
+ import { hasCommandErrorCode, mapCommandError } from './command-errors.js';
40
+ import { pluginsInfoLines } from './plugins-info.js';
41
+ import { cleanRenderText, cleanScalarText } from './sanitize.js';
42
+ /** `tui/input` return normalization: transform/handled/cancel or no opinion.
43
+ * A blank `{ text }` rewrite is NOT a decision — it is logged and the chain
44
+ * continues so a later veto listener still runs. */
45
+ function normalizeInputDecision(result, warn) {
46
+ if (result === undefined || result === null || result === false)
47
+ return undefined;
48
+ if (typeof result !== 'object') {
49
+ warn(`a non-object (${typeof result})`);
50
+ return undefined;
51
+ }
52
+ const record = result;
53
+ if (record.cancel === true) {
54
+ const reason = cleanScalarText(record.reason, NOTICE_CELLS);
55
+ return { cancel: true, ...(reason === '' ? {} : { reason }) };
56
+ }
57
+ if (record.handled === true) {
58
+ const notice = cleanScalarText(record.notice, NOTICE_CELLS);
59
+ return { handled: true, ...(notice === '' ? {} : { notice }) };
60
+ }
61
+ if (typeof record.text === 'string') {
62
+ if (record.text.trim() === '') {
63
+ warn('a blank {text} rewrite');
64
+ return undefined;
65
+ }
66
+ return { text: record.text };
67
+ }
68
+ warn('an unrecognized decision shape');
69
+ return undefined;
70
+ }
71
+ /** `tui/rewind-prompt` return normalization: cancel/modes or no opinion.
72
+ * Modes are COPIED with only validated, sanitized scalar fields — the raw
73
+ * plugin object must never reach the render path (a `description: {}` would
74
+ * crash ListItem's `.replace`, and control chars would corrupt the pane).
75
+ * Notices/reasons are toast-bound plugin text: sanitized too (see
76
+ * ./sanitize.js — the one implementation of the render-path contract). */
77
+ function normalizeRewindPromptDecision(result, warn) {
78
+ if (result === undefined || result === null || result === false)
79
+ return undefined;
80
+ if (typeof result !== 'object') {
81
+ warn(`a non-object (${typeof result})`);
82
+ return undefined;
83
+ }
84
+ const record = result;
85
+ if (record.cancel === true) {
86
+ const reason = cleanScalarText(record.reason, NOTICE_CELLS);
87
+ return { cancel: true, ...(reason === '' ? {} : { reason }) };
88
+ }
89
+ if (Array.isArray(record.modes)) {
90
+ const modes = [];
91
+ for (const raw of record.modes) {
92
+ if (modes.length >= 8)
93
+ break;
94
+ if (raw === null || typeof raw !== 'object')
95
+ continue;
96
+ const candidate = raw;
97
+ if (typeof candidate.id !== 'string' || candidate.id.trim() === '')
98
+ continue;
99
+ const label = typeof candidate.label === 'string' ? cleanRenderText(candidate.label, 120) : '';
100
+ if (label === '')
101
+ continue;
102
+ const description = typeof candidate.description === 'string' && candidate.description.trim() !== ''
103
+ ? cleanRenderText(candidate.description, 400)
104
+ : undefined;
105
+ modes.push({ id: candidate.id, label, ...(description === undefined ? {} : { description }) });
106
+ }
107
+ if (modes.length === 0) {
108
+ warn('an empty or invalid {modes} list');
109
+ return undefined;
110
+ }
111
+ return { modes };
112
+ }
113
+ warn('an unrecognized decision shape');
114
+ return undefined;
115
+ }
116
+ /** Toast-bound plugin text (veto reasons, handled notices, rewind summaries)
117
+ * is render-path data too: same sanitization, toast-width cap. */
118
+ const NOTICE_CELLS = 200;
119
+ /** `tui/rewind-done` return normalization: the first non-empty STRING is the
120
+ * summary; anything else is not a decision. */
121
+ function normalizeRewindDoneSummary(result, warn) {
122
+ if (result === undefined || result === null || result === false)
123
+ return undefined;
124
+ if (typeof result === 'string') {
125
+ const summary = cleanRenderText(result, NOTICE_CELLS);
126
+ return summary === '' ? undefined : summary;
127
+ }
128
+ warn('a non-string summary');
129
+ return undefined;
130
+ }
30
131
  /**
31
132
  * Delay before re-reading a skill catalog that reported an incomplete
32
133
  * observation (a provider whose directory watcher is still warming).
@@ -59,12 +160,25 @@ function preview(text, limit) {
59
160
  * a loadOlder() restore is not instantly undone. Returns the number of rows
60
161
  * folded.
61
162
  */
62
- function foldRows(rows, cap) {
163
+ function foldRows(rows, cap, cursor) {
63
164
  const excess = rows.length - cap;
64
- if (excess <= 0)
165
+ if (excess <= 0) {
166
+ if (cursor !== undefined)
167
+ cursor.index = 0;
168
+ return 0;
169
+ }
170
+ // Incremental pass: rows only ever append past the fold line and the
171
+ // folded/restored exemptions are permanent, so everything below a cursor
172
+ // over the SAME array identity needs no re-inspection. emit/emitStream
173
+ // fold on every frame during streaming — a full rescan of a long window
174
+ // there was the O(rows) per-frame term of long-session streaming.
175
+ const from = cursor === undefined ? 0 : cursor.rows === rows ? cursor.index : 0;
176
+ if (cursor !== undefined)
177
+ cursor.rows = rows;
178
+ if (excess <= from)
65
179
  return 0;
66
180
  let folded = 0;
67
- for (const row of rows.slice(0, excess)) {
181
+ for (const row of rows.slice(from, excess)) {
68
182
  if (row.folded || row.restored)
69
183
  continue;
70
184
  if (row.kind !== 'user' && row.kind !== 'assistant' && row.kind !== 'reasoning' && row.kind !== 'tool')
@@ -86,6 +200,8 @@ function foldRows(rows, cap) {
86
200
  row.text = preview(row.text, 200);
87
201
  }
88
202
  }
203
+ if (cursor !== undefined)
204
+ cursor.index = excess;
89
205
  return folded;
90
206
  }
91
207
  /**
@@ -214,46 +330,44 @@ function restoreToolResult(row, event) {
214
330
  row.tool.resultFull = result || undefined;
215
331
  }
216
332
  /**
217
- * Coalesce runs of same-type assistant/chunk deltas into single synthetic
218
- * events for REPLAY only. A streamed turn logs one event per token (~100k
219
- * events in long sessions); replaying them one at a time costs per-chunk
220
- * string growth on every row (quadratic in the turn's length). Merging is
221
- * outcome-identical: ensureStreaming/ensureReasoning only read chunk.type
222
- * and the concatenated text, and the row's seq comes from the run's FIRST
223
- * chunk (the fork boundary rewindTo derives from it). Parts join once —
224
- * no quadratic concat. Live events never go through this.
333
+ * Prepare durable events for REPLAY (resume / rewind / model-switch fork):
334
+ * drop settled `assistant/chunk` stream deltas — the sealed
335
+ * `assistant/message` events carry the full text and reasoning blocks, so
336
+ * per-token chunks add nothing to the replayed transcript while costing a
337
+ * per-chunk renderEvent pass (a real 4.5MB session logs ~19k chunks against
338
+ * ~30 messages). The trailing chunk run AFTER the last message belongs to an
339
+ * unfinished step (crash-orphaned turn) and is kept, so a resumed session
340
+ * still shows its partial content. Storage-level packed rows
341
+ * (`text-chunks`/`reasoning-chunks`/`tool-call-chunks`) are dropped the
342
+ * same way — defensive: the jsonl reader expands them, but a future
343
+ * direct-pass path must not resurrect them. Replay-side tps sampling is
344
+ * lost with the chunks (a live metric; lastUsage comes from the message's
345
+ * own usage). Live events never go through this.
225
346
  */
226
- function coalesceReplayEvents(events) {
227
- const out = [];
228
- let run = null;
229
- const flush = () => {
230
- if (run === null)
231
- return;
232
- const chunk = run.event.data.chunk;
233
- out.push({
234
- ...run.event,
235
- data: { ...run.event.data, chunk: { ...chunk, text: run.parts.join('') } },
236
- });
237
- run = null;
238
- };
347
+ function prepareReplayEvents(events) {
348
+ let lastMessageSeq = -1;
239
349
  for (const event of events) {
240
- if (event.type === 'assistant/chunk' &&
241
- (event.data.chunk.type === 'text-delta' || event.data.chunk.type === 'reasoning-delta')) {
242
- if (run !== null && run.type === event.data.chunk.type) {
243
- // oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may lack text
244
- run.parts.push(event.data.chunk.text ?? '');
245
- continue;
246
- }
247
- flush();
248
- // oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may lack text
249
- run = { event, type: event.data.chunk.type, parts: [event.data.chunk.text ?? ''] };
250
- continue;
251
- }
252
- flush();
253
- out.push(event);
350
+ if (event.type === 'assistant/message')
351
+ lastMessageSeq = event.seq;
254
352
  }
255
- flush();
256
- return out;
353
+ return events.filter(event => {
354
+ if (event.type === 'assistant/message')
355
+ return true;
356
+ if (event.type === 'assistant/chunk') {
357
+ // Keep only the in-flight tail (no message sealed after it).
358
+ return lastMessageSeq < 0 || event.seq > lastMessageSeq;
359
+ }
360
+ // Storage-level packed rows: not in the SessionEvent union (they exist
361
+ // only in the durable JSON), so compare through a widened view — the
362
+ // defensive drop is exactly for data the static type doesn't know.
363
+ const packedType = event.type;
364
+ if (packedType === 'text-chunks' ||
365
+ packedType === 'reasoning-chunks' ||
366
+ packedType === 'tool-call-chunks') {
367
+ return false;
368
+ }
369
+ return true;
370
+ });
257
371
  }
258
372
  /** Buffer below the context window at which CC warns (autoCompact.ts). */
259
373
  const CONTEXT_WARNING_BUFFER_TOKENS = 20_000;
@@ -288,18 +402,35 @@ async function waitForTurnEnd(session, fromSeq, timeoutMs) {
288
402
  export function createChannel(ctx, initialAgent, options) {
289
403
  let agent = initialAgent;
290
404
  let currentHandle = options.handle;
405
+ // D-7 backstop: the extensions row installs the decision-subscription
406
+ // gate, but the channel IS the dispatch path — a stale patch without that
407
+ // row (or a bare embed mounting neither) would otherwise leave tui/input
408
+ // & friends subscribable by default, silently voiding the default-deny
409
+ // posture. Idempotent per cordis root, so the full-patch path installs
410
+ // exactly once whichever side runs first. One store instance serves both
411
+ // the gate and the invoke checkpoint below.
412
+ // Keep a private fallback for bare embedders, but resolve the host-owned
413
+ // store on every operation so a plugin-host row mounted later (or a custom
414
+ // live GrantStore) is not shadowed by an early snapshot.
415
+ const fallbackGrantStore = readGrantStore();
416
+ const currentGrantStore = () => ctx.get('tuiPluginHost')?.grants ?? fallbackGrantStore;
417
+ installDecisionGuard(ctx, currentGrantStore());
291
418
  // The DSH slash-command registry (optional service): /plan, /goal and
292
419
  // friends register here; the TUI merges their descriptors into the slash
293
420
  // menu and dispatches through `execute` (which logs the paired
294
421
  // command/run + command/done records). Absent the service, only the
295
422
  // built-in local commands exist.
296
423
  const commandService = ctx.get('commands');
424
+ // messages.observe broker (optional service, C-042): mounted by the
425
+ // dsh-tui-plugin-host row; absent the row, publish is a no-op and nothing
426
+ // else changes (soft degradation, #183).
427
+ const messageObserver = getHostMessageObserver(ctx.get('tuiMessageObserver'));
297
428
  // Workspace registry runtime (optional service, issue #183): mounted by
298
429
  // the bundle patch's dsh-tui-workspaces row; absent the row (stale patch
299
430
  // or a bare embedder), degrade to the local-only runtime. plugin.ts owns
300
431
  // the degraded-boot warning for profile launches.
301
- const workspaceService = ctx.get('tuiWorkspaces') ?? createLocalWorkspaceRuntime();
302
- const commandTrees = ctx.get('tuiCommandTrees');
432
+ const workspaceService = getHostWorkspaceRuntime(ctx.get('tuiWorkspaces')) ?? createLocalWorkspaceRuntime();
433
+ const commandTrees = getHostCommandTrees(ctx.get('tuiCommandTrees'));
303
434
  // The `/settings` screen reads its host on EVERY render, so the host must
304
435
  // be a stable object: a fresh literal per call would re-fire the screen's
305
436
  // host-keyed effects endlessly (render → new host → effect → state →
@@ -310,7 +441,12 @@ export function createChannel(ctx, initialAgent, options) {
310
441
  // Plugin scene runtime (optional service, same degradation rule as
311
442
  // tuiWorkspaces/tuiCommandTrees): mounted by the bundle patch's
312
443
  // dsh-tui-scenes row; absent the row, `pluginScene` simply stays undefined.
313
- const sceneRuntime = ctx.get('tuiScenes');
444
+ const sceneRuntime = getHostSceneRuntime(ctx.get('tuiScenes'));
445
+ const settingsSectionsRuntime = getHostSettingsSections(ctx.get('tuiSettingsSections'));
446
+ // Custom-entry text renderers (optional service, dsh-tui-extensions row):
447
+ // absent the row, unknown plugin event types stay invisible in the
448
+ // transcript, exactly as before the seam existed.
449
+ const rendererRuntime = getHostRenderers(ctx.get('tuiRenderers'));
314
450
  // Shift+Tab session-mode cycle: cordis.yml `modes` wins; absent/empty/
315
451
  // atom-less → the built-in default/plan/full cycle (sessionModes.ts).
316
452
  const { modes: sessionModes, dropped: droppedModeIds } = resolveSessionModes(options.modes);
@@ -320,6 +456,9 @@ export function createChannel(ctx, initialAgent, options) {
320
456
  const listeners = new Set();
321
457
  /** True while a frame-aligned stream notification is pending (emitStream). */
322
458
  let streamNotifyScheduled = false;
459
+ // foldRows incremental cursor (see foldRows): rows only append past the
460
+ // fold line, so each pass touches only newly-eligible rows.
461
+ const foldCursor = { rows: null, index: 0 };
323
462
  let nextNotificationId = 1;
324
463
  /** One-shot context-low warning per session (CC's TokenWarning). */
325
464
  let contextWarned = false;
@@ -405,6 +544,142 @@ export function createChannel(ctx, initialAgent, options) {
405
544
  state.notify(t('send-failed', { err: message }), { color: 'error' });
406
545
  });
407
546
  };
547
+ /**
548
+ * RFC 0005 D-8: a flow parked on a plugin decision must be user-observable.
549
+ * Decisions normally resolve in milliseconds, so the notice only fires
550
+ * once the wait crosses a threshold — a slow plugin (e.g. one showing a
551
+ * managed dialog) then explains the pause instead of looking like the TUI
552
+ * ate the input.
553
+ */
554
+ const DECISION_PENDING_MS = 400;
555
+ const withDecisionPending = (name, pending) => {
556
+ let dismiss;
557
+ const timer = setTimeout(() => {
558
+ // Sticky (timeoutMs 0), D-8: the indicator must cover the WHOLE wait —
559
+ // an auto-expiring notice would vanish after ~4s while the decision,
560
+ // the delivery and every queued FIFO task behind them stay parked,
561
+ // leaving the user with no sign the flow is still waiting. It comes
562
+ // down only when the decision settles (finally below); a decision
563
+ // that never settles keeps its indicator up, which is the truthful
564
+ // state.
565
+ dismiss = state.notify(t('ext-decision-pending', { event: name }), { timeoutMs: 0 });
566
+ }, DECISION_PENDING_MS);
567
+ // Both exits are covered: a fast decision clears the timer before it
568
+ // fires; a slow one dismisses the indicator it raised.
569
+ return pending.finally(() => {
570
+ clearTimeout(timer);
571
+ dismiss?.();
572
+ });
573
+ };
574
+ /**
575
+ * The `tui/input` decision event (pi's `input` seam): the FIRST plugin
576
+ * returning a valid decision wins — transform the text, mark it handled,
577
+ * or cancel it. No listeners (or only crashing/malformed ones) means
578
+ * delivery proceeds unchanged, so a broken plugin can never wedge the
579
+ * input path — and can never skip a later veto listener either
580
+ * (dispatchTuiDecision isolates crashes and normalizes returns per
581
+ * listener instead of bailing on the first object).
582
+ *
583
+ * Decision AND delivery enter one FIFO chain in submission order: a slow
584
+ * listener on A parks A's delivery AND any later submissions behind it —
585
+ * without the chain, B's decision could resolve first and the model would
586
+ * receive B before A. Each submission binds its origin agent AT ENQUEUE,
587
+ * so a session switch landing before OR during its decision drops the
588
+ * stale text with a notice instead of sending the old conversation's
589
+ * words to the new session.
590
+ */
591
+ let inputChain = Promise.resolve();
592
+ const runUserTextDecision = async (text, placement, originAgent, originAgentId) => {
593
+ // Stale detection compares the AGENT REFERENCE, not the id: session ids
594
+ // are reusable (A → /new → /resume A lands back on the same id with a
595
+ // fresh agent), so an id check has an ABA hole. Both origin values are
596
+ // ENQUEUE-time captures (see dispatchUserText): a decision parked behind
597
+ // a slow predecessor must still be judged against the session its text
598
+ // was typed in, not whichever session is live when it finally runs.
599
+ const decision = await withDecisionPending('tui/input', dispatchTuiDecision(ctx, 'tui/input', {
600
+ text,
601
+ delivery: placement === 'steer' ? 'steer' : 'followup',
602
+ sessionId: originAgentId,
603
+ cwd: state.cwd,
604
+ }, normalizeInputDecision));
605
+ if (decision !== undefined) {
606
+ // Both intercepts toast — a bare {cancel}/{handled} must not make the
607
+ // typed line vanish silently (the host-localized fallback mirrors the
608
+ // other decision events' ext-action-cancelled handling).
609
+ if ('cancel' in decision) {
610
+ state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
611
+ return;
612
+ }
613
+ if ('handled' in decision) {
614
+ state.notify(decision.notice ?? t('ext-action-handled'), { timeoutMs: 4000 });
615
+ return;
616
+ }
617
+ text = decision.text.trim();
618
+ }
619
+ if (agent !== originAgent) {
620
+ state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
621
+ return;
622
+ }
623
+ deliverUserText(text, placement);
624
+ };
625
+ const dispatchUserText = (text, placement) => {
626
+ // D-6: bind the submission to the session it was typed in AT ENQUEUE
627
+ // TIME. The FIFO chain may park this task behind a slow predecessor
628
+ // while the user /new's away — capturing the agent at run time would
629
+ // adopt the NEW session as this text's origin and deliver the old
630
+ // conversation's words into it.
631
+ const originAgent = agent;
632
+ const originAgentId = state.agentId;
633
+ inputChain = inputChain.then(() => runUserTextDecision(text, placement, originAgent, originAgentId)).catch((error) => {
634
+ // The chain must survive a failed decision: log, then continue with
635
+ // the next queued submission.
636
+ ctx.logger.warn('dsh-tui: tui/input dispatch failed: %o', error);
637
+ });
638
+ };
639
+ /**
640
+ * The `tui/session-switch` decision event (pi's `session_before_switch`),
641
+ * fired before `/new` or `/resume` replaces the live session (rewind has
642
+ * its own prompt event). The first answering plugin may veto the switch;
643
+ * the reason is toasted here so the fallback string stays host-localized.
644
+ */
645
+ const sessionSwitchVetoed = async (kind, targetSessionId) => {
646
+ // D-6 stale detection captures the AGENT REFERENCE (session ids are
647
+ // reusable — ABA): a slow decision must not let an older /resume roll
648
+ // over a newer session the user already switched to mid-await.
649
+ const originAgent = agent;
650
+ const decision = await withDecisionPending('tui/session-switch', dispatchTuiDecision(ctx, 'tui/session-switch', {
651
+ kind,
652
+ ...(targetSessionId === undefined ? {} : { targetSessionId }),
653
+ sessionId: state.agentId,
654
+ cwd: state.cwd,
655
+ }, normalizeCancelDecision));
656
+ if (agent !== originAgent) {
657
+ // The world changed while the decision parked: drop the pending
658
+ // switch instead of replacing the user's newer session.
659
+ state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
660
+ return true;
661
+ }
662
+ if (decision !== undefined) {
663
+ state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
664
+ return true;
665
+ }
666
+ return false;
667
+ };
668
+ /** Fire-and-forget `tui/session-switched` (parallel): per-session plugin
669
+ * state rebinds here. Listener failures are logged, never propagated —
670
+ * the switch itself already succeeded. */
671
+ const notifySessionSwitched = (kind, sessionId, previousSessionId) => {
672
+ try {
673
+ void dispatchTuiNotification(ctx, 'tui/session-switched', { kind, sessionId, previousSessionId, cwd: state.cwd }).catch((error) => {
674
+ ctx.logger.warn('dsh-tui: tui/session-switched listener failed: %o', error);
675
+ });
676
+ }
677
+ catch (error) {
678
+ // A bare embedder's context may lack the event bus entirely; the
679
+ // switch itself already succeeded, so this stays a log line.
680
+ ctx.logger.warn('dsh-tui: tui/session-switched dispatch failed: %o', error);
681
+ }
682
+ };
408
683
  /** Monotonic token: only the latest `interruptAndDeliver` re-queues, so a
409
684
  * second interrupt while the abort settles cannot double-deliver. */
410
685
  let interruptSeq = 0;
@@ -519,6 +794,45 @@ export function createChannel(ctx, initialAgent, options) {
519
794
  const executeRegistryCommand = async (name, rawInput) => {
520
795
  if (!commandService)
521
796
  return undefined;
797
+ // Resolve the exact definition that execute() will select for this agent.
798
+ // Same names may exist in distinct agent scopes, so a name-only lookup can
799
+ // apply another scope's owner policy.
800
+ const definition = commandService.find(agent, name);
801
+ const owner = commandOwner(ctx, definition);
802
+ // The root checkpoint covers host/direct registrations. A built-in name
803
+ // is not necessarily a namespaced contribution id, so use a deterministic
804
+ // host scope for that case; legacy unscoped denies still conservatively
805
+ // revoke every valid command scope.
806
+ const rootScope = owner?.commandId
807
+ ?? (/^[a-z][a-z0-9]*(?:[.-][a-z0-9][a-z0-9-]*)+$/u.test(name)
808
+ ? name
809
+ : `dsh-tui.${name.toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '') || 'command'}`);
810
+ if (!currentGrantStore().allows({ componentId: 'root' }, 'commands.invoke', rootScope)) {
811
+ ctx.logger.warn('dsh-tui: registry command invocation denied (commands.invoke revoked for "root" in the grants file)');
812
+ ctx.get('tuiEffectLedger')?.record({
813
+ operation: 'bind',
814
+ resource: { kind: 'permission', id: `root:commands.invoke:${rootScope}` },
815
+ result: 'failed',
816
+ errorCode: 'PERMISSION_NOT_GRANTED',
817
+ }, ctx);
818
+ return t('command-invoke-denied');
819
+ }
820
+ // Per-owner gate (C-041): a command REGISTERED BY A PLUGIN through the
821
+ // plugin-host row's mediated registerCommand (see command-attribution.js)
822
+ // is additionally gated on the OWNER's grant, so a denies entry for the
823
+ // plugin closes the host-mediated invocation of ITS commands.
824
+ // Unattributed host/direct registrations remain inside the documented
825
+ // trusted-in-process boundary and have no plugin grant to evaluate.
826
+ if (owner !== undefined && !currentGrantStore().allows({ componentId: owner.componentId, activationId: owner.activationId }, 'commands.invoke', owner.commandId)) {
827
+ ctx.logger.warn(`dsh-tui: registry command "/${name}" invocation denied — owner Component "${owner.componentId}" lost commands.invoke for "${owner.commandId}"`);
828
+ ctx.get('tuiEffectLedger')?.record({
829
+ operation: 'bind',
830
+ resource: { kind: 'permission', id: `${owner.componentId}:commands.invoke:${owner.commandId}` },
831
+ result: 'failed',
832
+ errorCode: 'PERMISSION_NOT_GRANTED',
833
+ }, ctx);
834
+ return t('command-invoke-denied-owner', { name, owner: owner.componentId });
835
+ }
522
836
  try {
523
837
  const execution = await commandService.execute(agent, `/${name}${rawInput}`, new AbortController().signal);
524
838
  // `undefined` = not registered; a handler error surfaces as its
@@ -653,6 +967,7 @@ export function createChannel(ctx, initialAgent, options) {
653
967
  workingActivity: undefined,
654
968
  activityFrames: options.activityFrames,
655
969
  diffLayout: options.diffLayout ?? 'auto',
970
+ thinkingFold: options.thinkingFold ?? 'preview',
656
971
  activityEnabled: options.activity !== false,
657
972
  contextBarEnabled: options.contextBar !== false,
658
973
  agentPreset: options.agentPreset,
@@ -701,7 +1016,7 @@ export function createChannel(ctx, initialAgent, options) {
701
1016
  };
702
1017
  },
703
1018
  emit() {
704
- foldRows(state.rows, MAX_ROWS);
1019
+ foldRows(state.rows, MAX_ROWS, foldCursor);
705
1020
  state.version += 1;
706
1021
  for (const listener of listeners)
707
1022
  listener();
@@ -719,7 +1034,7 @@ export function createChannel(ctx, initialAgent, options) {
719
1034
  streamNotifyScheduled = true;
720
1035
  const timer = setTimeout(() => {
721
1036
  streamNotifyScheduled = false;
722
- foldRows(state.rows, MAX_ROWS);
1037
+ foldRows(state.rows, MAX_ROWS, foldCursor);
723
1038
  for (const listener of listeners)
724
1039
  listener();
725
1040
  }, 16);
@@ -778,7 +1093,7 @@ export function createChannel(ctx, initialAgent, options) {
778
1093
  // The current session is being used — move it to the MRU front
779
1094
  // (/resume sorts by last-used).
780
1095
  touchSession(state.agentId);
781
- deliverUserText(trimmed, 'followup');
1096
+ void dispatchUserText(trimmed, 'followup');
782
1097
  },
783
1098
  /** Steer a message into the RUNNING turn (Codex/pi semantics): it is
784
1099
  * injected at the next step boundary of the current turn and the agent
@@ -788,11 +1103,13 @@ export function createChannel(ctx, initialAgent, options) {
788
1103
  if (!trimmed)
789
1104
  return;
790
1105
  touchSession(state.agentId);
791
- // Official dsh-agent rc.6: steer() is synchronous void — the message
792
- // enters the next-step inbox. A rejected step leaves it parked for the
793
- // next wake; the inbox events below retire the preview (claimed →
794
- // turn boundary, discarded → cancel).
795
- deliverUserText(trimmed, 'steer');
1106
+ // Same tui/input decision pass as submit; the delivery re-validates
1107
+ // the live agent after the await. Official dsh-agent rc.6: steer() is
1108
+ // synchronous void — the message enters the next-step inbox; a
1109
+ // rejected step leaves it parked for the next wake, and the inbox
1110
+ // events retire the preview (claimed → turn boundary, discarded →
1111
+ // cancel).
1112
+ void dispatchUserText(trimmed, 'steer');
796
1113
  },
797
1114
  /** Pull a pending message back out of the inbox (Alt+Up): it returns to
798
1115
  * the input for editing instead of being delivered. */
@@ -838,7 +1155,10 @@ export function createChannel(ctx, initialAgent, options) {
838
1155
  return;
839
1156
  for (const text of queued) {
840
1157
  touchSession(state.agentId);
841
- deliverUserText(text, 'followup');
1158
+ // Same tui/input decision pass as a typed submit: Ctrl+Enter must
1159
+ // not bypass a plugin's cancel/transform policy, and re-queued
1160
+ // texts keep submission order through the one FIFO chain.
1161
+ dispatchUserText(text, 'followup');
842
1162
  }
843
1163
  };
844
1164
  if (typeof whenIdle === 'function') {
@@ -851,7 +1171,43 @@ export function createChannel(ctx, initialAgent, options) {
851
1171
  }
852
1172
  return queued.length;
853
1173
  },
854
- async rewindTo(row) {
1174
+ /**
1175
+ * The `tui/rewind-prompt` decision event (pi's `session_before_fork`):
1176
+ * fired when the rewind picker confirms a message, before any fork
1177
+ * work. The first answering plugin may cancel the rewind (the picker
1178
+ * stays open; the reason is toasted here so the UI string stays
1179
+ * host-localized when absent) or offer extra modes rendered in the
1180
+ * confirm pane. Returns 'cancel', the modes, or null for "no opinion".
1181
+ */
1182
+ async promptRewind(row) {
1183
+ if (row.seq === undefined)
1184
+ return null;
1185
+ // D-6, same as the other decision points: a slow /new or /resume can
1186
+ // replace the agent while this decision parks. Without the identity
1187
+ // check the picker would go on to show the OLD session's row in the
1188
+ // confirm pane and rewindTo would cut the NEW session at the old
1189
+ // seq — a wrong rewind or a fork failure. Compare agent REFERENCES
1190
+ // (session ids are reusable — ABA) and stale-cancel.
1191
+ const originAgent = agent;
1192
+ const decision = await withDecisionPending('tui/rewind-prompt', dispatchTuiDecision(ctx, 'tui/rewind-prompt', {
1193
+ text: row.text,
1194
+ seq: row.seq,
1195
+ sessionId: state.agentId,
1196
+ cwd: state.cwd,
1197
+ }, normalizeRewindPromptDecision));
1198
+ if (agent !== originAgent) {
1199
+ state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
1200
+ return 'cancel';
1201
+ }
1202
+ if (decision === undefined)
1203
+ return null;
1204
+ if ('cancel' in decision) {
1205
+ state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
1206
+ return 'cancel';
1207
+ }
1208
+ return { modes: decision.modes };
1209
+ },
1210
+ async rewindTo(row, mode = null) {
855
1211
  if (row.seq === undefined)
856
1212
  return null;
857
1213
  const sessions = ctx.get('sessions');
@@ -947,6 +1303,10 @@ export function createChannel(ctx, initialAgent, options) {
947
1303
  // counters land back at the rewind point, matching the fork).
948
1304
  streaming = undefined;
949
1305
  reasoning = undefined;
1306
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1307
+ // keep it out of the next turn's settle logs and revive cache.
1308
+ sealedReasoning.length = 0;
1309
+ lastReasoningRow = undefined;
950
1310
  toolCards.clear();
951
1311
  nextRowId = 0;
952
1312
  state.rows.length = 0;
@@ -980,10 +1340,16 @@ export function createChannel(ctx, initialAgent, options) {
980
1340
  thinking: 0,
981
1341
  tools: 0,
982
1342
  };
983
- for (const event of coalesceReplayEvents(seed))
984
- renderEvent(event);
1343
+ replayEvents(seed);
1344
+ settleStreaming();
1345
+ // A seed ending mid-turn replays a turn/start that set working=true;
1346
+ // the boot path resets this after replay — mirror it here so an idle
1347
+ // rewound agent doesn't sit with a live spinner (a still-running
1348
+ // agent re-asserts on its next event).
1349
+ state.working = handle.agent.status === 'running';
985
1350
  // Rebind subscriptions to the new agent, then free the old one.
986
1351
  const oldHandle = currentHandle;
1352
+ const sourceSessionId = String(agent.session.id);
987
1353
  agent = handle.agent;
988
1354
  currentHandle = handle;
989
1355
  bindAgent();
@@ -994,6 +1360,43 @@ export function createChannel(ctx, initialAgent, options) {
994
1360
  touchSession(childId);
995
1361
  state.emit();
996
1362
  void oldHandle?.dispose().catch(() => { });
1363
+ // Decision-event pair around the completed rewind: `tui/rewind-done`
1364
+ // (the first non-empty string is toasted as the post-rewind summary,
1365
+ // e.g. a plugin reporting restored files) and the generic
1366
+ // `tui/session-switched` notification. Listener failures are logged,
1367
+ // never surfaced — the rewind itself already succeeded.
1368
+ //
1369
+ // rewind-done is a post-hoc summary, NOT a gate, so it is dispatched
1370
+ // DECOUPLED from the return value: the picker is already closed and
1371
+ // PromptInput is live — awaiting a slow listener here would delay the
1372
+ // picked text's return to the draft, letting its late arrival
1373
+ // overwrite whatever the user typed meanwhile, and a listener that
1374
+ // never settles would park tui/session-switched forever. The summary
1375
+ // toasts whenever it lands.
1376
+ try {
1377
+ void dispatchTuiDecision(ctx, 'tui/rewind-done', {
1378
+ text: row.text,
1379
+ mode,
1380
+ boundarySeq: boundary,
1381
+ sourceSessionId,
1382
+ childSessionId: String(childId),
1383
+ sessionId: String(childId),
1384
+ cwd: state.cwd,
1385
+ }, normalizeRewindDoneSummary)
1386
+ .then(summary => {
1387
+ if (summary !== undefined)
1388
+ state.notify(summary, { timeoutMs: 6000 });
1389
+ })
1390
+ .catch((error) => {
1391
+ ctx.logger.warn('dsh-tui: tui/rewind-done dispatch failed: %o', error);
1392
+ });
1393
+ }
1394
+ catch (error) {
1395
+ // A bare embedder's context may lack the event bus entirely; the
1396
+ // rewind itself already succeeded, so this stays a log line.
1397
+ ctx.logger.warn('dsh-tui: tui/rewind-done dispatch failed: %o', error);
1398
+ }
1399
+ notifySessionSwitched('rewind', String(childId), sourceSessionId);
997
1400
  return row.text;
998
1401
  },
999
1402
  async resumeTo(sessionId) {
@@ -1002,13 +1405,18 @@ export function createChannel(ctx, initialAgent, options) {
1002
1405
  // resolving through DSH_TUI_RESUME_SESSION at boot).
1003
1406
  if (state.working) {
1004
1407
  state.notify(t('resume-while-working'), { color: 'warning' });
1005
- return false;
1408
+ return { ok: false, reason: 'working' };
1006
1409
  }
1007
1410
  const agents = ctx.get('agents');
1008
1411
  if (!agents) {
1009
1412
  state.notify(t('resume-unavailable'), { color: 'error' });
1010
- return false;
1413
+ return { ok: false, reason: 'unavailable' };
1011
1414
  }
1415
+ // Plugin veto point (tui/session-switch): before any read of the
1416
+ // target — a veto leaves the live session and its transcript
1417
+ // untouched.
1418
+ if (await sessionSwitchVetoed('resume', sessionId))
1419
+ return { ok: false, reason: 'cancelled' };
1012
1420
  let handle;
1013
1421
  // Compat boundary: register vouched-for legacy event types (e.g.
1014
1422
  // activity/status from pre-#143 logs) in every reachable dsh-session
@@ -1039,7 +1447,7 @@ export function createChannel(ctx, initialAgent, options) {
1039
1447
  catch (error) {
1040
1448
  const message = error instanceof Error ? error.message : String(error);
1041
1449
  state.notify(t('resume-failed', { err: message }), { color: 'error', timeoutMs: 8000 });
1042
- return false;
1450
+ return { ok: false, reason: 'failed', error: message };
1043
1451
  }
1044
1452
  try {
1045
1453
  // `/resume` is an explicit adoption of this persisted conversation.
@@ -1054,6 +1462,10 @@ export function createChannel(ctx, initialAgent, options) {
1054
1462
  // rewindTo, plus the context window which the replay re-derives).
1055
1463
  streaming = undefined;
1056
1464
  reasoning = undefined;
1465
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1466
+ // keep it out of the next turn's settle logs and revive cache.
1467
+ sealedReasoning.length = 0;
1468
+ lastReasoningRow = undefined;
1057
1469
  toolCards.clear();
1058
1470
  nextRowId = 0;
1059
1471
  state.rows.length = 0;
@@ -1106,11 +1518,15 @@ export function createChannel(ctx, initialAgent, options) {
1106
1518
  thinking: 0,
1107
1519
  tools: 0,
1108
1520
  };
1109
- for (const event of coalesceReplayEvents(handle.agent.session.events))
1110
- renderEvent(event);
1521
+ replayEvents(handle.agent.session.events);
1111
1522
  settleStreaming();
1523
+ // A log ending mid-turn replays a turn/start that set working=true;
1524
+ // mirror the boot path's post-replay reset (a still-running agent
1525
+ // re-asserts on its next event).
1526
+ state.working = handle.agent.status === 'running';
1112
1527
  // Rebind subscriptions to the resumed agent, then free the old one.
1113
1528
  const oldHandle = currentHandle;
1529
+ const previousSessionId = String(agent.session.id);
1114
1530
  agent = handle.agent;
1115
1531
  currentHandle = handle;
1116
1532
  bindAgent();
@@ -1124,7 +1540,8 @@ export function createChannel(ctx, initialAgent, options) {
1124
1540
  state.emit();
1125
1541
  void oldHandle?.dispose().catch(() => { });
1126
1542
  clearStagedImages();
1127
- return true;
1543
+ notifySessionSwitched('resume', sessionId, previousSessionId);
1544
+ return { ok: true };
1128
1545
  },
1129
1546
  async newSession() {
1130
1547
  // `/new` — start a fresh conversation: brand-new agent + session, the
@@ -1143,6 +1560,10 @@ export function createChannel(ctx, initialAgent, options) {
1143
1560
  });
1144
1561
  return false;
1145
1562
  }
1563
+ // Plugin veto point (tui/session-switch): no side effects have
1564
+ // happened yet — the session id below is not even allocated.
1565
+ if (await sessionSwitchVetoed('new'))
1566
+ return false;
1146
1567
  const sessionId = SessionId(randomUUID());
1147
1568
  let handle;
1148
1569
  // A fresh session composes the caller's DEFAULT preset: the cordis.yml
@@ -1197,6 +1618,10 @@ export function createChannel(ctx, initialAgent, options) {
1197
1618
  }
1198
1619
  streaming = undefined;
1199
1620
  reasoning = undefined;
1621
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1622
+ // keep it out of the next turn's settle logs and revive cache.
1623
+ sealedReasoning.length = 0;
1624
+ lastReasoningRow = undefined;
1200
1625
  toolCards.clear();
1201
1626
  nextRowId = 0;
1202
1627
  state.rows.length = 0;
@@ -1234,6 +1659,7 @@ export function createChannel(ctx, initialAgent, options) {
1234
1659
  tools: 0,
1235
1660
  };
1236
1661
  const oldHandle = currentHandle;
1662
+ const previousSessionId = String(agent.session.id);
1237
1663
  agent = handle.agent;
1238
1664
  currentHandle = handle;
1239
1665
  bindAgent();
@@ -1245,6 +1671,7 @@ export function createChannel(ctx, initialAgent, options) {
1245
1671
  touchSession(handle.agent.id);
1246
1672
  void oldHandle?.dispose().catch(() => { });
1247
1673
  clearStagedImages();
1674
+ notifySessionSwitched('new', String(handle.agent.id), previousSessionId);
1248
1675
  return true;
1249
1676
  },
1250
1677
  listWorkspaces() {
@@ -1368,6 +1795,10 @@ export function createChannel(ctx, initialAgent, options) {
1368
1795
  }
1369
1796
  streaming = undefined;
1370
1797
  reasoning = undefined;
1798
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1799
+ // keep it out of the next turn's settle logs and revive cache.
1800
+ sealedReasoning.length = 0;
1801
+ lastReasoningRow = undefined;
1371
1802
  toolCards.clear();
1372
1803
  nextRowId = 0;
1373
1804
  state.rows.length = 0;
@@ -1404,9 +1835,10 @@ export function createChannel(ctx, initialAgent, options) {
1404
1835
  thinking: 0,
1405
1836
  tools: 0,
1406
1837
  };
1407
- for (const event of coalesceReplayEvents(seed))
1408
- renderEvent(event);
1838
+ replayEvents(seed);
1409
1839
  settleStreaming();
1840
+ // Same mid-turn-seed spinner reset as resume above.
1841
+ state.working = handle.agent.status === 'running';
1410
1842
  const oldHandle = currentHandle;
1411
1843
  agent = handle.agent;
1412
1844
  currentHandle = handle;
@@ -1456,13 +1888,24 @@ export function createChannel(ctx, initialAgent, options) {
1456
1888
  };
1457
1889
  state.notifications.push(item);
1458
1890
  state.emit();
1459
- setTimeout(() => {
1891
+ const remove = () => {
1460
1892
  const index = state.notifications.indexOf(item);
1461
1893
  if (index >= 0) {
1462
1894
  state.notifications.splice(index, 1);
1463
1895
  state.emit();
1464
1896
  }
1465
- }, item.timeoutMs);
1897
+ };
1898
+ // timeoutMs 0 = sticky: no expiry timer, the dismiss handle is the
1899
+ // only way out (a decision-parked indicator must outlive the wait it
1900
+ // describes — auto-expiring it would hide a still-parked flow, D-8).
1901
+ const expire = item.timeoutMs > 0 ? setTimeout(remove, item.timeoutMs) : undefined;
1902
+ // Early-dismiss handle for flows that know their notice went stale
1903
+ // (e.g. a decision-parked indicator whose decision just landed).
1904
+ return () => {
1905
+ if (expire !== undefined)
1906
+ clearTimeout(expire);
1907
+ remove();
1908
+ };
1466
1909
  },
1467
1910
  setDiffLayout(layout) {
1468
1911
  if (layout === state.diffLayout)
@@ -1470,6 +1913,12 @@ export function createChannel(ctx, initialAgent, options) {
1470
1913
  state.diffLayout = layout;
1471
1914
  state.emit();
1472
1915
  },
1916
+ setThinkingFold(mode) {
1917
+ if (mode === state.thinkingFold)
1918
+ return;
1919
+ state.thinkingFold = mode;
1920
+ state.emit();
1921
+ },
1473
1922
  setActivityFrames(name) {
1474
1923
  if (!isPresetName(name)) {
1475
1924
  state.notify(t('unknown-activity-preset', { name }), { color: 'error' });
@@ -1650,12 +2099,10 @@ export function createChannel(ctx, initialAgent, options) {
1650
2099
  return settingsHostCache;
1651
2100
  },
1652
2101
  settingsSections() {
1653
- const sections = ctx.get('tuiSettingsSections');
1654
- return sections?.list() ?? [];
2102
+ return settingsSectionsRuntime?.list() ?? [];
1655
2103
  },
1656
2104
  subscribeSettingsSections(listener) {
1657
- const sections = ctx.get('tuiSettingsSections');
1658
- return sections?.subscribe(listener) ?? (() => { });
2105
+ return settingsSectionsRuntime?.subscribe(listener) ?? (() => { });
1659
2106
  },
1660
2107
  providerSetup() {
1661
2108
  // The `/provider` wizard's runtime surface, over the dsh-base seams:
@@ -1839,16 +2286,49 @@ export function createChannel(ctx, initialAgent, options) {
1839
2286
  state.notify(t('compact-while-working'), { color: 'warning' });
1840
2287
  return;
1841
2288
  }
1842
- const signal = new AbortController().signal;
1843
- state.notify(t('compact-working'));
1844
- void compactService
1845
- .compactNow(agent, signal)
1846
- .then((result) => {
1847
- state.notify(result ? t('compact-done') : t('compact-nothing'));
1848
- })
1849
- .catch((error) => {
1850
- state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
1851
- });
2289
+ // Plugin veto point (tui/compact): the first answering plugin may
2290
+ // cancel the compaction before anything runs.
2291
+ const originAgentId = state.agentId;
2292
+ // Compare the AGENT REFERENCE after the await, not the id — session
2293
+ // ids are reusable (A → /new → /resume A returns the same id on a new
2294
+ // agent), so an id comparison has an ABA hole that would hand the new
2295
+ // agent to the OLD scope's compaction service.
2296
+ const originAgent = agent;
2297
+ void (async () => {
2298
+ const decision = await withDecisionPending('tui/compact', dispatchTuiDecision(ctx, 'tui/compact', {
2299
+ sessionId: originAgentId,
2300
+ cwd: state.cwd,
2301
+ }, normalizeCancelDecision));
2302
+ if (decision !== undefined) {
2303
+ state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
2304
+ return;
2305
+ }
2306
+ // Stale-drop (same rule as tui/input): the await parked us while the
2307
+ // user switched sessions — `compactService` was resolved through the
2308
+ // OLD agent's scope chain, and the mutable `agent` now points at the
2309
+ // new session. Running now would hand the new agent to the old
2310
+ // service (or call into an unloaded one).
2311
+ if (agent !== originAgent) {
2312
+ state.notify(t('ext-compact-stale'), { color: 'warning', timeoutMs: 4000 });
2313
+ return;
2314
+ }
2315
+ if (state.working) {
2316
+ // The await above gave a queued turn time to start; compacting
2317
+ // mid-turn now would be the same race the check upfront avoided.
2318
+ state.notify(t('compact-while-working'), { color: 'warning' });
2319
+ return;
2320
+ }
2321
+ const signal = new AbortController().signal;
2322
+ state.notify(t('compact-working'));
2323
+ void compactService
2324
+ .compactNow(agent, signal)
2325
+ .then((result) => {
2326
+ state.notify(result ? t('compact-done') : t('compact-nothing'));
2327
+ })
2328
+ .catch((error) => {
2329
+ state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
2330
+ });
2331
+ })();
1852
2332
  },
1853
2333
  runExternalCommand(name, rawInput) {
1854
2334
  return executeRegistryCommand(name, rawInput);
@@ -2017,8 +2497,23 @@ export function createChannel(ctx, initialAgent, options) {
2017
2497
  if (existsSync(LEGACY_DATA_DIR)) {
2018
2498
  lines.push(t('doctor-legacy-dir'));
2019
2499
  }
2500
+ // Plugin-spec diagnostics (v0.15): the runtime generation and the
2501
+ // vendored registry self-check, both soft-probed (#183 discipline).
2502
+ const pluginHost = ctx.get('tuiPluginHost');
2503
+ lines.push(t('doctor-plugin-generation', { id: pluginHost?.generationId ?? t('doctor-plugin-host-missing') }));
2504
+ const violations = pluginHost?.selfCheck();
2505
+ lines.push(t('doctor-plugin-registry', {
2506
+ state: violations === undefined ? t('doctor-plugin-host-missing') : violations.length === 0 ? '✓' : `✗ ${violations.length}`,
2507
+ }));
2020
2508
  return lines;
2021
2509
  },
2510
+ pluginsInfo(args) {
2511
+ const host = ctx.get('tuiPluginHost');
2512
+ return pluginsInfoLines(args, {
2513
+ grants: host?.grants ?? currentGrantStore(),
2514
+ host: host?.describe(),
2515
+ });
2516
+ },
2022
2517
  async listSubagents() {
2023
2518
  const subagents = ctx.get('subagents');
2024
2519
  if (!subagents)
@@ -2372,10 +2867,21 @@ export function createChannel(ctx, initialAgent, options) {
2372
2867
  },
2373
2868
  });
2374
2869
  skillCommands.set(name, { dispose, description });
2870
+ ctx.get('tuiEffectLedger')?.record({ operation: 'create', resource: { kind: 'command', id: name }, result: 'applied' }, ctx);
2375
2871
  }
2376
2872
  catch (error) {
2873
+ // C-041: a duplicate registration arrives as a plain-message Error
2874
+ // from dsh-commands; map it onto the contract code before handling
2875
+ // (the refusal path itself is unchanged).
2876
+ const mapped = mapCommandError(error);
2377
2877
  skillCommandsRefused.add(name);
2378
- ctx.logger.warn(`skill commands: "${name}" not registrable: %o`, error);
2878
+ ctx.logger.warn(`skill commands: "${name}" not registrable%s: %o`, hasCommandErrorCode(mapped, 'DUPLICATE_CONTRIBUTION_ID') ? ' (DUPLICATE_CONTRIBUTION_ID)' : '', mapped);
2879
+ ctx.get('tuiEffectLedger')?.record({
2880
+ operation: 'create',
2881
+ resource: { kind: 'command', id: name },
2882
+ result: 'failed',
2883
+ errorCode: hasCommandErrorCode(mapped, 'DUPLICATE_CONTRIBUTION_ID') ? 'DUPLICATE_CONTRIBUTION_ID' : 'COMMAND_FAILED',
2884
+ }, ctx);
2379
2885
  }
2380
2886
  }
2381
2887
  };
@@ -2541,16 +3047,60 @@ ${output}
2541
3047
  }
2542
3048
  return streaming;
2543
3049
  };
2544
- const ensureReasoning = (seq) => {
3050
+ /** Latest reasoning row keyed by its (turn, step) — lets a resumed
3051
+ * mid-step stream REVIVE the row the replay sealed (crash-orphan tail:
3052
+ * replay folds the partial row, live continuation chunks would
3053
+ * otherwise open a SECOND row for the same step, splitting one
3054
+ * thinking block in two). */
3055
+ let lastReasoningRow;
3056
+ const ensureReasoning = (seq, turn, step) => {
2545
3057
  if (reasoning === undefined) {
3058
+ // Same-step revive: the sealed row is this step's thinking — continue
3059
+ // it (durationMs carried over via reasoningStart back-dating).
3060
+ if (lastReasoningRow !== undefined &&
3061
+ turn !== undefined &&
3062
+ lastReasoningRow.turn === turn &&
3063
+ lastReasoningRow.step === step) {
3064
+ reasoning = lastReasoningRow.row;
3065
+ reasoning.streaming = true;
3066
+ const sealedIdx = sealedReasoning.indexOf(reasoning);
3067
+ if (sealedIdx !== -1)
3068
+ sealedReasoning.splice(sealedIdx, 1);
3069
+ reasoningStart = Date.now() - (reasoning.durationMs ?? 0);
3070
+ logForDebugging('thinking: revived sealed reasoning row for same step');
3071
+ return reasoning;
3072
+ }
2546
3073
  reasoningStart = Date.now();
2547
3074
  reasoning = { id: nextRowId, kind: 'reasoning', text: '', streaming: true, ...seq !== undefined ? { seq } : {} };
2548
3075
  nextRowId += 1;
2549
3076
  state.rows.push(reasoning);
2550
3077
  logForDebugging('thinking: reasoning row open (expanded)');
2551
3078
  }
3079
+ if (turn !== undefined && step !== undefined) {
3080
+ lastReasoningRow = { row: reasoning, turn, step };
3081
+ }
2552
3082
  return reasoning;
2553
3083
  };
3084
+ /** Fold the live reasoning preview the moment the model moves PAST
3085
+ * thinking — the answer's first text token or a tool call — not at
3086
+ * `assistant/message` (end of step). A long reply pushes the thinking
3087
+ * block into terminal scrollback long before the message seals, and
3088
+ * scrollback rows cannot be repainted (the cursor cannot reach them),
3089
+ * so a late fold leaves a stale unfolded preview frozen above the
3090
+ * window — the user scrolls up and the thinking looks "not folded".
3091
+ * Folding while the block still sits in the live window keeps the
3092
+ * shrink inside the diff engine's reachable region. Preview mode only
3093
+ * (`full` holds every block open until turn settle by design). */
3094
+ const foldLiveReasoning = (where) => {
3095
+ if (reasoning === undefined || state.thinkingFold !== 'preview')
3096
+ return;
3097
+ const duration = Math.max(0, Date.now() - reasoningStart);
3098
+ reasoning.durationMs = duration;
3099
+ reasoning.streaming = false;
3100
+ sealedReasoning.push(reasoning);
3101
+ reasoning = undefined;
3102
+ logForDebugging(`thinking: folded at ${where} (${duration}ms)`);
3103
+ };
2554
3104
  const settleStreaming = () => {
2555
3105
  if (streaming !== undefined)
2556
3106
  streaming.streaming = false;
@@ -2616,6 +3166,24 @@ ${output}
2616
3166
  };
2617
3167
  }
2618
3168
  };
3169
+ /** True while the durable transcript is being replayed (boot /resume /
3170
+ * rewind / model-switch fork). The assistant/message reasoning-rebuild
3171
+ * branch below must run ONLY on this path: in a live stream the chunks
3172
+ * already created the reasoning row, and foldLiveReasoning clears the
3173
+ * `reasoning` handle before assistant/message arrives — so
3174
+ * `reasoning === undefined` alone cannot tell replay from live, and
3175
+ * using it would rebuild a second thinking block per step. */
3176
+ let replaying = false;
3177
+ const replayEvents = (events) => {
3178
+ replaying = true;
3179
+ try {
3180
+ for (const event of prepareReplayEvents(events))
3181
+ renderEvent(event);
3182
+ }
3183
+ finally {
3184
+ replaying = false;
3185
+ }
3186
+ };
2619
3187
  const renderEvent = (event) => {
2620
3188
  switch (event.type) {
2621
3189
  case 'user/message': {
@@ -2697,13 +3265,18 @@ ${output}
2697
3265
  const chunk = event.data.chunk;
2698
3266
  if (chunk.type === 'text-delta') {
2699
3267
  if (chunk.text) {
3268
+ // Fold the thinking preview while it is still in the live
3269
+ // window (see foldLiveReasoning) — before this text grows the
3270
+ // transcript and pushes the block into scrollback.
3271
+ foldLiveReasoning('first text token');
2700
3272
  ensureStreaming(event.seq).text += chunk.text;
2701
3273
  state.responseChars += chunk.text.length;
2702
3274
  }
2703
3275
  }
2704
3276
  else if (chunk.type === 'reasoning-delta') {
2705
- if (chunk.text)
2706
- ensureReasoning(event.seq).text += chunk.text;
3277
+ if (chunk.text) {
3278
+ ensureReasoning(event.seq, event.data.turn, event.data.step).text += chunk.text;
3279
+ }
2707
3280
  }
2708
3281
  const step = tpsStep;
2709
3282
  if (step !== undefined &&
@@ -2724,6 +3297,30 @@ ${output}
2724
3297
  }
2725
3298
  case 'assistant/message': {
2726
3299
  const text = textOf(event.data.message.content);
3300
+ // Replay without chunk deltas (prepareReplayEvents drops settled
3301
+ // ones): rebuild the reasoning row from the sealed message's
3302
+ // reasoning blocks. Replay-only — gated on the `replaying` flag,
3303
+ // not on `reasoning === undefined`: a live stream's chunks already
3304
+ // created the row, and foldLiveReasoning has cleared the `reasoning`
3305
+ // handle by the time this event lands, so the undefined check alone
3306
+ // would rebuild a duplicate thinking block per step. Pushed BEFORE
3307
+ // the assistant row so the transcript order matches the live
3308
+ // stream; settled (folded) immediately, durationMs unknown without
3309
+ // a live clock.
3310
+ if (replaying && reasoning === undefined) {
3311
+ const reasoningText = event.data.message.content
3312
+ .map(block => (block.type === 'reasoning' ? block.text : ''))
3313
+ .join('');
3314
+ if (reasoningText !== '') {
3315
+ state.rows.push({
3316
+ id: nextRowId,
3317
+ kind: 'reasoning',
3318
+ text: reasoningText,
3319
+ seq: event.seq,
3320
+ });
3321
+ nextRowId += 1;
3322
+ }
3323
+ }
2727
3324
  // Reasoning/tool-only steps emit no text: creating an assistant row
2728
3325
  // anyway leaves an empty `●` bullet in the transcript. A pre-existing
2729
3326
  // streaming row always has text (ensureStreaming is only reached on
@@ -2737,10 +3334,16 @@ ${output}
2737
3334
  }
2738
3335
  streaming = undefined;
2739
3336
  if (reasoning !== undefined) {
2740
- // Seal, don't fold: the per-step duration settles here, but the
2741
- // row keeps streaming=true (expanded) until turn/end — WebUI
2742
- // keepOpen parity. The next step's reasoning opens a fresh row.
3337
+ // Backstop fold: reasoning whose step ended with no text token
3338
+ // and no tool call (foldLiveReasoning handles those earlier —
3339
+ // while the block is still in the repaintable live window;
3340
+ // here a long reply may already have pushed it into scrollback,
3341
+ // where the shrink cannot be repainted). `full` mode
3342
+ // (/settings opt-in) keeps the block expanded until turn settle
3343
+ // — settleStreaming folds the sealed rows then.
2743
3344
  reasoning.durationMs = Math.max(0, Date.now() - reasoningStart);
3345
+ if (state.thinkingFold === 'preview')
3346
+ reasoning.streaming = false;
2744
3347
  sealedReasoning.push(reasoning);
2745
3348
  logForDebugging(`thinking: step sealed (${reasoning.durationMs}ms), expanded until turn/end`);
2746
3349
  }
@@ -2809,6 +3412,10 @@ ${output}
2809
3412
  // no card is a no-op below.
2810
3413
  if (event.data.name === 'ask_user_question')
2811
3414
  break;
3415
+ // Reasoning that led to a tool call is done thinking — fold the
3416
+ // preview now, before the tool card grows the transcript past it
3417
+ // (see foldLiveReasoning).
3418
+ foldLiveReasoning('tool call');
2812
3419
  const card = {
2813
3420
  id: nextRowId,
2814
3421
  kind: 'tool',
@@ -2976,13 +3583,36 @@ ${output}
2976
3583
  text: t('agent-preset-switched', { preset: data.agentPreset ?? 'unknown' }),
2977
3584
  });
2978
3585
  nextRowId += 1;
3586
+ break;
3587
+ }
3588
+ // Custom plugin events (tuiRenderers seam): a registered renderer
3589
+ // maps the payload to text rows — title as a local row, body as
3590
+ // preview-clipped local-output rows, same shape pushLocal uses.
3591
+ // Runs on the live stream AND on replay (resume/rewind), so the
3592
+ // projection must stay total; the runtime isolates renderer
3593
+ // crashes per type.
3594
+ if (rendererRuntime !== undefined) {
3595
+ const rendered = rendererRuntime.render(event.type, event.data);
3596
+ if (rendered !== undefined) {
3597
+ if (rendered.title !== undefined && rendered.title !== '') {
3598
+ state.rows.push({ id: nextRowId, kind: 'local', text: rendered.title });
3599
+ nextRowId += 1;
3600
+ }
3601
+ for (const line of rendered.lines) {
3602
+ state.rows.push({
3603
+ id: nextRowId,
3604
+ kind: 'local-output',
3605
+ text: preview(String(line), LOCAL_OUTPUT_LIMIT),
3606
+ });
3607
+ nextRowId += 1;
3608
+ }
3609
+ }
2979
3610
  }
2980
3611
  break;
2981
3612
  }
2982
3613
  };
2983
3614
  // Replay the durable transcript first, then follow live events.
2984
- for (const event of coalesceReplayEvents(agent.session.events))
2985
- renderEvent(event);
3615
+ replayEvents(agent.session.events);
2986
3616
  settleStreaming();
2987
3617
  // Attached to an idle agent: any replayed turn/start belongs to a previous
2988
3618
  // session run, so the spinner must not come up on boot.
@@ -3113,6 +3743,10 @@ ${output}
3113
3743
  ctx.on('session/event', (session, event) => {
3114
3744
  if (session !== agent.session)
3115
3745
  return;
3746
+ // Observation broker (C-042): maps user/message + assistant/message
3747
+ // into grant-gated envelopes; every other event type is a no-op, and
3748
+ // publish never throws into this arm.
3749
+ messageObserver?.publish(session, event);
3116
3750
  activityTracker.onSessionEvent(event);
3117
3751
  renderWorkingActivity();
3118
3752
  // Mode-affecting atoms fold into the Shift+Tab mode indicator the
@@ -3274,47 +3908,60 @@ function usageOutputTokens(usage) {
3274
3908
  : undefined;
3275
3909
  }
3276
3910
  /**
3277
- * Recursive `@` file listing through the leaf's fs service (dsh-fs-local):
3278
- * walks up to MAX_DEPTH levels below `root`, skipping VCS/dependency dirs,
3279
- * returning relative paths (directories with a trailing `/`, matching the
3280
- * FileSuggestions tag logic) capped at MAX_FILES entries. Best-effort —
3281
- * unreadable subtrees are skipped, not fatal.
3911
+ * `@` file listing through the leaf's fs service (dsh-fs-local): skips
3912
+ * VCS/dependency/build dirs and rotates across directory listings so one
3913
+ * large subtree cannot consume the global MAX_FILES budget. That budget also
3914
+ * bounds directory reads without imposing an arbitrary source-tree depth.
3915
+ * Relative directories retain the trailing `/` that FileSuggestions expects.
3916
+ * Unreadable subtrees are skipped, not fatal.
3282
3917
  */
3283
3918
  async function listFilesDeep(fs, root) {
3284
3919
  if (!fs)
3285
3920
  return [];
3286
3921
  const out = [];
3287
- const SKIP = new Set(['node_modules', '.git', '.hg', '.svn', '.DS_Store', 'dist', 'build']);
3288
- const MAX_DEPTH = 3;
3922
+ const SKIP = new Set(['node_modules', '.git', '.hg', '.svn', '.DS_Store', 'dist']);
3923
+ const BUILD_DIR = /^(?:build(?:[-_].*)?|cmake-build(?:[-_].*)?)$/i;
3289
3924
  const MAX_FILES = 100;
3290
- const walk = async (dir, prefix, depth) => {
3291
- if (depth > MAX_DEPTH || out.length >= MAX_FILES)
3292
- return;
3293
- let entries = [];
3294
- try {
3295
- const target = await fs.resolve(dir);
3296
- entries = await fs.listDir(target);
3297
- }
3298
- catch {
3299
- return; // unreadable subtree — skip
3925
+ const directories = [{ dir: root, prefix: '', index: 0 }];
3926
+ while (directories.length > 0 && out.length < MAX_FILES) {
3927
+ const current = directories.shift();
3928
+ if (!current)
3929
+ continue;
3930
+ if (!current.entries) {
3931
+ try {
3932
+ const target = await fs.resolve(current.dir);
3933
+ current.entries = await fs.listDir(target);
3934
+ }
3935
+ catch {
3936
+ continue; // unreadable subtree — skip
3937
+ }
3300
3938
  }
3301
- for (const entry of entries) {
3302
- if (out.length >= MAX_FILES)
3303
- return;
3304
- if (SKIP.has(entry.name))
3939
+ let entry;
3940
+ while (current.index < current.entries.length) {
3941
+ const candidate = current.entries[current.index++];
3942
+ if (SKIP.has(candidate.name) || BUILD_DIR.test(candidate.name))
3305
3943
  continue;
3306
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
3307
- if (entry.type === 'directory') {
3308
- out.push(`${rel}/`);
3944
+ entry = candidate;
3945
+ break;
3946
+ }
3947
+ if (!entry)
3948
+ continue;
3949
+ if (current.index < current.entries.length)
3950
+ directories.push(current);
3951
+ const rel = current.prefix ? `${current.prefix}/${entry.name}` : entry.name;
3952
+ if (entry.type === 'directory') {
3953
+ out.push(`${rel}/`);
3954
+ directories.push({
3309
3955
  // oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: symlink targets optional
3310
- await walk(entry.target?.displayPath ?? join(dir, entry.name), rel, depth + 1);
3311
- }
3312
- else if (entry.type === 'file') {
3313
- out.push(rel);
3314
- }
3956
+ dir: entry.target?.displayPath ?? join(current.dir, entry.name),
3957
+ prefix: rel,
3958
+ index: 0,
3959
+ });
3315
3960
  }
3316
- };
3317
- await walk(root, '', 1);
3961
+ else if (entry.type === 'file') {
3962
+ out.push(rel);
3963
+ }
3964
+ }
3318
3965
  return out;
3319
3966
  }
3320
3967
  /** One attached file's contribution is capped so an absent-minded `@` of a