@deepseek-harness-tui/dsh-tui 0.7.3 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +15 -7
  2. package/bin/dsh-tui.js +19 -3
  3. package/cordis.patch.yml +43 -11
  4. package/cordis.yml +20 -1
  5. package/ecosystem-spec/registry/contracts/decision-events-v1alpha1.json +13 -0
  6. package/ecosystem-spec/registry/host-descriptor.tui.example.json +56 -0
  7. package/ecosystem-spec/registry/permissions-0.1.json +13 -0
  8. package/ecosystem-spec/registry/registry-0.15.json +57 -0
  9. package/ecosystem-spec/schemas/conformance-claim.schema.json +15 -0
  10. package/ecosystem-spec/schemas/effect-ledger-record.schema.json +16 -0
  11. package/ecosystem-spec/schemas/host-descriptor.schema.json +26 -0
  12. package/lib/types/cc/figures.d.ts +4 -0
  13. package/lib/types/cc/figures.d.ts.map +1 -1
  14. package/lib/types/cc/figures.js +4 -0
  15. package/lib/types/commands.d.ts.map +1 -1
  16. package/lib/types/commands.js +4 -1
  17. package/lib/types/components/ExtensionDialog.d.ts +40 -0
  18. package/lib/types/components/ExtensionDialog.d.ts.map +1 -0
  19. package/lib/types/components/ExtensionDialog.js +210 -0
  20. package/lib/types/components/HelpMenu.js +1 -1
  21. package/lib/types/components/LoadedContextPanel.d.ts +5 -10
  22. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  23. package/lib/types/components/LoadedContextPanel.js +7 -18
  24. package/lib/types/components/LogoV2.d.ts.map +1 -1
  25. package/lib/types/components/LogoV2.js +1 -2
  26. package/lib/types/components/MessageList.d.ts +3 -1
  27. package/lib/types/components/MessageList.d.ts.map +1 -1
  28. package/lib/types/components/MessageList.js +29 -5
  29. package/lib/types/components/PluginSceneBoundary.d.ts +31 -0
  30. package/lib/types/components/PluginSceneBoundary.d.ts.map +1 -0
  31. package/lib/types/components/PluginSceneBoundary.js +29 -0
  32. package/lib/types/components/RewindPicker.d.ts +13 -1
  33. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  34. package/lib/types/components/RewindPicker.js +26 -2
  35. package/lib/types/components/SkillsPicker.d.ts +18 -0
  36. package/lib/types/components/SkillsPicker.d.ts.map +1 -0
  37. package/lib/types/components/SkillsPicker.js +50 -0
  38. package/lib/types/components/SplitDiffView.d.ts +28 -0
  39. package/lib/types/components/SplitDiffView.d.ts.map +1 -0
  40. package/lib/types/components/SplitDiffView.js +417 -0
  41. package/lib/types/components/ToolUseLoader.d.ts +8 -3
  42. package/lib/types/components/ToolUseLoader.d.ts.map +1 -1
  43. package/lib/types/components/ToolUseLoader.js +43 -9
  44. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +3 -1
  45. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  46. package/lib/types/components/messages/AssistantToolUseMessage.js +17 -7
  47. package/lib/types/dsh-adapter/channel.d.ts +98 -7
  48. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  49. package/lib/types/dsh-adapter/channel.js +623 -26
  50. package/lib/types/dsh-adapter/command-attribution.d.ts +51 -0
  51. package/lib/types/dsh-adapter/command-attribution.d.ts.map +1 -0
  52. package/lib/types/dsh-adapter/command-attribution.js +93 -0
  53. package/lib/types/dsh-adapter/command-errors.d.ts +42 -0
  54. package/lib/types/dsh-adapter/command-errors.d.ts.map +1 -0
  55. package/lib/types/dsh-adapter/command-errors.js +63 -0
  56. package/lib/types/dsh-adapter/component-identity.d.ts +28 -0
  57. package/lib/types/dsh-adapter/component-identity.d.ts.map +1 -0
  58. package/lib/types/dsh-adapter/component-identity.js +108 -0
  59. package/lib/types/dsh-adapter/contract.d.ts +2 -2
  60. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  61. package/lib/types/dsh-adapter/contract.js +3 -2
  62. package/lib/types/dsh-adapter/decision-guard.d.ts +84 -0
  63. package/lib/types/dsh-adapter/decision-guard.d.ts.map +1 -0
  64. package/lib/types/dsh-adapter/decision-guard.js +354 -0
  65. package/lib/types/dsh-adapter/dialogs.d.ts +146 -0
  66. package/lib/types/dsh-adapter/dialogs.d.ts.map +1 -0
  67. package/lib/types/dsh-adapter/dialogs.js +262 -0
  68. package/lib/types/dsh-adapter/effect-ledger.d.ts +99 -0
  69. package/lib/types/dsh-adapter/effect-ledger.d.ts.map +1 -0
  70. package/lib/types/dsh-adapter/effect-ledger.js +231 -0
  71. package/lib/types/dsh-adapter/extension-events.d.ts +193 -0
  72. package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -0
  73. package/lib/types/dsh-adapter/extension-events.js +169 -0
  74. package/lib/types/dsh-adapter/extensions.d.ts +32 -0
  75. package/lib/types/dsh-adapter/extensions.d.ts.map +1 -0
  76. package/lib/types/dsh-adapter/extensions.js +42 -0
  77. package/lib/types/dsh-adapter/grants.d.ts +25 -0
  78. package/lib/types/dsh-adapter/grants.d.ts.map +1 -0
  79. package/lib/types/dsh-adapter/grants.js +184 -0
  80. package/lib/types/dsh-adapter/host-descriptor.d.ts +24 -0
  81. package/lib/types/dsh-adapter/host-descriptor.d.ts.map +1 -0
  82. package/lib/types/dsh-adapter/host-descriptor.js +146 -0
  83. package/lib/types/dsh-adapter/index.d.ts +4 -0
  84. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  85. package/lib/types/dsh-adapter/index.js +1 -0
  86. package/lib/types/dsh-adapter/message-observer.d.ts +178 -0
  87. package/lib/types/dsh-adapter/message-observer.d.ts.map +1 -0
  88. package/lib/types/dsh-adapter/message-observer.js +509 -0
  89. package/lib/types/dsh-adapter/plugin-host.d.ts +103 -0
  90. package/lib/types/dsh-adapter/plugin-host.d.ts.map +1 -0
  91. package/lib/types/dsh-adapter/plugin-host.js +270 -0
  92. package/lib/types/dsh-adapter/plugin-storage.d.ts +113 -0
  93. package/lib/types/dsh-adapter/plugin-storage.d.ts.map +1 -0
  94. package/lib/types/dsh-adapter/plugin-storage.js +385 -0
  95. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  96. package/lib/types/dsh-adapter/plugin.js +106 -1
  97. package/lib/types/dsh-adapter/plugins-info.d.ts +43 -0
  98. package/lib/types/dsh-adapter/plugins-info.d.ts.map +1 -0
  99. package/lib/types/dsh-adapter/plugins-info.js +276 -0
  100. package/lib/types/dsh-adapter/promptDebug.d.ts +10 -0
  101. package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -0
  102. package/lib/types/dsh-adapter/promptDebug.js +148 -0
  103. package/lib/types/dsh-adapter/renderers.d.ts +65 -0
  104. package/lib/types/dsh-adapter/renderers.d.ts.map +1 -0
  105. package/lib/types/dsh-adapter/renderers.js +138 -0
  106. package/lib/types/dsh-adapter/sanitize.d.ts +36 -0
  107. package/lib/types/dsh-adapter/sanitize.d.ts.map +1 -0
  108. package/lib/types/dsh-adapter/sanitize.js +65 -0
  109. package/lib/types/dsh-adapter/scenes.d.ts +81 -0
  110. package/lib/types/dsh-adapter/scenes.d.ts.map +1 -0
  111. package/lib/types/dsh-adapter/scenes.js +93 -0
  112. package/lib/types/dsh-adapter/settings-sections.d.ts +116 -0
  113. package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -0
  114. package/lib/types/dsh-adapter/settings-sections.js +63 -0
  115. package/lib/types/dsh-adapter/settingsEditor.d.ts +128 -0
  116. package/lib/types/dsh-adapter/settingsEditor.d.ts.map +1 -0
  117. package/lib/types/dsh-adapter/settingsEditor.js +244 -0
  118. package/lib/types/dsh-adapter/shortcuts.d.ts +109 -0
  119. package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -0
  120. package/lib/types/dsh-adapter/shortcuts.js +268 -0
  121. package/lib/types/dsh-adapter/status.d.ts +75 -0
  122. package/lib/types/dsh-adapter/status.d.ts.map +1 -0
  123. package/lib/types/dsh-adapter/status.js +168 -0
  124. package/lib/types/extensions.d.ts +8 -0
  125. package/lib/types/extensions.d.ts.map +1 -0
  126. package/lib/types/extensions.js +15 -0
  127. package/lib/types/i18n.d.ts +299 -26
  128. package/lib/types/i18n.d.ts.map +1 -1
  129. package/lib/types/i18n.js +89 -8
  130. package/lib/types/ink/ink.d.ts +11 -0
  131. package/lib/types/ink/ink.d.ts.map +1 -1
  132. package/lib/types/ink/ink.js +33 -0
  133. package/lib/types/ink/log-update.d.ts.map +1 -1
  134. package/lib/types/ink/log-update.js +101 -51
  135. package/lib/types/ink/output.d.ts +7 -0
  136. package/lib/types/ink/output.d.ts.map +1 -1
  137. package/lib/types/ink/output.js +125 -4
  138. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  139. package/lib/types/ink/render-node-to-output.js +14 -6
  140. package/lib/types/ink/terminal.d.ts.map +1 -1
  141. package/lib/types/ink/terminal.js +18 -6
  142. package/lib/types/jsx-runtime.d.ts +2 -0
  143. package/lib/types/jsx-runtime.d.ts.map +1 -0
  144. package/lib/types/jsx-runtime.js +13 -0
  145. package/lib/types/modelRoute.d.ts +10 -2
  146. package/lib/types/modelRoute.d.ts.map +1 -1
  147. package/lib/types/modelRoute.js +33 -4
  148. package/lib/types/plugin-host.d.ts +13 -0
  149. package/lib/types/plugin-host.d.ts.map +1 -0
  150. package/lib/types/plugin-host.js +17 -0
  151. package/lib/types/plugin-spec/negotiate.d.ts +11 -0
  152. package/lib/types/plugin-spec/negotiate.d.ts.map +1 -0
  153. package/lib/types/plugin-spec/negotiate.js +97 -0
  154. package/lib/types/plugin-spec/permission-scope.d.ts +32 -0
  155. package/lib/types/plugin-spec/permission-scope.d.ts.map +1 -0
  156. package/lib/types/plugin-spec/permission-scope.js +90 -0
  157. package/lib/types/plugin-spec/registry.d.ts +23 -0
  158. package/lib/types/plugin-spec/registry.d.ts.map +1 -0
  159. package/lib/types/plugin-spec/registry.js +157 -0
  160. package/lib/types/plugin-spec/schema-check.d.ts +17 -0
  161. package/lib/types/plugin-spec/schema-check.d.ts.map +1 -0
  162. package/lib/types/plugin-spec/schema-check.js +165 -0
  163. package/lib/types/plugin-spec/tui-extension.d.ts +16 -0
  164. package/lib/types/plugin-spec/tui-extension.d.ts.map +1 -0
  165. package/lib/types/plugin-spec/tui-extension.js +77 -0
  166. package/lib/types/plugin-spec/types.d.ts +114 -0
  167. package/lib/types/plugin-spec/types.d.ts.map +1 -0
  168. package/lib/types/plugin-spec/types.js +10 -0
  169. package/lib/types/plugin-spec/validate.d.ts +21 -0
  170. package/lib/types/plugin-spec/validate.d.ts.map +1 -0
  171. package/lib/types/plugin-spec/validate.js +119 -0
  172. package/lib/types/scenes.d.ts +3 -0
  173. package/lib/types/scenes.d.ts.map +1 -0
  174. package/lib/types/scenes.js +4 -0
  175. package/lib/types/screens/Chat.d.ts +14 -1
  176. package/lib/types/screens/Chat.d.ts.map +1 -1
  177. package/lib/types/screens/Chat.js +247 -48
  178. package/lib/types/screens/Settings.d.ts +20 -0
  179. package/lib/types/screens/Settings.d.ts.map +1 -0
  180. package/lib/types/screens/Settings.js +333 -0
  181. package/lib/types/screens/StatusLine.js +1 -1
  182. package/lib/types/settings-sections.d.ts +3 -0
  183. package/lib/types/settings-sections.d.ts.map +1 -0
  184. package/lib/types/settings-sections.js +4 -0
  185. package/lib/types/theme.d.ts +17 -0
  186. package/lib/types/theme.d.ts.map +1 -1
  187. package/lib/types/theme.js +51 -0
  188. package/lib/types/update.d.ts +20 -0
  189. package/lib/types/update.d.ts.map +1 -1
  190. package/lib/types/update.js +54 -7
  191. package/lib/types/utils/loaded-context.d.ts +8 -3
  192. package/lib/types/utils/loaded-context.d.ts.map +1 -1
  193. package/lib/types/utils/loaded-context.js +39 -3
  194. package/lib/types/utils/modifiers.d.ts.map +1 -1
  195. package/lib/types/utils/modifiers.js +3 -2
  196. package/lib/types/working-activity.d.ts +12 -0
  197. package/lib/types/working-activity.d.ts.map +1 -1
  198. package/lib/types/working-activity.js +13 -0
  199. package/node_modules/@dsh-std/command/LICENSE +21 -0
  200. package/node_modules/@dsh-std/command/README.md +26 -0
  201. package/node_modules/@dsh-std/command/README.zh.md +28 -0
  202. package/node_modules/@dsh-std/command/lib/index.d.ts +144 -0
  203. package/node_modules/@dsh-std/command/lib/index.d.ts.map +1 -0
  204. package/node_modules/@dsh-std/command/lib/index.js +252 -0
  205. package/node_modules/@dsh-std/command/lib/index.js.map +1 -0
  206. package/node_modules/@dsh-std/command/package.json +22 -0
  207. package/node_modules/@dsh-std/connection/LICENSE +21 -0
  208. package/node_modules/@dsh-std/connection/README.md +42 -0
  209. package/node_modules/@dsh-std/connection/README.zh.md +42 -0
  210. package/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
  211. package/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
  212. package/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
  213. package/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
  214. package/node_modules/@dsh-std/connection/lib/index.js +158 -0
  215. package/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
  216. package/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
  217. package/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
  218. package/node_modules/@dsh-std/connection/lib/memory.js +257 -0
  219. package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
  220. package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
  221. package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
  222. package/node_modules/@dsh-std/connection/package.json +20 -0
  223. package/node_modules/@dsh-std/core/LICENSE +21 -0
  224. package/node_modules/@dsh-std/core/README.md +13 -0
  225. package/node_modules/@dsh-std/core/README.zh.md +13 -0
  226. package/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  227. package/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  228. package/node_modules/@dsh-std/core/lib/index.js +424 -0
  229. package/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  230. package/node_modules/@dsh-std/core/package.json +21 -0
  231. package/node_modules/@dsh-std/manifest/LICENSE +21 -0
  232. package/node_modules/@dsh-std/manifest/README.md +5 -0
  233. package/node_modules/@dsh-std/manifest/README.zh.md +5 -0
  234. package/node_modules/@dsh-std/manifest/lib/index.d.ts +182 -0
  235. package/node_modules/@dsh-std/manifest/lib/index.d.ts.map +1 -0
  236. package/node_modules/@dsh-std/manifest/lib/index.js +625 -0
  237. package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -0
  238. package/node_modules/@dsh-std/manifest/package.json +20 -0
  239. package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -0
  240. package/node_modules/@dsh-std/messages/LICENSE +21 -0
  241. package/node_modules/@dsh-std/messages/README.md +5 -0
  242. package/node_modules/@dsh-std/messages/README.zh.md +7 -0
  243. package/node_modules/@dsh-std/messages/lib/index.d.ts +50 -0
  244. package/node_modules/@dsh-std/messages/lib/index.d.ts.map +1 -0
  245. package/node_modules/@dsh-std/messages/lib/index.js +151 -0
  246. package/node_modules/@dsh-std/messages/lib/index.js.map +1 -0
  247. package/node_modules/@dsh-std/messages/package.json +20 -0
  248. package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -0
  249. package/node_modules/@dsh-std/presentation/LICENSE +21 -0
  250. package/node_modules/@dsh-std/presentation/README.md +26 -0
  251. package/node_modules/@dsh-std/presentation/README.zh.md +28 -0
  252. package/node_modules/@dsh-std/presentation/lib/index.d.ts +219 -0
  253. package/node_modules/@dsh-std/presentation/lib/index.d.ts.map +1 -0
  254. package/node_modules/@dsh-std/presentation/lib/index.js +547 -0
  255. package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -0
  256. package/node_modules/@dsh-std/presentation/lib/interaction.d.ts +2 -0
  257. package/node_modules/@dsh-std/presentation/lib/interaction.js +2 -0
  258. package/node_modules/@dsh-std/presentation/lib/operations.d.ts +2 -0
  259. package/node_modules/@dsh-std/presentation/lib/operations.js +2 -0
  260. package/node_modules/@dsh-std/presentation/package.json +21 -0
  261. package/node_modules/@dsh-std/storage/LICENSE +21 -0
  262. package/node_modules/@dsh-std/storage/README.md +5 -0
  263. package/node_modules/@dsh-std/storage/README.zh.md +7 -0
  264. package/node_modules/@dsh-std/storage/lib/index.d.ts +48 -0
  265. package/node_modules/@dsh-std/storage/lib/index.d.ts.map +1 -0
  266. package/node_modules/@dsh-std/storage/lib/index.js +104 -0
  267. package/node_modules/@dsh-std/storage/lib/index.js.map +1 -0
  268. package/node_modules/@dsh-std/storage/package.json +20 -0
  269. package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -0
  270. package/package.json +118 -67
  271. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -0
  272. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -0
  273. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -0
  274. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
  275. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
  276. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
  277. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
  278. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js +158 -0
  279. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
  280. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
  281. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
  282. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js +257 -0
  283. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
  284. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
  285. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
  286. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -0
  287. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -0
  288. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -0
  289. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -0
  290. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  291. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  292. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js +424 -0
  293. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  294. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -0
  295. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -0
  296. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -0
  297. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -0
  298. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.d.ts +182 -0
  299. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.d.ts.map +1 -0
  300. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js +625 -0
  301. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -0
  302. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -0
  303. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -0
  304. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -0
  305. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +26 -0
  306. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +28 -0
  307. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.d.ts +219 -0
  308. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.d.ts.map +1 -0
  309. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js +547 -0
  310. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -0
  311. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/interaction.d.ts +2 -0
  312. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/interaction.js +2 -0
  313. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/operations.d.ts +2 -0
  314. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/operations.js +2 -0
  315. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +21 -0
  316. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -0
  317. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -0
  318. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -0
  319. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  320. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  321. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js +424 -0
  322. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  323. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -0
  324. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -0
  325. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -0
  326. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -0
  327. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  328. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  329. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js +424 -0
  330. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  331. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -0
  332. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -0
  333. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -0
  334. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -0
  335. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  336. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  337. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js +424 -0
  338. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  339. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -0
  340. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -0
  341. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -0
  342. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -0
  343. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
  344. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
  345. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
  346. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
  347. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js +158 -0
  348. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
  349. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
  350. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
  351. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js +257 -0
  352. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
  353. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
  354. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
  355. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -0
  356. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -0
  357. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -0
  358. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -0
  359. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  360. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  361. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js +424 -0
  362. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  363. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -0
  364. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -0
  365. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -0
  366. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -0
  367. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
  368. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
  369. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js +424 -0
  370. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -0
  371. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -0
@@ -27,6 +27,102 @@ import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
27
27
  import { ActivityTracker } from 'dsh-working-activity/status';
28
28
  import { attachSessionToWorkspace } from './workspace.js';
29
29
  import { createLocalWorkspaceRuntime } from './workspaces.js';
30
+ import { dispatchTuiDecision, dispatchTuiNotification, normalizeCancelDecision } from './extension-events.js';
31
+ import { installDecisionGuard } from './decision-guard.js';
32
+ import { commandOwner } from './command-attribution.js';
33
+ import { readGrantStore } from './grants.js';
34
+ import { hasCommandErrorCode, mapCommandError } from './command-errors.js';
35
+ import { pluginsInfoLines } from './plugins-info.js';
36
+ import { cleanRenderText, cleanScalarText } from './sanitize.js';
37
+ /** `tui/input` return normalization: transform/handled/cancel or no opinion.
38
+ * A blank `{ text }` rewrite is NOT a decision — it is logged and the chain
39
+ * continues so a later veto listener still runs. */
40
+ function normalizeInputDecision(result, warn) {
41
+ if (result === undefined || result === null || result === false)
42
+ return undefined;
43
+ if (typeof result !== 'object') {
44
+ warn(`a non-object (${typeof result})`);
45
+ return undefined;
46
+ }
47
+ const record = result;
48
+ if (record.cancel === true) {
49
+ const reason = cleanScalarText(record.reason, NOTICE_CELLS);
50
+ return { cancel: true, ...(reason === '' ? {} : { reason }) };
51
+ }
52
+ if (record.handled === true) {
53
+ const notice = cleanScalarText(record.notice, NOTICE_CELLS);
54
+ return { handled: true, ...(notice === '' ? {} : { notice }) };
55
+ }
56
+ if (typeof record.text === 'string') {
57
+ if (record.text.trim() === '') {
58
+ warn('a blank {text} rewrite');
59
+ return undefined;
60
+ }
61
+ return { text: record.text };
62
+ }
63
+ warn('an unrecognized decision shape');
64
+ return undefined;
65
+ }
66
+ /** `tui/rewind-prompt` return normalization: cancel/modes or no opinion.
67
+ * Modes are COPIED with only validated, sanitized scalar fields — the raw
68
+ * plugin object must never reach the render path (a `description: {}` would
69
+ * crash ListItem's `.replace`, and control chars would corrupt the pane).
70
+ * Notices/reasons are toast-bound plugin text: sanitized too (see
71
+ * ./sanitize.js — the one implementation of the render-path contract). */
72
+ function normalizeRewindPromptDecision(result, warn) {
73
+ if (result === undefined || result === null || result === false)
74
+ return undefined;
75
+ if (typeof result !== 'object') {
76
+ warn(`a non-object (${typeof result})`);
77
+ return undefined;
78
+ }
79
+ const record = result;
80
+ if (record.cancel === true) {
81
+ const reason = cleanScalarText(record.reason, NOTICE_CELLS);
82
+ return { cancel: true, ...(reason === '' ? {} : { reason }) };
83
+ }
84
+ if (Array.isArray(record.modes)) {
85
+ const modes = [];
86
+ for (const raw of record.modes) {
87
+ if (modes.length >= 8)
88
+ break;
89
+ if (raw === null || typeof raw !== 'object')
90
+ continue;
91
+ const candidate = raw;
92
+ if (typeof candidate.id !== 'string' || candidate.id.trim() === '')
93
+ continue;
94
+ const label = typeof candidate.label === 'string' ? cleanRenderText(candidate.label, 120) : '';
95
+ if (label === '')
96
+ continue;
97
+ const description = typeof candidate.description === 'string' && candidate.description.trim() !== ''
98
+ ? cleanRenderText(candidate.description, 400)
99
+ : undefined;
100
+ modes.push({ id: candidate.id, label, ...(description === undefined ? {} : { description }) });
101
+ }
102
+ if (modes.length === 0) {
103
+ warn('an empty or invalid {modes} list');
104
+ return undefined;
105
+ }
106
+ return { modes };
107
+ }
108
+ warn('an unrecognized decision shape');
109
+ return undefined;
110
+ }
111
+ /** Toast-bound plugin text (veto reasons, handled notices, rewind summaries)
112
+ * is render-path data too: same sanitization, toast-width cap. */
113
+ const NOTICE_CELLS = 200;
114
+ /** `tui/rewind-done` return normalization: the first non-empty STRING is the
115
+ * summary; anything else is not a decision. */
116
+ function normalizeRewindDoneSummary(result, warn) {
117
+ if (result === undefined || result === null || result === false)
118
+ return undefined;
119
+ if (typeof result === 'string') {
120
+ const summary = cleanRenderText(result, NOTICE_CELLS);
121
+ return summary === '' ? undefined : summary;
122
+ }
123
+ warn('a non-string summary');
124
+ return undefined;
125
+ }
30
126
  /**
31
127
  * Delay before re-reading a skill catalog that reported an incomplete
32
128
  * observation (a provider whose directory watcher is still warming).
@@ -288,18 +384,50 @@ async function waitForTurnEnd(session, fromSeq, timeoutMs) {
288
384
  export function createChannel(ctx, initialAgent, options) {
289
385
  let agent = initialAgent;
290
386
  let currentHandle = options.handle;
387
+ // D-7 backstop: the extensions row installs the decision-subscription
388
+ // gate, but the channel IS the dispatch path — a stale patch without that
389
+ // row (or a bare embed mounting neither) would otherwise leave tui/input
390
+ // & friends subscribable by default, silently voiding the default-deny
391
+ // posture. Idempotent per cordis root, so the full-patch path installs
392
+ // exactly once whichever side runs first. One store instance serves both
393
+ // the gate and the invoke checkpoint below.
394
+ // Keep a private fallback for bare embedders, but resolve the host-owned
395
+ // store on every operation so a plugin-host row mounted later (or a custom
396
+ // live GrantStore) is not shadowed by an early snapshot.
397
+ const fallbackGrantStore = readGrantStore();
398
+ const currentGrantStore = () => ctx.get('tuiPluginHost')?.grants ?? fallbackGrantStore;
399
+ installDecisionGuard(ctx, currentGrantStore());
291
400
  // The DSH slash-command registry (optional service): /plan, /goal and
292
401
  // friends register here; the TUI merges their descriptors into the slash
293
402
  // menu and dispatches through `execute` (which logs the paired
294
403
  // command/run + command/done records). Absent the service, only the
295
404
  // built-in local commands exist.
296
405
  const commandService = ctx.get('commands');
406
+ // messages.observe broker (optional service, C-042): mounted by the
407
+ // dsh-tui-plugin-host row; absent the row, publish is a no-op and nothing
408
+ // else changes (soft degradation, #183).
409
+ const messageObserver = ctx.get('tuiMessageObserver');
297
410
  // Workspace registry runtime (optional service, issue #183): mounted by
298
411
  // the bundle patch's dsh-tui-workspaces row; absent the row (stale patch
299
412
  // or a bare embedder), degrade to the local-only runtime. plugin.ts owns
300
413
  // the degraded-boot warning for profile launches.
301
414
  const workspaceService = ctx.get('tuiWorkspaces') ?? createLocalWorkspaceRuntime();
302
415
  const commandTrees = ctx.get('tuiCommandTrees');
416
+ // The `/settings` screen reads its host on EVERY render, so the host must
417
+ // be a stable object: a fresh literal per call would re-fire the screen's
418
+ // host-keyed effects endlessly (render → new host → effect → state →
419
+ // render). The underlying services are fixed for the channel's lifetime,
420
+ // so compute once and cache.
421
+ let settingsHostCache;
422
+ let settingsHostResolved = false;
423
+ // Plugin scene runtime (optional service, same degradation rule as
424
+ // tuiWorkspaces/tuiCommandTrees): mounted by the bundle patch's
425
+ // dsh-tui-scenes row; absent the row, `pluginScene` simply stays undefined.
426
+ const sceneRuntime = ctx.get('tuiScenes');
427
+ // Custom-entry text renderers (optional service, dsh-tui-extensions row):
428
+ // absent the row, unknown plugin event types stay invisible in the
429
+ // transcript, exactly as before the seam existed.
430
+ const rendererRuntime = ctx.get('tuiRenderers');
303
431
  // Shift+Tab session-mode cycle: cordis.yml `modes` wins; absent/empty/
304
432
  // atom-less → the built-in default/plan/full cycle (sessionModes.ts).
305
433
  const { modes: sessionModes, dropped: droppedModeIds } = resolveSessionModes(options.modes);
@@ -394,6 +522,142 @@ export function createChannel(ctx, initialAgent, options) {
394
522
  state.notify(t('send-failed', { err: message }), { color: 'error' });
395
523
  });
396
524
  };
525
+ /**
526
+ * RFC 0005 D-8: a flow parked on a plugin decision must be user-observable.
527
+ * Decisions normally resolve in milliseconds, so the notice only fires
528
+ * once the wait crosses a threshold — a slow plugin (e.g. one showing a
529
+ * managed dialog) then explains the pause instead of looking like the TUI
530
+ * ate the input.
531
+ */
532
+ const DECISION_PENDING_MS = 400;
533
+ const withDecisionPending = (name, pending) => {
534
+ let dismiss;
535
+ const timer = setTimeout(() => {
536
+ // Sticky (timeoutMs 0), D-8: the indicator must cover the WHOLE wait —
537
+ // an auto-expiring notice would vanish after ~4s while the decision,
538
+ // the delivery and every queued FIFO task behind them stay parked,
539
+ // leaving the user with no sign the flow is still waiting. It comes
540
+ // down only when the decision settles (finally below); a decision
541
+ // that never settles keeps its indicator up, which is the truthful
542
+ // state.
543
+ dismiss = state.notify(t('ext-decision-pending', { event: name }), { timeoutMs: 0 });
544
+ }, DECISION_PENDING_MS);
545
+ // Both exits are covered: a fast decision clears the timer before it
546
+ // fires; a slow one dismisses the indicator it raised.
547
+ return pending.finally(() => {
548
+ clearTimeout(timer);
549
+ dismiss?.();
550
+ });
551
+ };
552
+ /**
553
+ * The `tui/input` decision event (pi's `input` seam): the FIRST plugin
554
+ * returning a valid decision wins — transform the text, mark it handled,
555
+ * or cancel it. No listeners (or only crashing/malformed ones) means
556
+ * delivery proceeds unchanged, so a broken plugin can never wedge the
557
+ * input path — and can never skip a later veto listener either
558
+ * (dispatchTuiDecision isolates crashes and normalizes returns per
559
+ * listener instead of bailing on the first object).
560
+ *
561
+ * Decision AND delivery enter one FIFO chain in submission order: a slow
562
+ * listener on A parks A's delivery AND any later submissions behind it —
563
+ * without the chain, B's decision could resolve first and the model would
564
+ * receive B before A. Each submission binds its origin agent AT ENQUEUE,
565
+ * so a session switch landing before OR during its decision drops the
566
+ * stale text with a notice instead of sending the old conversation's
567
+ * words to the new session.
568
+ */
569
+ let inputChain = Promise.resolve();
570
+ const runUserTextDecision = async (text, placement, originAgent, originAgentId) => {
571
+ // Stale detection compares the AGENT REFERENCE, not the id: session ids
572
+ // are reusable (A → /new → /resume A lands back on the same id with a
573
+ // fresh agent), so an id check has an ABA hole. Both origin values are
574
+ // ENQUEUE-time captures (see dispatchUserText): a decision parked behind
575
+ // a slow predecessor must still be judged against the session its text
576
+ // was typed in, not whichever session is live when it finally runs.
577
+ const decision = await withDecisionPending('tui/input', dispatchTuiDecision(ctx, 'tui/input', {
578
+ text,
579
+ delivery: placement === 'steer' ? 'steer' : 'followup',
580
+ sessionId: originAgentId,
581
+ cwd: state.cwd,
582
+ }, normalizeInputDecision));
583
+ if (decision !== undefined) {
584
+ // Both intercepts toast — a bare {cancel}/{handled} must not make the
585
+ // typed line vanish silently (the host-localized fallback mirrors the
586
+ // other decision events' ext-action-cancelled handling).
587
+ if ('cancel' in decision) {
588
+ state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
589
+ return;
590
+ }
591
+ if ('handled' in decision) {
592
+ state.notify(decision.notice ?? t('ext-action-handled'), { timeoutMs: 4000 });
593
+ return;
594
+ }
595
+ text = decision.text.trim();
596
+ }
597
+ if (agent !== originAgent) {
598
+ state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
599
+ return;
600
+ }
601
+ deliverUserText(text, placement);
602
+ };
603
+ const dispatchUserText = (text, placement) => {
604
+ // D-6: bind the submission to the session it was typed in AT ENQUEUE
605
+ // TIME. The FIFO chain may park this task behind a slow predecessor
606
+ // while the user /new's away — capturing the agent at run time would
607
+ // adopt the NEW session as this text's origin and deliver the old
608
+ // conversation's words into it.
609
+ const originAgent = agent;
610
+ const originAgentId = state.agentId;
611
+ inputChain = inputChain.then(() => runUserTextDecision(text, placement, originAgent, originAgentId)).catch((error) => {
612
+ // The chain must survive a failed decision: log, then continue with
613
+ // the next queued submission.
614
+ ctx.logger.warn('dsh-tui: tui/input dispatch failed: %o', error);
615
+ });
616
+ };
617
+ /**
618
+ * The `tui/session-switch` decision event (pi's `session_before_switch`),
619
+ * fired before `/new` or `/resume` replaces the live session (rewind has
620
+ * its own prompt event). The first answering plugin may veto the switch;
621
+ * the reason is toasted here so the fallback string stays host-localized.
622
+ */
623
+ const sessionSwitchVetoed = async (kind, targetSessionId) => {
624
+ // D-6 stale detection captures the AGENT REFERENCE (session ids are
625
+ // reusable — ABA): a slow decision must not let an older /resume roll
626
+ // over a newer session the user already switched to mid-await.
627
+ const originAgent = agent;
628
+ const decision = await withDecisionPending('tui/session-switch', dispatchTuiDecision(ctx, 'tui/session-switch', {
629
+ kind,
630
+ ...(targetSessionId === undefined ? {} : { targetSessionId }),
631
+ sessionId: state.agentId,
632
+ cwd: state.cwd,
633
+ }, normalizeCancelDecision));
634
+ if (agent !== originAgent) {
635
+ // The world changed while the decision parked: drop the pending
636
+ // switch instead of replacing the user's newer session.
637
+ state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
638
+ return true;
639
+ }
640
+ if (decision !== undefined) {
641
+ state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
642
+ return true;
643
+ }
644
+ return false;
645
+ };
646
+ /** Fire-and-forget `tui/session-switched` (parallel): per-session plugin
647
+ * state rebinds here. Listener failures are logged, never propagated —
648
+ * the switch itself already succeeded. */
649
+ const notifySessionSwitched = (kind, sessionId, previousSessionId) => {
650
+ try {
651
+ void dispatchTuiNotification(ctx, 'tui/session-switched', { kind, sessionId, previousSessionId, cwd: state.cwd }).catch((error) => {
652
+ ctx.logger.warn('dsh-tui: tui/session-switched listener failed: %o', error);
653
+ });
654
+ }
655
+ catch (error) {
656
+ // A bare embedder's context may lack the event bus entirely; the
657
+ // switch itself already succeeded, so this stays a log line.
658
+ ctx.logger.warn('dsh-tui: tui/session-switched dispatch failed: %o', error);
659
+ }
660
+ };
397
661
  /** Monotonic token: only the latest `interruptAndDeliver` re-queues, so a
398
662
  * second interrupt while the abort settles cannot double-deliver. */
399
663
  let interruptSeq = 0;
@@ -508,6 +772,45 @@ export function createChannel(ctx, initialAgent, options) {
508
772
  const executeRegistryCommand = async (name, rawInput) => {
509
773
  if (!commandService)
510
774
  return undefined;
775
+ // Resolve the exact definition that execute() will select for this agent.
776
+ // Same names may exist in distinct agent scopes, so a name-only lookup can
777
+ // apply another scope's owner policy.
778
+ const definition = commandService.find(agent, name);
779
+ const owner = commandOwner(ctx, definition);
780
+ // The root checkpoint covers host/direct registrations. A built-in name
781
+ // is not necessarily a namespaced contribution id, so use a deterministic
782
+ // host scope for that case; legacy unscoped denies still conservatively
783
+ // revoke every valid command scope.
784
+ const rootScope = owner?.commandId
785
+ ?? (/^[a-z][a-z0-9]*(?:[.-][a-z0-9][a-z0-9-]*)+$/u.test(name)
786
+ ? name
787
+ : `dsh-tui.${name.toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '') || 'command'}`);
788
+ if (!currentGrantStore().allows({ componentId: 'root' }, 'commands.invoke', rootScope)) {
789
+ ctx.logger.warn('dsh-tui: registry command invocation denied (commands.invoke revoked for "root" in the grants file)');
790
+ ctx.get('tuiEffectLedger')?.record({
791
+ operation: 'bind',
792
+ resource: { kind: 'permission', id: `root:commands.invoke:${rootScope}` },
793
+ result: 'failed',
794
+ errorCode: 'PERMISSION_NOT_GRANTED',
795
+ }, ctx);
796
+ return t('command-invoke-denied');
797
+ }
798
+ // Per-owner gate (C-041): a command REGISTERED BY A PLUGIN through the
799
+ // plugin-host row's mediated registerCommand (see command-attribution.js)
800
+ // is additionally gated on the OWNER's grant, so a denies entry for the
801
+ // plugin closes the host-mediated invocation of ITS commands.
802
+ // Unattributed host/direct registrations remain inside the documented
803
+ // trusted-in-process boundary and have no plugin grant to evaluate.
804
+ if (owner !== undefined && !currentGrantStore().allows({ componentId: owner.componentId, activationId: owner.activationId }, 'commands.invoke', owner.commandId)) {
805
+ ctx.logger.warn(`dsh-tui: registry command "/${name}" invocation denied — owner Component "${owner.componentId}" lost commands.invoke for "${owner.commandId}"`);
806
+ ctx.get('tuiEffectLedger')?.record({
807
+ operation: 'bind',
808
+ resource: { kind: 'permission', id: `${owner.componentId}:commands.invoke:${owner.commandId}` },
809
+ result: 'failed',
810
+ errorCode: 'PERMISSION_NOT_GRANTED',
811
+ }, ctx);
812
+ return t('command-invoke-denied-owner', { name, owner: owner.componentId });
813
+ }
511
814
  try {
512
815
  const execution = await commandService.execute(agent, `/${name}${rawInput}`, new AbortController().signal);
513
816
  // `undefined` = not registered; a handler error surfaces as its
@@ -641,6 +944,7 @@ export function createChannel(ctx, initialAgent, options) {
641
944
  modeIndex: 0,
642
945
  workingActivity: undefined,
643
946
  activityFrames: options.activityFrames,
947
+ diffLayout: options.diffLayout ?? 'auto',
644
948
  activityEnabled: options.activity !== false,
645
949
  contextBarEnabled: options.contextBar !== false,
646
950
  agentPreset: options.agentPreset,
@@ -766,7 +1070,7 @@ export function createChannel(ctx, initialAgent, options) {
766
1070
  // The current session is being used — move it to the MRU front
767
1071
  // (/resume sorts by last-used).
768
1072
  touchSession(state.agentId);
769
- deliverUserText(trimmed, 'followup');
1073
+ void dispatchUserText(trimmed, 'followup');
770
1074
  },
771
1075
  /** Steer a message into the RUNNING turn (Codex/pi semantics): it is
772
1076
  * injected at the next step boundary of the current turn and the agent
@@ -776,11 +1080,13 @@ export function createChannel(ctx, initialAgent, options) {
776
1080
  if (!trimmed)
777
1081
  return;
778
1082
  touchSession(state.agentId);
779
- // Official dsh-agent rc.6: steer() is synchronous void — the message
780
- // enters the next-step inbox. A rejected step leaves it parked for the
781
- // next wake; the inbox events below retire the preview (claimed →
782
- // turn boundary, discarded → cancel).
783
- deliverUserText(trimmed, 'steer');
1083
+ // Same tui/input decision pass as submit; the delivery re-validates
1084
+ // the live agent after the await. Official dsh-agent rc.6: steer() is
1085
+ // synchronous void — the message enters the next-step inbox; a
1086
+ // rejected step leaves it parked for the next wake, and the inbox
1087
+ // events retire the preview (claimed → turn boundary, discarded →
1088
+ // cancel).
1089
+ void dispatchUserText(trimmed, 'steer');
784
1090
  },
785
1091
  /** Pull a pending message back out of the inbox (Alt+Up): it returns to
786
1092
  * the input for editing instead of being delivered. */
@@ -826,7 +1132,10 @@ export function createChannel(ctx, initialAgent, options) {
826
1132
  return;
827
1133
  for (const text of queued) {
828
1134
  touchSession(state.agentId);
829
- deliverUserText(text, 'followup');
1135
+ // Same tui/input decision pass as a typed submit: Ctrl+Enter must
1136
+ // not bypass a plugin's cancel/transform policy, and re-queued
1137
+ // texts keep submission order through the one FIFO chain.
1138
+ dispatchUserText(text, 'followup');
830
1139
  }
831
1140
  };
832
1141
  if (typeof whenIdle === 'function') {
@@ -839,7 +1148,43 @@ export function createChannel(ctx, initialAgent, options) {
839
1148
  }
840
1149
  return queued.length;
841
1150
  },
842
- async rewindTo(row) {
1151
+ /**
1152
+ * The `tui/rewind-prompt` decision event (pi's `session_before_fork`):
1153
+ * fired when the rewind picker confirms a message, before any fork
1154
+ * work. The first answering plugin may cancel the rewind (the picker
1155
+ * stays open; the reason is toasted here so the UI string stays
1156
+ * host-localized when absent) or offer extra modes rendered in the
1157
+ * confirm pane. Returns 'cancel', the modes, or null for "no opinion".
1158
+ */
1159
+ async promptRewind(row) {
1160
+ if (row.seq === undefined)
1161
+ return null;
1162
+ // D-6, same as the other decision points: a slow /new or /resume can
1163
+ // replace the agent while this decision parks. Without the identity
1164
+ // check the picker would go on to show the OLD session's row in the
1165
+ // confirm pane and rewindTo would cut the NEW session at the old
1166
+ // seq — a wrong rewind or a fork failure. Compare agent REFERENCES
1167
+ // (session ids are reusable — ABA) and stale-cancel.
1168
+ const originAgent = agent;
1169
+ const decision = await withDecisionPending('tui/rewind-prompt', dispatchTuiDecision(ctx, 'tui/rewind-prompt', {
1170
+ text: row.text,
1171
+ seq: row.seq,
1172
+ sessionId: state.agentId,
1173
+ cwd: state.cwd,
1174
+ }, normalizeRewindPromptDecision));
1175
+ if (agent !== originAgent) {
1176
+ state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
1177
+ return 'cancel';
1178
+ }
1179
+ if (decision === undefined)
1180
+ return null;
1181
+ if ('cancel' in decision) {
1182
+ state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
1183
+ return 'cancel';
1184
+ }
1185
+ return { modes: decision.modes };
1186
+ },
1187
+ async rewindTo(row, mode = null) {
843
1188
  if (row.seq === undefined)
844
1189
  return null;
845
1190
  const sessions = ctx.get('sessions');
@@ -972,6 +1317,7 @@ export function createChannel(ctx, initialAgent, options) {
972
1317
  renderEvent(event);
973
1318
  // Rebind subscriptions to the new agent, then free the old one.
974
1319
  const oldHandle = currentHandle;
1320
+ const sourceSessionId = String(agent.session.id);
975
1321
  agent = handle.agent;
976
1322
  currentHandle = handle;
977
1323
  bindAgent();
@@ -982,6 +1328,43 @@ export function createChannel(ctx, initialAgent, options) {
982
1328
  touchSession(childId);
983
1329
  state.emit();
984
1330
  void oldHandle?.dispose().catch(() => { });
1331
+ // Decision-event pair around the completed rewind: `tui/rewind-done`
1332
+ // (the first non-empty string is toasted as the post-rewind summary,
1333
+ // e.g. a plugin reporting restored files) and the generic
1334
+ // `tui/session-switched` notification. Listener failures are logged,
1335
+ // never surfaced — the rewind itself already succeeded.
1336
+ //
1337
+ // rewind-done is a post-hoc summary, NOT a gate, so it is dispatched
1338
+ // DECOUPLED from the return value: the picker is already closed and
1339
+ // PromptInput is live — awaiting a slow listener here would delay the
1340
+ // picked text's return to the draft, letting its late arrival
1341
+ // overwrite whatever the user typed meanwhile, and a listener that
1342
+ // never settles would park tui/session-switched forever. The summary
1343
+ // toasts whenever it lands.
1344
+ try {
1345
+ void dispatchTuiDecision(ctx, 'tui/rewind-done', {
1346
+ text: row.text,
1347
+ mode,
1348
+ boundarySeq: boundary,
1349
+ sourceSessionId,
1350
+ childSessionId: String(childId),
1351
+ sessionId: String(childId),
1352
+ cwd: state.cwd,
1353
+ }, normalizeRewindDoneSummary)
1354
+ .then(summary => {
1355
+ if (summary !== undefined)
1356
+ state.notify(summary, { timeoutMs: 6000 });
1357
+ })
1358
+ .catch((error) => {
1359
+ ctx.logger.warn('dsh-tui: tui/rewind-done dispatch failed: %o', error);
1360
+ });
1361
+ }
1362
+ catch (error) {
1363
+ // A bare embedder's context may lack the event bus entirely; the
1364
+ // rewind itself already succeeded, so this stays a log line.
1365
+ ctx.logger.warn('dsh-tui: tui/rewind-done dispatch failed: %o', error);
1366
+ }
1367
+ notifySessionSwitched('rewind', String(childId), sourceSessionId);
985
1368
  return row.text;
986
1369
  },
987
1370
  async resumeTo(sessionId) {
@@ -997,6 +1380,11 @@ export function createChannel(ctx, initialAgent, options) {
997
1380
  state.notify(t('resume-unavailable'), { color: 'error' });
998
1381
  return false;
999
1382
  }
1383
+ // Plugin veto point (tui/session-switch): before any read of the
1384
+ // target — a veto leaves the live session and its transcript
1385
+ // untouched.
1386
+ if (await sessionSwitchVetoed('resume', sessionId))
1387
+ return false;
1000
1388
  let handle;
1001
1389
  // Compat boundary: register vouched-for legacy event types (e.g.
1002
1390
  // activity/status from pre-#143 logs) in every reachable dsh-session
@@ -1099,6 +1487,7 @@ export function createChannel(ctx, initialAgent, options) {
1099
1487
  settleStreaming();
1100
1488
  // Rebind subscriptions to the resumed agent, then free the old one.
1101
1489
  const oldHandle = currentHandle;
1490
+ const previousSessionId = String(agent.session.id);
1102
1491
  agent = handle.agent;
1103
1492
  currentHandle = handle;
1104
1493
  bindAgent();
@@ -1112,6 +1501,7 @@ export function createChannel(ctx, initialAgent, options) {
1112
1501
  state.emit();
1113
1502
  void oldHandle?.dispose().catch(() => { });
1114
1503
  clearStagedImages();
1504
+ notifySessionSwitched('resume', sessionId, previousSessionId);
1115
1505
  return true;
1116
1506
  },
1117
1507
  async newSession() {
@@ -1131,6 +1521,10 @@ export function createChannel(ctx, initialAgent, options) {
1131
1521
  });
1132
1522
  return false;
1133
1523
  }
1524
+ // Plugin veto point (tui/session-switch): no side effects have
1525
+ // happened yet — the session id below is not even allocated.
1526
+ if (await sessionSwitchVetoed('new'))
1527
+ return false;
1134
1528
  const sessionId = SessionId(randomUUID());
1135
1529
  let handle;
1136
1530
  // A fresh session composes the caller's DEFAULT preset: the cordis.yml
@@ -1222,6 +1616,7 @@ export function createChannel(ctx, initialAgent, options) {
1222
1616
  tools: 0,
1223
1617
  };
1224
1618
  const oldHandle = currentHandle;
1619
+ const previousSessionId = String(agent.session.id);
1225
1620
  agent = handle.agent;
1226
1621
  currentHandle = handle;
1227
1622
  bindAgent();
@@ -1233,6 +1628,7 @@ export function createChannel(ctx, initialAgent, options) {
1233
1628
  touchSession(handle.agent.id);
1234
1629
  void oldHandle?.dispose().catch(() => { });
1235
1630
  clearStagedImages();
1631
+ notifySessionSwitched('new', String(handle.agent.id), previousSessionId);
1236
1632
  return true;
1237
1633
  },
1238
1634
  listWorkspaces() {
@@ -1444,13 +1840,30 @@ export function createChannel(ctx, initialAgent, options) {
1444
1840
  };
1445
1841
  state.notifications.push(item);
1446
1842
  state.emit();
1447
- setTimeout(() => {
1843
+ const remove = () => {
1448
1844
  const index = state.notifications.indexOf(item);
1449
1845
  if (index >= 0) {
1450
1846
  state.notifications.splice(index, 1);
1451
1847
  state.emit();
1452
1848
  }
1453
- }, item.timeoutMs);
1849
+ };
1850
+ // timeoutMs 0 = sticky: no expiry timer, the dismiss handle is the
1851
+ // only way out (a decision-parked indicator must outlive the wait it
1852
+ // describes — auto-expiring it would hide a still-parked flow, D-8).
1853
+ const expire = item.timeoutMs > 0 ? setTimeout(remove, item.timeoutMs) : undefined;
1854
+ // Early-dismiss handle for flows that know their notice went stale
1855
+ // (e.g. a decision-parked indicator whose decision just landed).
1856
+ return () => {
1857
+ if (expire !== undefined)
1858
+ clearTimeout(expire);
1859
+ remove();
1860
+ };
1861
+ },
1862
+ setDiffLayout(layout) {
1863
+ if (layout === state.diffLayout)
1864
+ return;
1865
+ state.diffLayout = layout;
1866
+ state.emit();
1454
1867
  },
1455
1868
  setActivityFrames(name) {
1456
1869
  if (!isPresetName(name)) {
@@ -1561,12 +1974,84 @@ export function createChannel(ctx, initialAgent, options) {
1561
1974
  return Promise.all(providers.map(provider => llm.listModels(provider.id).catch(() => [])))
1562
1975
  .then(lists => lists.flat());
1563
1976
  },
1977
+ async listSkills() {
1978
+ // snapshot() over list(): only a COMPLETE observation is authoritative
1979
+ // (same contract as the skill-command merge above) — a partial catalog
1980
+ // must surface as "failed", not as a misleading near-empty picker.
1981
+ const registry = skillRegistryFor(agent);
1982
+ if (registry === undefined)
1983
+ return [];
1984
+ try {
1985
+ const observation = await registry.snapshot(skillViewOptions(agent));
1986
+ if (!observation.complete)
1987
+ return undefined;
1988
+ return observation.skills.map(skill => ({
1989
+ name: skill.name,
1990
+ description: skill.description,
1991
+ userInvocable: isUserInvocable(skill),
1992
+ source: skill.source,
1993
+ }));
1994
+ }
1995
+ catch {
1996
+ return undefined;
1997
+ }
1998
+ },
1564
1999
  async describeCredential(ref) {
1565
2000
  const credentials = ctx.get('credentials');
1566
2001
  if (!credentials)
1567
2002
  return undefined;
1568
2003
  return credentials.describe(ref);
1569
2004
  },
2005
+ settingsHost() {
2006
+ if (settingsHostResolved)
2007
+ return settingsHostCache;
2008
+ settingsHostResolved = true;
2009
+ // The `/settings` screen's runtime surface, over the same dsh-base
2010
+ // seams the `/provider` wizard uses: settings (namespace descriptors +
2011
+ // revision-fenced mutate) and credentials (secret writes). Structurally
2012
+ // typed like the other optional seams in this file.
2013
+ const settings = ctx.get('settings');
2014
+ const credentials = ctx.get('credentials');
2015
+ if (!settings)
2016
+ return undefined;
2017
+ settingsHostCache = {
2018
+ listNamespaces() {
2019
+ // redactSecrets: the screen never renders a secret literal — secret
2020
+ // fields are write-only controls over the credentials seam.
2021
+ return settings.describe({ redactSecrets: true }).map(descriptor => ({
2022
+ ns: descriptor.ns,
2023
+ revision: descriptor.revision,
2024
+ applies: descriptor.applies,
2025
+ value: descriptor.value,
2026
+ user: descriptor.user,
2027
+ }));
2028
+ },
2029
+ write(ns, ops, expectedRevision) {
2030
+ return settings.mutate(ns, ops, expectedRevision);
2031
+ },
2032
+ async credentialConfigured(ref) {
2033
+ // The environment shadows the store (providerSetup.envShadows), so
2034
+ // an env-provided key counts as configured.
2035
+ if (process.env[ref] !== undefined)
2036
+ return true;
2037
+ return credentials !== undefined && (await credentials.resolve(ref)) !== undefined;
2038
+ },
2039
+ async writeCredential(ref, value) {
2040
+ if (!credentials)
2041
+ throw new Error('credentials service unavailable');
2042
+ await credentials.set(ref, value);
2043
+ },
2044
+ };
2045
+ return settingsHostCache;
2046
+ },
2047
+ settingsSections() {
2048
+ const sections = ctx.get('tuiSettingsSections');
2049
+ return sections?.list() ?? [];
2050
+ },
2051
+ subscribeSettingsSections(listener) {
2052
+ const sections = ctx.get('tuiSettingsSections');
2053
+ return sections?.subscribe(listener) ?? (() => { });
2054
+ },
1570
2055
  providerSetup() {
1571
2056
  // The `/provider` wizard's runtime surface, over the dsh-base seams:
1572
2057
  // settings (profile persistence), credentials (key storage) and the
@@ -1749,20 +2234,60 @@ export function createChannel(ctx, initialAgent, options) {
1749
2234
  state.notify(t('compact-while-working'), { color: 'warning' });
1750
2235
  return;
1751
2236
  }
1752
- const signal = new AbortController().signal;
1753
- state.notify(t('compact-working'));
1754
- void compactService
1755
- .compactNow(agent, signal)
1756
- .then((result) => {
1757
- state.notify(result ? t('compact-done') : t('compact-nothing'));
1758
- })
1759
- .catch((error) => {
1760
- state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
1761
- });
2237
+ // Plugin veto point (tui/compact): the first answering plugin may
2238
+ // cancel the compaction before anything runs.
2239
+ const originAgentId = state.agentId;
2240
+ // Compare the AGENT REFERENCE after the await, not the id — session
2241
+ // ids are reusable (A → /new → /resume A returns the same id on a new
2242
+ // agent), so an id comparison has an ABA hole that would hand the new
2243
+ // agent to the OLD scope's compaction service.
2244
+ const originAgent = agent;
2245
+ void (async () => {
2246
+ const decision = await withDecisionPending('tui/compact', dispatchTuiDecision(ctx, 'tui/compact', {
2247
+ sessionId: originAgentId,
2248
+ cwd: state.cwd,
2249
+ }, normalizeCancelDecision));
2250
+ if (decision !== undefined) {
2251
+ state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
2252
+ return;
2253
+ }
2254
+ // Stale-drop (same rule as tui/input): the await parked us while the
2255
+ // user switched sessions — `compactService` was resolved through the
2256
+ // OLD agent's scope chain, and the mutable `agent` now points at the
2257
+ // new session. Running now would hand the new agent to the old
2258
+ // service (or call into an unloaded one).
2259
+ if (agent !== originAgent) {
2260
+ state.notify(t('ext-compact-stale'), { color: 'warning', timeoutMs: 4000 });
2261
+ return;
2262
+ }
2263
+ if (state.working) {
2264
+ // The await above gave a queued turn time to start; compacting
2265
+ // mid-turn now would be the same race the check upfront avoided.
2266
+ state.notify(t('compact-while-working'), { color: 'warning' });
2267
+ return;
2268
+ }
2269
+ const signal = new AbortController().signal;
2270
+ state.notify(t('compact-working'));
2271
+ void compactService
2272
+ .compactNow(agent, signal)
2273
+ .then((result) => {
2274
+ state.notify(result ? t('compact-done') : t('compact-nothing'));
2275
+ })
2276
+ .catch((error) => {
2277
+ state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
2278
+ });
2279
+ })();
1762
2280
  },
1763
2281
  runExternalCommand(name, rawInput) {
1764
2282
  return executeRegistryCommand(name, rawInput);
1765
2283
  },
2284
+ pluginScene: sceneRuntime?.active,
2285
+ openPluginScene(id) {
2286
+ return sceneRuntime?.open(id) ?? false;
2287
+ },
2288
+ closePluginScene() {
2289
+ sceneRuntime?.close();
2290
+ },
1766
2291
  pushLocal(title, lines) {
1767
2292
  state.rows.push({ id: nextRowId++, kind: 'local', text: title });
1768
2293
  for (const line of lines) {
@@ -1920,8 +2445,23 @@ export function createChannel(ctx, initialAgent, options) {
1920
2445
  if (existsSync(LEGACY_DATA_DIR)) {
1921
2446
  lines.push(t('doctor-legacy-dir'));
1922
2447
  }
2448
+ // Plugin-spec diagnostics (v0.15): the runtime generation and the
2449
+ // vendored registry self-check, both soft-probed (#183 discipline).
2450
+ const pluginHost = ctx.get('tuiPluginHost');
2451
+ lines.push(t('doctor-plugin-generation', { id: pluginHost?.generationId ?? t('doctor-plugin-host-missing') }));
2452
+ const violations = pluginHost?.selfCheck();
2453
+ lines.push(t('doctor-plugin-registry', {
2454
+ state: violations === undefined ? t('doctor-plugin-host-missing') : violations.length === 0 ? '✓' : `✗ ${violations.length}`,
2455
+ }));
1923
2456
  return lines;
1924
2457
  },
2458
+ pluginsInfo(args) {
2459
+ const host = ctx.get('tuiPluginHost');
2460
+ return pluginsInfoLines(args, {
2461
+ grants: host?.grants ?? currentGrantStore(),
2462
+ host: host?.describe(),
2463
+ });
2464
+ },
1925
2465
  async listSubagents() {
1926
2466
  const subagents = ctx.get('subagents');
1927
2467
  if (!subagents)
@@ -1943,6 +2483,7 @@ export function createChannel(ctx, initialAgent, options) {
1943
2483
  },
1944
2484
  releaseContributions() {
1945
2485
  releaseSkillCommands();
2486
+ unsubscribeScenes?.();
1946
2487
  },
1947
2488
  traceEvents() {
1948
2489
  // Immutable per-append snapshot (dsh-session caches the frozen array);
@@ -2274,16 +2815,38 @@ export function createChannel(ctx, initialAgent, options) {
2274
2815
  },
2275
2816
  });
2276
2817
  skillCommands.set(name, { dispose, description });
2818
+ ctx.get('tuiEffectLedger')?.record({ operation: 'create', resource: { kind: 'command', id: name }, result: 'applied' }, ctx);
2277
2819
  }
2278
2820
  catch (error) {
2821
+ // C-041: a duplicate registration arrives as a plain-message Error
2822
+ // from dsh-commands; map it onto the contract code before handling
2823
+ // (the refusal path itself is unchanged).
2824
+ const mapped = mapCommandError(error);
2279
2825
  skillCommandsRefused.add(name);
2280
- ctx.logger.warn(`skill commands: "${name}" not registrable: %o`, error);
2826
+ ctx.logger.warn(`skill commands: "${name}" not registrable%s: %o`, hasCommandErrorCode(mapped, 'DUPLICATE_CONTRIBUTION_ID') ? ' (DUPLICATE_CONTRIBUTION_ID)' : '', mapped);
2827
+ ctx.get('tuiEffectLedger')?.record({
2828
+ operation: 'create',
2829
+ resource: { kind: 'command', id: name },
2830
+ result: 'failed',
2831
+ errorCode: hasCommandErrorCode(mapped, 'DUPLICATE_CONTRIBUTION_ID') ? 'DUPLICATE_CONTRIBUTION_ID' : 'COMMAND_FAILED',
2832
+ }, ctx);
2281
2833
  }
2282
2834
  }
2283
2835
  };
2284
2836
  ctx.on('skills/change', () => {
2285
2837
  void refreshSkillCommands();
2286
2838
  });
2839
+ /**
2840
+ * Mirror the scene runtime's active scene into channel state, so screens
2841
+ * swap to it through the ordinary version-bump re-render instead of a
2842
+ * second subscription channel.
2843
+ */
2844
+ const unsubscribeScenes = sceneRuntime?.subscribe(() => {
2845
+ if (state.pluginScene === sceneRuntime.active)
2846
+ return;
2847
+ state.pluginScene = sceneRuntime.active;
2848
+ state.emit();
2849
+ });
2287
2850
  /** See {@link Channel.releaseContributions}. */
2288
2851
  const releaseSkillCommands = () => {
2289
2852
  if (skillCommandsRetry !== undefined)
@@ -2614,12 +3177,18 @@ ${output}
2614
3177
  break;
2615
3178
  }
2616
3179
  case 'assistant/message': {
2617
- const row = ensureStreaming(event.seq);
2618
- row.time = event.time;
2619
3180
  const text = textOf(event.data.message.content);
2620
- if (text)
2621
- row.text = text;
2622
- row.streaming = false;
3181
+ // Reasoning/tool-only steps emit no text: creating an assistant row
3182
+ // anyway leaves an empty `●` bullet in the transcript. A pre-existing
3183
+ // streaming row always has text (ensureStreaming is only reached on
3184
+ // non-empty text deltas), so only create one when text arrives.
3185
+ const row = streaming ?? (text ? ensureStreaming(event.seq) : undefined);
3186
+ if (row !== undefined) {
3187
+ row.time = event.time;
3188
+ if (text)
3189
+ row.text = text;
3190
+ row.streaming = false;
3191
+ }
2623
3192
  streaming = undefined;
2624
3193
  if (reasoning !== undefined) {
2625
3194
  // Seal, don't fold: the per-step duration settles here, but the
@@ -2861,6 +3430,30 @@ ${output}
2861
3430
  text: t('agent-preset-switched', { preset: data.agentPreset ?? 'unknown' }),
2862
3431
  });
2863
3432
  nextRowId += 1;
3433
+ break;
3434
+ }
3435
+ // Custom plugin events (tuiRenderers seam): a registered renderer
3436
+ // maps the payload to text rows — title as a local row, body as
3437
+ // preview-clipped local-output rows, same shape pushLocal uses.
3438
+ // Runs on the live stream AND on replay (resume/rewind), so the
3439
+ // projection must stay total; the runtime isolates renderer
3440
+ // crashes per type.
3441
+ if (rendererRuntime !== undefined) {
3442
+ const rendered = rendererRuntime.render(event.type, event.data);
3443
+ if (rendered !== undefined) {
3444
+ if (rendered.title !== undefined && rendered.title !== '') {
3445
+ state.rows.push({ id: nextRowId, kind: 'local', text: rendered.title });
3446
+ nextRowId += 1;
3447
+ }
3448
+ for (const line of rendered.lines) {
3449
+ state.rows.push({
3450
+ id: nextRowId,
3451
+ kind: 'local-output',
3452
+ text: preview(String(line), LOCAL_OUTPUT_LIMIT),
3453
+ });
3454
+ nextRowId += 1;
3455
+ }
3456
+ }
2864
3457
  }
2865
3458
  break;
2866
3459
  }
@@ -2998,6 +3591,10 @@ ${output}
2998
3591
  ctx.on('session/event', (session, event) => {
2999
3592
  if (session !== agent.session)
3000
3593
  return;
3594
+ // Observation broker (C-042): maps user/message + assistant/message
3595
+ // into grant-gated envelopes; every other event type is a no-op, and
3596
+ // publish never throws into this arm.
3597
+ messageObserver?.publish(session, event);
3001
3598
  activityTracker.onSessionEvent(event);
3002
3599
  renderWorkingActivity();
3003
3600
  // Mode-affecting atoms fold into the Shift+Tab mode indicator the