@clawpump/claw-agent 0.1.5 → 0.1.6

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 (1212) hide show
  1. package/agent/.dockerignore +67 -0
  2. package/agent/.envrc +1 -1
  3. package/agent/.gitattributes +8 -0
  4. package/agent/AGENTS.md +216 -4
  5. package/agent/CONTRIBUTING.md +46 -8
  6. package/agent/Dockerfile +78 -35
  7. package/agent/MANIFEST.in +2 -0
  8. package/agent/README.md +12 -5
  9. package/agent/README.ur-pk.md +261 -0
  10. package/agent/README.zh-CN.md +11 -8
  11. package/agent/SECURITY.md +5 -4
  12. package/agent/acp_adapter/provenance.py +127 -0
  13. package/agent/acp_adapter/server.py +112 -5
  14. package/agent/acp_adapter/session.py +1 -6
  15. package/agent/acp_registry/agent.json +2 -2
  16. package/agent/agent/account_usage.py +313 -1
  17. package/agent/agent/agent_init.py +140 -37
  18. package/agent/agent/agent_runtime_helpers.py +342 -83
  19. package/agent/agent/anthropic_adapter.py +320 -33
  20. package/agent/agent/auxiliary_client.py +525 -105
  21. package/agent/agent/background_review.py +157 -19
  22. package/agent/agent/bedrock_adapter.py +71 -6
  23. package/agent/agent/billing_view.py +295 -0
  24. package/agent/agent/chat_completion_helpers.py +229 -4
  25. package/agent/agent/codex_responses_adapter.py +86 -10
  26. package/agent/agent/codex_runtime.py +153 -1
  27. package/agent/agent/coding_context.py +738 -0
  28. package/agent/agent/context_compressor.py +392 -44
  29. package/agent/agent/context_references.py +34 -1
  30. package/agent/agent/conversation_compression.py +159 -22
  31. package/agent/agent/conversation_loop.py +643 -908
  32. package/agent/agent/copilot_acp_client.py +4 -11
  33. package/agent/agent/credential_pool.py +5 -3
  34. package/agent/agent/credits_tracker.py +794 -0
  35. package/agent/agent/curator.py +91 -18
  36. package/agent/agent/curator_backup.py +26 -10
  37. package/agent/agent/display.py +42 -1
  38. package/agent/agent/error_classifier.py +52 -3
  39. package/agent/agent/errors.py +3 -0
  40. package/agent/agent/file_safety.py +0 -17
  41. package/agent/agent/gemini_native_adapter.py +31 -1
  42. package/agent/agent/i18n.py +48 -4
  43. package/agent/agent/image_gen_provider.py +74 -5
  44. package/agent/agent/image_routing.py +29 -0
  45. package/agent/agent/insights.py +8 -17
  46. package/agent/agent/lsp/install.py +3 -0
  47. package/agent/agent/memory_manager.py +326 -31
  48. package/agent/agent/message_content.py +50 -0
  49. package/agent/agent/model_metadata.py +214 -3
  50. package/agent/agent/moonshot_schema.py +8 -1
  51. package/agent/agent/onboarding.py +60 -0
  52. package/agent/agent/prompt_builder.py +327 -37
  53. package/agent/agent/redact.py +1 -0
  54. package/agent/agent/runtime_cwd.py +34 -5
  55. package/agent/agent/secret_scope.py +205 -0
  56. package/agent/agent/secret_sources/bitwarden.py +34 -2
  57. package/agent/agent/skill_commands.py +90 -1
  58. package/agent/agent/skill_preprocessing.py +1 -0
  59. package/agent/agent/skill_utils.py +209 -36
  60. package/agent/agent/ssl_guard.py +94 -0
  61. package/agent/agent/system_prompt.py +133 -5
  62. package/agent/agent/tool_executor.py +496 -70
  63. package/agent/agent/transports/anthropic.py +83 -21
  64. package/agent/agent/transports/chat_completions.py +94 -5
  65. package/agent/agent/transports/codex.py +67 -2
  66. package/agent/agent/transports/codex_app_server.py +1 -0
  67. package/agent/agent/transports/codex_app_server_session.py +30 -0
  68. package/agent/agent/transports/types.py +12 -0
  69. package/agent/agent/turn_context.py +408 -0
  70. package/agent/agent/turn_finalizer.py +428 -0
  71. package/agent/agent/turn_retry_state.py +68 -0
  72. package/agent/agent/usage_pricing.py +3 -0
  73. package/agent/apps/bootstrap-installer/package.json +6 -5
  74. package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
  75. package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
  76. package/agent/apps/bootstrap-installer/src/store.ts +3 -2
  77. package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
  78. package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
  79. package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
  80. package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
  81. package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
  82. package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
  83. package/agent/apps/desktop/DESIGN.md +167 -0
  84. package/agent/apps/desktop/README.md +20 -16
  85. package/agent/apps/desktop/assets/icon.icns +0 -0
  86. package/agent/apps/desktop/assets/icon.ico +0 -0
  87. package/agent/apps/desktop/assets/icon.png +0 -0
  88. package/agent/apps/desktop/electron/backend-env.cjs +112 -0
  89. package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
  90. package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
  91. package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
  92. package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
  93. package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
  94. package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
  95. package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
  96. package/agent/apps/desktop/electron/connection-config.cjs +288 -0
  97. package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
  98. package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
  99. package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
  100. package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
  101. package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
  102. package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
  103. package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
  104. package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
  105. package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
  106. package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
  107. package/agent/apps/desktop/electron/git-root.cjs +54 -0
  108. package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
  109. package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
  110. package/agent/apps/desktop/electron/hardening.cjs +123 -28
  111. package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
  112. package/agent/apps/desktop/electron/main.cjs +3121 -331
  113. package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
  114. package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
  115. package/agent/apps/desktop/electron/preload.cjs +52 -2
  116. package/agent/apps/desktop/electron/session-windows.cjs +124 -0
  117. package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
  118. package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
  119. package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
  120. package/agent/apps/desktop/electron/update-remote.cjs +56 -0
  121. package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
  122. package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
  123. package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
  124. package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
  125. package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
  126. package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
  127. package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
  128. package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
  129. package/agent/apps/desktop/eslint.config.mjs +0 -3
  130. package/agent/apps/desktop/index.html +27 -2
  131. package/agent/apps/desktop/package.json +31 -11
  132. package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
  133. package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
  134. package/agent/apps/desktop/public/nous-girl.jpg +0 -0
  135. package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
  136. package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
  137. package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
  138. package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
  139. package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
  140. package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
  141. package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
  142. package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
  143. package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
  144. package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
  145. package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
  146. package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
  147. package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
  148. package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
  149. package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
  150. package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  151. package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
  152. package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
  153. package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
  154. package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
  155. package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
  156. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
  157. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
  158. package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  159. package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
  160. package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
  161. package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
  162. package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
  163. package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
  164. package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
  165. package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  166. package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
  167. package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  168. package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
  169. package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
  170. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
  171. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
  172. package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
  173. package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
  174. package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
  175. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  176. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
  177. package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
  178. package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
  179. package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
  180. package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
  181. package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
  182. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
  183. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
  184. package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
  185. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  186. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  187. package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
  188. package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
  189. package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
  190. package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
  191. package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
  192. package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
  193. package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
  194. package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
  195. package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
  196. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  197. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
  198. package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
  199. package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
  200. package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
  201. package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  202. package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
  203. package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
  204. package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
  205. package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
  206. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  207. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
  208. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
  209. package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
  210. package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
  211. package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
  212. package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
  213. package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
  214. package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
  215. package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
  216. package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
  217. package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
  218. package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
  219. package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
  220. package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
  221. package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
  222. package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
  223. package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  224. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
  225. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
  226. package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  227. package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
  228. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
  229. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
  230. package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
  231. package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
  232. package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
  233. package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
  234. package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
  235. package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
  236. package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
  237. package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
  238. package/agent/apps/desktop/src/app/routes.ts +9 -0
  239. package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
  240. package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
  241. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  242. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
  243. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
  244. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
  245. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
  246. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
  247. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
  248. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
  249. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
  250. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
  251. package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
  252. package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
  253. package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
  254. package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
  255. package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
  256. package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
  257. package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
  258. package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
  259. package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
  260. package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
  261. package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
  262. package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
  263. package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
  264. package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
  265. package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
  266. package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
  267. package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
  268. package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
  269. package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
  270. package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
  271. package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
  272. package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
  273. package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
  274. package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
  275. package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
  276. package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
  277. package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
  278. package/agent/apps/desktop/src/app/settings/types.ts +9 -6
  279. package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
  280. package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
  281. package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
  282. package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
  283. package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
  284. package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
  285. package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
  286. package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
  287. package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
  288. package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
  289. package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
  290. package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
  291. package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
  292. package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
  293. package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
  294. package/agent/apps/desktop/src/app/types.ts +85 -0
  295. package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
  296. package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
  297. package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
  298. package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
  299. package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
  300. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
  301. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
  302. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  303. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  304. package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
  305. package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
  306. package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
  307. package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
  308. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
  309. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
  310. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
  311. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
  312. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
  313. package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
  314. package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  315. package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
  316. package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
  317. package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
  318. package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
  319. package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
  320. package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
  321. package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
  322. package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
  323. package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
  324. package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
  325. package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
  326. package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
  327. package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
  328. package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
  329. package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
  330. package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
  331. package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
  332. package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
  333. package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
  334. package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
  335. package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
  336. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
  337. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
  338. package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
  339. package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
  340. package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
  341. package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
  342. package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
  343. package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
  344. package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
  345. package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
  346. package/agent/apps/desktop/src/components/notifications.tsx +48 -27
  347. package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
  348. package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
  349. package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
  350. package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
  351. package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
  352. package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
  353. package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
  354. package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
  355. package/agent/apps/desktop/src/components/ui/control.ts +25 -0
  356. package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
  357. package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
  358. package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
  359. package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
  360. package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
  361. package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
  362. package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
  363. package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
  364. package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
  365. package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
  366. package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
  367. package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
  368. package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
  369. package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
  370. package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
  371. package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
  372. package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
  373. package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
  374. package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
  375. package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
  376. package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
  377. package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/agent/apps/desktop/src/global.d.ts +181 -4
  381. package/agent/apps/desktop/src/hermes.test.ts +60 -0
  382. package/agent/apps/desktop/src/hermes.ts +190 -13
  383. package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
  384. package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
  385. package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
  386. package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
  387. package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
  388. package/agent/apps/desktop/src/i18n/context.tsx +183 -0
  389. package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
  390. package/agent/apps/desktop/src/i18n/en.ts +1921 -0
  391. package/agent/apps/desktop/src/i18n/index.ts +20 -0
  392. package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
  393. package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
  394. package/agent/apps/desktop/src/i18n/languages.ts +86 -0
  395. package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
  396. package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
  397. package/agent/apps/desktop/src/i18n/types.ts +1559 -0
  398. package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
  399. package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
  400. package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
  401. package/agent/apps/desktop/src/lib/ansi.ts +186 -0
  402. package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
  403. package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
  404. package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
  405. package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
  406. package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
  407. package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
  408. package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
  409. package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
  410. package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
  411. package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
  412. package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
  413. package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
  414. package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
  415. package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
  416. package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
  417. package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
  418. package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
  419. package/agent/apps/desktop/src/lib/haptics.ts +17 -0
  420. package/agent/apps/desktop/src/lib/icons.ts +10 -2
  421. package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
  422. package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
  423. package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
  424. package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
  425. package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
  426. package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
  427. package/agent/apps/desktop/src/lib/media.ts +40 -1
  428. package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
  429. package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
  430. package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
  431. package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
  432. package/agent/apps/desktop/src/lib/query-client.ts +13 -0
  433. package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
  434. package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
  435. package/agent/apps/desktop/src/lib/session-export.ts +6 -3
  436. package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
  437. package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
  438. package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
  439. package/agent/apps/desktop/src/lib/session-search.ts +21 -0
  440. package/agent/apps/desktop/src/lib/session-source.ts +126 -0
  441. package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
  442. package/agent/apps/desktop/src/lib/storage.ts +35 -1
  443. package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
  444. package/agent/apps/desktop/src/lib/todos.ts +37 -0
  445. package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
  446. package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
  447. package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
  448. package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
  449. package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
  450. package/agent/apps/desktop/src/main.tsx +19 -19
  451. package/agent/apps/desktop/src/store/boot.ts +4 -3
  452. package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
  453. package/agent/apps/desktop/src/store/clarify.ts +50 -13
  454. package/agent/apps/desktop/src/store/command-palette.ts +20 -0
  455. package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
  456. package/agent/apps/desktop/src/store/compaction.ts +38 -0
  457. package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
  458. package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
  459. package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
  460. package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
  461. package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
  462. package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
  463. package/agent/apps/desktop/src/store/composer-status.ts +277 -0
  464. package/agent/apps/desktop/src/store/composer.test.ts +106 -0
  465. package/agent/apps/desktop/src/store/composer.ts +116 -0
  466. package/agent/apps/desktop/src/store/cron.ts +19 -0
  467. package/agent/apps/desktop/src/store/gateway.ts +280 -6
  468. package/agent/apps/desktop/src/store/keybinds.ts +143 -0
  469. package/agent/apps/desktop/src/store/layout.ts +107 -9
  470. package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
  471. package/agent/apps/desktop/src/store/model-presets.ts +86 -0
  472. package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
  473. package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
  474. package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
  475. package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
  476. package/agent/apps/desktop/src/store/notifications.ts +10 -7
  477. package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
  478. package/agent/apps/desktop/src/store/onboarding.ts +268 -38
  479. package/agent/apps/desktop/src/store/preview.ts +10 -1
  480. package/agent/apps/desktop/src/store/profile.test.ts +89 -0
  481. package/agent/apps/desktop/src/store/profile.ts +395 -0
  482. package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
  483. package/agent/apps/desktop/src/store/prompts.ts +117 -0
  484. package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
  485. package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
  486. package/agent/apps/desktop/src/store/session-sync.ts +25 -0
  487. package/agent/apps/desktop/src/store/session.test.ts +268 -2
  488. package/agent/apps/desktop/src/store/session.ts +392 -18
  489. package/agent/apps/desktop/src/store/subagents.ts +3 -0
  490. package/agent/apps/desktop/src/store/system-actions.ts +48 -0
  491. package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
  492. package/agent/apps/desktop/src/store/todos.test.ts +47 -0
  493. package/agent/apps/desktop/src/store/todos.ts +64 -0
  494. package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
  495. package/agent/apps/desktop/src/store/translucency.ts +38 -0
  496. package/agent/apps/desktop/src/store/updates.test.ts +187 -2
  497. package/agent/apps/desktop/src/store/updates.ts +268 -18
  498. package/agent/apps/desktop/src/store/windows.test.ts +143 -0
  499. package/agent/apps/desktop/src/store/windows.ts +115 -0
  500. package/agent/apps/desktop/src/styles.css +510 -119
  501. package/agent/apps/desktop/src/themes/color.ts +142 -0
  502. package/agent/apps/desktop/src/themes/context.tsx +128 -75
  503. package/agent/apps/desktop/src/themes/install.test.ts +119 -0
  504. package/agent/apps/desktop/src/themes/install.ts +95 -0
  505. package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
  506. package/agent/apps/desktop/src/themes/presets.ts +13 -4
  507. package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
  508. package/agent/apps/desktop/src/themes/types.ts +35 -0
  509. package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
  510. package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
  511. package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
  512. package/agent/apps/desktop/src/themes/vscode.ts +343 -0
  513. package/agent/apps/desktop/src/types/hermes.ts +138 -1
  514. package/agent/apps/desktop/tsconfig.json +2 -2
  515. package/agent/apps/desktop/vite.config.ts +18 -0
  516. package/agent/apps/shared/package.json +1 -1
  517. package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
  518. package/agent/apps/shared/tsconfig.json +2 -2
  519. package/agent/cli-config.yaml.example +78 -1
  520. package/agent/cli.py +2177 -3162
  521. package/agent/cron/blueprint_catalog.py +713 -0
  522. package/agent/cron/jobs.py +226 -110
  523. package/agent/cron/scheduler.py +468 -193
  524. package/agent/cron/scheduler_provider.py +177 -0
  525. package/agent/cron/scripts/__init__.py +1 -0
  526. package/agent/cron/scripts/classify_items.py +226 -0
  527. package/agent/cron/suggestion_catalog.py +154 -0
  528. package/agent/cron/suggestions.py +257 -0
  529. package/agent/docs/chronos-managed-cron-contract.md +196 -0
  530. package/agent/docs/design/profile-builder.md +146 -0
  531. package/agent/docs/middleware/README.md +260 -0
  532. package/agent/docs/observability/README.md +316 -0
  533. package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
  534. package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
  535. package/agent/docs/relay-connector-contract.md +285 -0
  536. package/agent/gateway/authz_mixin.py +536 -0
  537. package/agent/gateway/channel_directory.py +65 -3
  538. package/agent/gateway/config.py +222 -12
  539. package/agent/gateway/display_config.py +10 -0
  540. package/agent/gateway/hooks.py +17 -0
  541. package/agent/gateway/kanban_watchers.py +1146 -0
  542. package/agent/gateway/message_timestamps.py +166 -0
  543. package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
  544. package/agent/gateway/platforms/api_server.py +216 -38
  545. package/agent/gateway/platforms/base.py +210 -58
  546. package/agent/gateway/platforms/email.py +122 -12
  547. package/agent/gateway/platforms/feishu.py +80 -11
  548. package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
  549. package/agent/gateway/platforms/matrix.py +1498 -297
  550. package/agent/gateway/platforms/qqbot/adapter.py +6 -0
  551. package/agent/gateway/platforms/signal.py +8 -0
  552. package/agent/gateway/platforms/slack.py +308 -12
  553. package/agent/gateway/platforms/telegram.py +831 -24
  554. package/agent/gateway/platforms/webhook.py +109 -21
  555. package/agent/gateway/platforms/weixin.py +113 -2
  556. package/agent/gateway/platforms/whatsapp.py +94 -288
  557. package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
  558. package/agent/gateway/platforms/whatsapp_common.py +367 -0
  559. package/agent/gateway/platforms/yuanbao.py +608 -191
  560. package/agent/gateway/platforms/yuanbao_proto.py +232 -23
  561. package/agent/gateway/relay/__init__.py +375 -0
  562. package/agent/gateway/relay/adapter.py +222 -0
  563. package/agent/gateway/relay/auth.py +168 -0
  564. package/agent/gateway/relay/descriptor.py +118 -0
  565. package/agent/gateway/relay/transport.py +101 -0
  566. package/agent/gateway/relay/ws_transport.py +327 -0
  567. package/agent/gateway/response_filters.py +53 -0
  568. package/agent/gateway/rich_sent_store.py +80 -0
  569. package/agent/gateway/run.py +2940 -5001
  570. package/agent/gateway/session.py +109 -8
  571. package/agent/gateway/session_context.py +22 -4
  572. package/agent/gateway/slash_commands.py +3854 -0
  573. package/agent/gateway/status.py +141 -21
  574. package/agent/gateway/stream_consumer.py +288 -31
  575. package/agent/hermes-already-has-routines.md +1 -1
  576. package/agent/hermes_cli/__init__.py +62 -17
  577. package/agent/hermes_cli/_parser.py +30 -0
  578. package/agent/hermes_cli/_subprocess_compat.py +61 -0
  579. package/agent/hermes_cli/active_sessions.py +320 -0
  580. package/agent/hermes_cli/auth.py +707 -59
  581. package/agent/hermes_cli/auth_commands.py +39 -22
  582. package/agent/hermes_cli/backup.py +109 -7
  583. package/agent/hermes_cli/banner.py +88 -0
  584. package/agent/hermes_cli/blueprint_cmd.py +318 -0
  585. package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
  586. package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
  587. package/agent/hermes_cli/commands.py +215 -91
  588. package/agent/hermes_cli/config.py +967 -130
  589. package/agent/hermes_cli/container_boot.py +76 -11
  590. package/agent/hermes_cli/cron.py +5 -11
  591. package/agent/hermes_cli/curator.py +21 -0
  592. package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
  593. package/agent/hermes_cli/dashboard_auth/base.py +62 -0
  594. package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
  595. package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
  596. package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
  597. package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
  598. package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
  599. package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
  600. package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
  601. package/agent/hermes_cli/dashboard_register.py +427 -0
  602. package/agent/hermes_cli/debug.py +155 -50
  603. package/agent/hermes_cli/doctor.py +255 -14
  604. package/agent/hermes_cli/dump.py +60 -6
  605. package/agent/hermes_cli/env_loader.py +33 -0
  606. package/agent/hermes_cli/gateway.py +755 -103
  607. package/agent/hermes_cli/gateway_enroll.py +250 -0
  608. package/agent/hermes_cli/gateway_windows.py +254 -11
  609. package/agent/hermes_cli/gui_uninstall.py +285 -0
  610. package/agent/hermes_cli/inventory.py +105 -4
  611. package/agent/hermes_cli/kanban.py +58 -71
  612. package/agent/hermes_cli/kanban_db.py +391 -14
  613. package/agent/hermes_cli/kanban_decompose.py +2 -2
  614. package/agent/hermes_cli/kanban_specify.py +3 -1
  615. package/agent/hermes_cli/logs.py +2 -0
  616. package/agent/hermes_cli/main.py +2889 -5287
  617. package/agent/hermes_cli/managed_scope.py +214 -0
  618. package/agent/hermes_cli/managed_uv.py +254 -0
  619. package/agent/hermes_cli/mcp_catalog.py +6 -3
  620. package/agent/hermes_cli/mcp_config.py +145 -21
  621. package/agent/hermes_cli/mcp_security.py +96 -0
  622. package/agent/hermes_cli/mcp_startup.py +32 -3
  623. package/agent/hermes_cli/memory_providers.py +149 -0
  624. package/agent/hermes_cli/memory_setup.py +97 -42
  625. package/agent/hermes_cli/middleware.py +313 -0
  626. package/agent/hermes_cli/model_catalog.py +31 -0
  627. package/agent/hermes_cli/model_cost_guard.py +134 -0
  628. package/agent/hermes_cli/model_normalize.py +2 -1
  629. package/agent/hermes_cli/model_setup_flows.py +2759 -0
  630. package/agent/hermes_cli/model_switch.py +242 -27
  631. package/agent/hermes_cli/models.py +284 -44
  632. package/agent/hermes_cli/nous_account.py +33 -6
  633. package/agent/hermes_cli/nous_billing.py +406 -0
  634. package/agent/hermes_cli/nous_subscription.py +202 -5
  635. package/agent/hermes_cli/platforms.py +1 -0
  636. package/agent/hermes_cli/plugins.py +218 -18
  637. package/agent/hermes_cli/plugins_cmd.py +249 -105
  638. package/agent/hermes_cli/portal_cli.py +56 -16
  639. package/agent/hermes_cli/profile_distribution.py +6 -1
  640. package/agent/hermes_cli/profiles.py +283 -32
  641. package/agent/hermes_cli/provider_catalog.py +170 -0
  642. package/agent/hermes_cli/providers.py +4 -1
  643. package/agent/hermes_cli/pty_bridge.py +53 -4
  644. package/agent/hermes_cli/runtime_provider.py +216 -34
  645. package/agent/hermes_cli/secret_prompt.py +4 -4
  646. package/agent/hermes_cli/secrets_cli.py +24 -0
  647. package/agent/hermes_cli/send_cmd.py +28 -2
  648. package/agent/hermes_cli/service_manager.py +166 -19
  649. package/agent/hermes_cli/session_listing.py +97 -0
  650. package/agent/hermes_cli/setup.py +158 -94
  651. package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
  652. package/agent/hermes_cli/skills_config.py +8 -2
  653. package/agent/hermes_cli/skills_hub.py +149 -7
  654. package/agent/hermes_cli/status.py +2 -2
  655. package/agent/hermes_cli/subcommands/__init__.py +18 -0
  656. package/agent/hermes_cli/subcommands/_shared.py +29 -0
  657. package/agent/hermes_cli/subcommands/acp.py +52 -0
  658. package/agent/hermes_cli/subcommands/auth.py +109 -0
  659. package/agent/hermes_cli/subcommands/backup.py +38 -0
  660. package/agent/hermes_cli/subcommands/claw.py +92 -0
  661. package/agent/hermes_cli/subcommands/config.py +49 -0
  662. package/agent/hermes_cli/subcommands/cron.py +163 -0
  663. package/agent/hermes_cli/subcommands/dashboard.py +143 -0
  664. package/agent/hermes_cli/subcommands/debug.py +77 -0
  665. package/agent/hermes_cli/subcommands/doctor.py +35 -0
  666. package/agent/hermes_cli/subcommands/dump.py +28 -0
  667. package/agent/hermes_cli/subcommands/gateway.py +332 -0
  668. package/agent/hermes_cli/subcommands/gui.py +63 -0
  669. package/agent/hermes_cli/subcommands/hooks.py +77 -0
  670. package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
  671. package/agent/hermes_cli/subcommands/insights.py +25 -0
  672. package/agent/hermes_cli/subcommands/login.py +78 -0
  673. package/agent/hermes_cli/subcommands/logout.py +28 -0
  674. package/agent/hermes_cli/subcommands/logs.py +78 -0
  675. package/agent/hermes_cli/subcommands/mcp.py +108 -0
  676. package/agent/hermes_cli/subcommands/memory.py +53 -0
  677. package/agent/hermes_cli/subcommands/model.py +72 -0
  678. package/agent/hermes_cli/subcommands/pairing.py +36 -0
  679. package/agent/hermes_cli/subcommands/plugins.py +94 -0
  680. package/agent/hermes_cli/subcommands/postinstall.py +23 -0
  681. package/agent/hermes_cli/subcommands/profile.py +203 -0
  682. package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
  683. package/agent/hermes_cli/subcommands/security.py +62 -0
  684. package/agent/hermes_cli/subcommands/setup.py +58 -0
  685. package/agent/hermes_cli/subcommands/skills.py +298 -0
  686. package/agent/hermes_cli/subcommands/slack.py +60 -0
  687. package/agent/hermes_cli/subcommands/status.py +28 -0
  688. package/agent/hermes_cli/subcommands/tools.py +95 -0
  689. package/agent/hermes_cli/subcommands/uninstall.py +41 -0
  690. package/agent/hermes_cli/subcommands/update.py +70 -0
  691. package/agent/hermes_cli/subcommands/version.py +18 -0
  692. package/agent/hermes_cli/subcommands/webhook.py +76 -0
  693. package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
  694. package/agent/hermes_cli/suggestions_cmd.py +153 -0
  695. package/agent/hermes_cli/telegram_managed_bot.py +358 -0
  696. package/agent/hermes_cli/tips.py +3 -4
  697. package/agent/hermes_cli/tools_config.py +155 -28
  698. package/agent/hermes_cli/uninstall.py +231 -35
  699. package/agent/hermes_cli/web_server.py +6188 -975
  700. package/agent/hermes_cli/win_pty_bridge.py +179 -0
  701. package/agent/hermes_cli/write_approval_commands.py +209 -0
  702. package/agent/hermes_constants.py +164 -33
  703. package/agent/hermes_logging.py +74 -2
  704. package/agent/hermes_state.py +919 -106
  705. package/agent/hermes_time.py +20 -0
  706. package/agent/locales/af.yaml +23 -0
  707. package/agent/locales/de.yaml +23 -0
  708. package/agent/locales/en.yaml +20 -0
  709. package/agent/locales/es.yaml +23 -0
  710. package/agent/locales/fr.yaml +23 -0
  711. package/agent/locales/ga.yaml +23 -0
  712. package/agent/locales/hu.yaml +23 -0
  713. package/agent/locales/it.yaml +23 -0
  714. package/agent/locales/ja.yaml +23 -0
  715. package/agent/locales/ko.yaml +23 -0
  716. package/agent/locales/pt.yaml +23 -0
  717. package/agent/locales/ru.yaml +23 -0
  718. package/agent/locales/tr.yaml +23 -0
  719. package/agent/locales/uk.yaml +23 -0
  720. package/agent/locales/zh-hant.yaml +23 -0
  721. package/agent/locales/zh.yaml +23 -0
  722. package/agent/model_tools.py +204 -40
  723. package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
  724. package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
  725. package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
  726. package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
  727. package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
  728. package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
  729. package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
  730. package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
  731. package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
  732. package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
  733. package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
  734. package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
  735. package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
  736. package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
  737. package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
  738. package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
  739. package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
  740. package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
  741. package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
  742. package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
  743. package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
  744. package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
  745. package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
  746. package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
  747. package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
  748. package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
  749. package/agent/optional-skills/security/1password/SKILL.md +1 -1
  750. package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
  751. package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
  752. package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
  753. package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
  754. package/agent/package-lock.json +4082 -7907
  755. package/agent/package.json +18 -3
  756. package/agent/plugins/browser/firecrawl/provider.py +4 -1
  757. package/agent/plugins/cron/__init__.py +344 -0
  758. package/agent/plugins/cron/chronos/__init__.py +241 -0
  759. package/agent/plugins/cron/chronos/_nas_client.py +123 -0
  760. package/agent/plugins/cron/chronos/plugin.yaml +9 -0
  761. package/agent/plugins/cron/chronos/verify.py +103 -0
  762. package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
  763. package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
  764. package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
  765. package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
  766. package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
  767. package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
  768. package/agent/plugins/google_meet/audio_bridge.py +4 -0
  769. package/agent/plugins/google_meet/meet_bot.py +7 -1
  770. package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
  771. package/agent/plugins/image_gen/fal/__init__.py +35 -6
  772. package/agent/plugins/image_gen/krea/__init__.py +56 -13
  773. package/agent/plugins/image_gen/openai/__init__.py +122 -24
  774. package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
  775. package/agent/plugins/image_gen/xai/__init__.py +92 -12
  776. package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
  777. package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
  778. package/agent/plugins/memory/__init__.py +48 -5
  779. package/agent/plugins/memory/byterover/__init__.py +1 -0
  780. package/agent/plugins/memory/hindsight/README.md +1 -1
  781. package/agent/plugins/memory/hindsight/__init__.py +138 -24
  782. package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
  783. package/agent/plugins/memory/honcho/README.md +13 -10
  784. package/agent/plugins/memory/honcho/cli.py +247 -122
  785. package/agent/plugins/memory/honcho/client.py +112 -102
  786. package/agent/plugins/memory/openviking/README.md +12 -1
  787. package/agent/plugins/memory/openviking/__init__.py +2281 -107
  788. package/agent/plugins/memory/openviking/plugin.yaml +1 -2
  789. package/agent/plugins/memory/supermemory/README.md +22 -10
  790. package/agent/plugins/memory/supermemory/__init__.py +142 -37
  791. package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
  792. package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
  793. package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
  794. package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
  795. package/agent/plugins/model-providers/custom/__init__.py +8 -2
  796. package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
  797. package/agent/plugins/model-providers/minimax/__init__.py +60 -8
  798. package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
  799. package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
  800. package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
  801. package/agent/plugins/model-providers/zai/__init__.py +1 -0
  802. package/agent/plugins/observability/langfuse/__init__.py +147 -14
  803. package/agent/plugins/observability/nemo_relay/README.md +559 -0
  804. package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
  805. package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
  806. package/agent/plugins/platforms/discord/adapter.py +932 -61
  807. package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
  808. package/agent/plugins/platforms/google_chat/adapter.py +9 -3
  809. package/agent/plugins/platforms/google_chat/oauth.py +1 -1
  810. package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
  811. package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
  812. package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
  813. package/agent/plugins/platforms/irc/adapter.py +4 -1
  814. package/agent/plugins/platforms/line/adapter.py +16 -1
  815. package/agent/plugins/platforms/mattermost/adapter.py +100 -24
  816. package/agent/plugins/platforms/photon/README.md +179 -0
  817. package/agent/plugins/platforms/photon/__init__.py +4 -0
  818. package/agent/plugins/platforms/photon/adapter.py +1586 -0
  819. package/agent/plugins/platforms/photon/auth.py +1046 -0
  820. package/agent/plugins/platforms/photon/cli.py +439 -0
  821. package/agent/plugins/platforms/photon/plugin.yaml +88 -0
  822. package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
  823. package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
  824. package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
  825. package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
  826. package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
  827. package/agent/plugins/platforms/raft/__init__.py +3 -0
  828. package/agent/plugins/platforms/raft/adapter.py +774 -0
  829. package/agent/plugins/platforms/raft/plugin.yaml +19 -0
  830. package/agent/plugins/platforms/simplex/adapter.py +777 -220
  831. package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
  832. package/agent/plugins/platforms/teams/adapter.py +175 -5
  833. package/agent/plugins/plugin_utils.py +135 -0
  834. package/agent/plugins/video_gen/fal/__init__.py +10 -3
  835. package/agent/plugins/web/searxng/provider.py +15 -2
  836. package/agent/plugins/web/xai/provider.py +2 -2
  837. package/agent/providers/base.py +22 -3
  838. package/agent/pyproject.toml +115 -21
  839. package/agent/run_agent.py +733 -39
  840. package/agent/scripts/build_skills_index.py +51 -19
  841. package/agent/scripts/check_subprocess_stdin.py +177 -0
  842. package/agent/scripts/contributor_audit.py +2 -0
  843. package/agent/scripts/docker_config_migrate.py +67 -0
  844. package/agent/scripts/install.cmd +3 -3
  845. package/agent/scripts/install.ps1 +580 -154
  846. package/agent/scripts/install.sh +402 -185
  847. package/agent/scripts/lib/node-bootstrap.sh +39 -4
  848. package/agent/scripts/release.py +183 -0
  849. package/agent/scripts/run_tests.sh +1 -0
  850. package/agent/scripts/run_tests_parallel.py +18 -23
  851. package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
  852. package/agent/setup.py +59 -0
  853. package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
  854. package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
  855. package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
  856. package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
  857. package/agent/skills/clawpump/SKILL.md +3 -1
  858. package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
  859. package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
  860. package/agent/skills/github/github-auth/SKILL.md +2 -2
  861. package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
  862. package/agent/skills/github/github-code-review/SKILL.md +2 -2
  863. package/agent/skills/github/github-issues/SKILL.md +2 -2
  864. package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
  865. package/agent/skills/github/github-repo-management/SKILL.md +2 -2
  866. package/agent/skills/media/gif-search/SKILL.md +1 -1
  867. package/agent/skills/media/youtube-content/SKILL.md +10 -7
  868. package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
  869. package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
  870. package/agent/skills/productivity/airtable/SKILL.md +2 -2
  871. package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
  872. package/agent/skills/productivity/notion/SKILL.md +2 -2
  873. package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
  874. package/agent/skills/research/llm-wiki/SKILL.md +1 -1
  875. package/agent/skills/social-media/xurl/SKILL.md +9 -0
  876. package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
  877. package/agent/skills/software-development/plan/SKILL.md +285 -5
  878. package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
  879. package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
  880. package/agent/skills/software-development/spike/SKILL.md +2 -2
  881. package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
  882. package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
  883. package/agent/tools/approval.py +302 -4
  884. package/agent/tools/async_delegation.py +386 -0
  885. package/agent/tools/blueprints.py +325 -0
  886. package/agent/tools/browser_cdp_tool.py +3 -3
  887. package/agent/tools/browser_tool.py +34 -6
  888. package/agent/tools/checkpoint_manager.py +31 -1
  889. package/agent/tools/clarify_tool.py +55 -5
  890. package/agent/tools/code_execution_tool.py +31 -14
  891. package/agent/tools/computer_use/cua_backend.py +81 -3
  892. package/agent/tools/computer_use/tool.py +79 -5
  893. package/agent/tools/computer_use/vision_routing.py +55 -3
  894. package/agent/tools/credential_files.py +31 -12
  895. package/agent/tools/cronjob_tools.py +30 -20
  896. package/agent/tools/delegate_tool.py +356 -31
  897. package/agent/tools/env_probe.py +1 -0
  898. package/agent/tools/environments/docker.py +163 -8
  899. package/agent/tools/environments/file_sync.py +2 -1
  900. package/agent/tools/environments/local.py +74 -23
  901. package/agent/tools/environments/singularity.py +4 -1
  902. package/agent/tools/environments/ssh.py +78 -11
  903. package/agent/tools/file_operations.py +277 -41
  904. package/agent/tools/file_tools.py +166 -28
  905. package/agent/tools/image_generation_tool.py +515 -29
  906. package/agent/tools/kanban_tools.py +99 -0
  907. package/agent/tools/lazy_deps.py +33 -2
  908. package/agent/tools/mcp_oauth.py +5 -5
  909. package/agent/tools/mcp_oauth_manager.py +7 -5
  910. package/agent/tools/mcp_tool.py +840 -33
  911. package/agent/tools/memory_tool.py +335 -38
  912. package/agent/tools/osv_check.py +15 -1
  913. package/agent/tools/process_registry.py +155 -11
  914. package/agent/tools/read_extract.py +248 -0
  915. package/agent/tools/read_terminal_tool.py +93 -0
  916. package/agent/tools/schema_sanitizer.py +38 -0
  917. package/agent/tools/send_message_tool.py +163 -49
  918. package/agent/tools/session_search_tool.py +189 -7
  919. package/agent/tools/skill_manager_tool.py +202 -3
  920. package/agent/tools/skill_usage.py +52 -4
  921. package/agent/tools/skills_hub.py +184 -44
  922. package/agent/tools/skills_sync.py +232 -5
  923. package/agent/tools/skills_tool.py +125 -11
  924. package/agent/tools/terminal_tool.py +148 -26
  925. package/agent/tools/tirith_security.py +2 -0
  926. package/agent/tools/todo_tool.py +32 -1
  927. package/agent/tools/transcription_tools.py +13 -5
  928. package/agent/tools/tts_tool.py +332 -38
  929. package/agent/tools/url_safety.py +52 -1
  930. package/agent/tools/vision_tools.py +124 -39
  931. package/agent/tools/voice_mode.py +4 -3
  932. package/agent/tools/web_tools.py +45 -15
  933. package/agent/tools/write_approval.py +493 -0
  934. package/agent/toolsets.py +34 -10
  935. package/agent/trajectory_compressor.py +81 -10
  936. package/agent/tui_gateway/entry.py +43 -6
  937. package/agent/tui_gateway/server.py +3335 -330
  938. package/agent/tui_gateway/slash_worker.py +61 -0
  939. package/agent/tui_gateway/ws.py +67 -9
  940. package/agent/ui-tui/eslint.config.mjs +0 -4
  941. package/agent/ui-tui/package.json +6 -6
  942. package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
  943. package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
  944. package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  945. package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
  946. package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
  947. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
  948. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
  949. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
  950. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
  951. package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
  952. package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
  953. package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  954. package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
  955. package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
  956. package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
  957. package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
  958. package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
  959. package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
  960. package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
  961. package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
  962. package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
  963. package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
  964. package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
  965. package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
  966. package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
  967. package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
  968. package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
  969. package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
  970. package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
  971. package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
  972. package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
  973. package/agent/ui-tui/src/app/interfaces.ts +64 -1
  974. package/agent/ui-tui/src/app/overlayStore.ts +18 -2
  975. package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
  976. package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
  977. package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
  978. package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
  979. package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
  980. package/agent/ui-tui/src/app/slash/registry.ts +4 -0
  981. package/agent/ui-tui/src/app/turnController.ts +145 -2
  982. package/agent/ui-tui/src/app/uiStore.ts +2 -0
  983. package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
  984. package/agent/ui-tui/src/app/useMainApp.ts +54 -8
  985. package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
  986. package/agent/ui-tui/src/app/useSubmission.ts +23 -31
  987. package/agent/ui-tui/src/components/appChrome.tsx +112 -5
  988. package/agent/ui-tui/src/components/appLayout.tsx +9 -0
  989. package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
  990. package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
  991. package/agent/ui-tui/src/components/branding.tsx +15 -3
  992. package/agent/ui-tui/src/components/messageLine.tsx +25 -3
  993. package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
  994. package/agent/ui-tui/src/components/prompts.tsx +31 -17
  995. package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
  996. package/agent/ui-tui/src/components/textInput.tsx +16 -0
  997. package/agent/ui-tui/src/config/env.ts +12 -0
  998. package/agent/ui-tui/src/config/limits.ts +13 -0
  999. package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
  1000. package/agent/ui-tui/src/domain/paths.ts +24 -0
  1001. package/agent/ui-tui/src/domain/slash.ts +40 -0
  1002. package/agent/ui-tui/src/entry.tsx +35 -4
  1003. package/agent/ui-tui/src/gatewayClient.ts +22 -10
  1004. package/agent/ui-tui/src/gatewayTypes.ts +130 -1
  1005. package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
  1006. package/agent/ui-tui/src/lib/memory.test.ts +162 -0
  1007. package/agent/ui-tui/src/lib/memory.ts +60 -1
  1008. package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
  1009. package/agent/ui-tui/src/lib/osc52.ts +1 -1
  1010. package/agent/ui-tui/src/lib/text.test.ts +32 -1
  1011. package/agent/ui-tui/src/lib/text.ts +29 -2
  1012. package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
  1013. package/agent/ui-tui/src/types.ts +5 -0
  1014. package/agent/ui-tui/tsconfig.build.json +0 -1
  1015. package/agent/ui-tui/tsconfig.json +2 -1
  1016. package/agent/utils.py +66 -2
  1017. package/agent/uv.lock +300 -684
  1018. package/agent/web/index.html +2 -2
  1019. package/agent/web/package.json +11 -6
  1020. package/agent/web/public/claw-bg.webp +0 -0
  1021. package/agent/web/public/claw-logo.webp +0 -0
  1022. package/agent/web/src/App.tsx +138 -48
  1023. package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
  1024. package/agent/web/src/components/Backdrop.tsx +15 -0
  1025. package/agent/web/src/components/ChatSessionList.tsx +260 -0
  1026. package/agent/web/src/components/ChatSidebar.tsx +262 -78
  1027. package/agent/web/src/components/ConfirmDialog.tsx +122 -0
  1028. package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
  1029. package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
  1030. package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
  1031. package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
  1032. package/agent/web/src/components/ReasoningPicker.tsx +167 -0
  1033. package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
  1034. package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
  1035. package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
  1036. package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
  1037. package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
  1038. package/agent/web/src/contexts/SystemActions.tsx +6 -8
  1039. package/agent/web/src/contexts/profile-context.ts +19 -0
  1040. package/agent/web/src/contexts/useProfileScope.ts +6 -0
  1041. package/agent/web/src/i18n/af.ts +5 -4
  1042. package/agent/web/src/i18n/de.ts +5 -4
  1043. package/agent/web/src/i18n/en.ts +58 -4
  1044. package/agent/web/src/i18n/es.ts +5 -3
  1045. package/agent/web/src/i18n/fr.ts +5 -3
  1046. package/agent/web/src/i18n/ga.ts +5 -4
  1047. package/agent/web/src/i18n/hu.ts +5 -4
  1048. package/agent/web/src/i18n/it.ts +5 -4
  1049. package/agent/web/src/i18n/ja.ts +5 -4
  1050. package/agent/web/src/i18n/ko.ts +5 -4
  1051. package/agent/web/src/i18n/pt.ts +5 -3
  1052. package/agent/web/src/i18n/ru.ts +5 -4
  1053. package/agent/web/src/i18n/tr.ts +5 -4
  1054. package/agent/web/src/i18n/types.ts +59 -1
  1055. package/agent/web/src/i18n/uk.ts +5 -3
  1056. package/agent/web/src/i18n/zh-hant.ts +5 -4
  1057. package/agent/web/src/i18n/zh.ts +5 -4
  1058. package/agent/web/src/index.css +2 -2
  1059. package/agent/web/src/lib/api.ts +819 -52
  1060. package/agent/web/src/lib/dashboard-flags.ts +16 -7
  1061. package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
  1062. package/agent/web/src/lib/reasoning-effort.ts +36 -0
  1063. package/agent/web/src/lib/session-refresh.test.ts +21 -0
  1064. package/agent/web/src/lib/session-refresh.ts +26 -0
  1065. package/agent/web/src/pages/ChannelsPage.tsx +529 -68
  1066. package/agent/web/src/pages/ChatPage.tsx +249 -56
  1067. package/agent/web/src/pages/ConfigPage.tsx +11 -1
  1068. package/agent/web/src/pages/CronPage.tsx +219 -31
  1069. package/agent/web/src/pages/EnvPage.tsx +25 -6
  1070. package/agent/web/src/pages/FilesPage.tsx +525 -0
  1071. package/agent/web/src/pages/McpPage.tsx +80 -3
  1072. package/agent/web/src/pages/ModelsPage.tsx +97 -12
  1073. package/agent/web/src/pages/PluginsPage.tsx +1 -1
  1074. package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
  1075. package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
  1076. package/agent/web/src/pages/SessionsPage.tsx +144 -13
  1077. package/agent/web/src/pages/SkillsPage.tsx +851 -70
  1078. package/agent/web/src/pages/SystemPage.tsx +340 -4
  1079. package/agent/web/src/pages/WalletPage.tsx +401 -0
  1080. package/agent/web/src/pages/WebhooksPage.tsx +145 -15
  1081. package/agent/web/src/pages/X402Page.tsx +207 -0
  1082. package/agent/web/src/plugins/registry.ts +28 -11
  1083. package/agent/web/src/plugins/sdk.d.ts +160 -0
  1084. package/agent/web/src/themes/context.tsx +112 -5
  1085. package/agent/web/src/themes/fonts.ts +167 -0
  1086. package/agent/web/src/themes/index.ts +7 -0
  1087. package/agent/web/tsconfig.app.json +0 -1
  1088. package/agent/web/vite.config.ts +1 -8
  1089. package/agent/web/vitest.config.ts +16 -0
  1090. package/package.json +1 -1
  1091. package/agent/apps/desktop/package-lock.json +0 -18363
  1092. package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
  1093. package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
  1094. package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
  1095. package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
  1096. package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
  1097. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
  1098. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
  1099. package/agent/skills/diagramming/DESCRIPTION.md +0 -3
  1100. package/agent/skills/domain/DESCRIPTION.md +0 -24
  1101. package/agent/skills/gifs/DESCRIPTION.md +0 -3
  1102. package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
  1103. package/agent/skills/mcp/DESCRIPTION.md +0 -3
  1104. package/agent/skills/media/spotify/SKILL.md +0 -135
  1105. package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
  1106. package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
  1107. package/agent/skills/productivity/linear/SKILL.md +0 -380
  1108. package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
  1109. package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
  1110. package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
  1111. package/agent/ui-tui/package-lock.json +0 -7449
  1112. package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
  1113. package/agent/web/package-lock.json +0 -8887
  1114. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
  1115. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
  1116. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
  1117. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
  1118. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
  1119. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
  1120. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
  1121. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
  1122. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
  1123. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
  1124. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
  1125. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
  1126. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
  1127. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
  1128. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
  1129. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
  1130. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
  1131. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
  1132. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
  1133. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
  1134. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
  1135. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
  1136. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
  1137. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
  1138. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
  1139. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
  1140. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
  1141. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
  1142. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
  1143. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
  1144. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
  1145. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
  1146. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
  1147. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
  1148. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
  1149. /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
  1150. /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
  1151. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
  1152. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
  1153. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
  1154. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
  1155. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
  1156. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
  1157. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
  1158. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
  1159. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
  1160. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
  1161. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
  1162. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
  1163. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
  1164. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
  1165. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
  1166. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
  1167. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
  1168. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
  1169. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
  1170. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
  1171. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
  1172. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
  1173. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
  1174. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
  1175. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
  1176. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
  1177. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
  1178. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
  1179. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
  1180. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
  1181. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
  1182. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
  1183. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
  1184. /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
  1185. /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
  1186. /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
  1187. /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
  1188. /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
  1189. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
  1190. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
  1191. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
  1192. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
  1193. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
  1194. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
  1195. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
  1196. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
  1197. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
  1198. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
  1199. /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
  1200. /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
  1201. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
  1202. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
  1203. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
  1204. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
  1205. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
  1206. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
  1207. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
  1208. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
  1209. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
  1210. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
  1211. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
  1212. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
@@ -1,36 +1,89 @@
1
1
  import type { AppendMessage, ThreadMessage } from '@assistant-ui/react'
2
- import { type MutableRefObject, useCallback } from 'react'
2
+ import { useStore } from '@nanostores/react'
3
+ import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
3
4
 
4
- import { transcribeAudio } from '@/hermes'
5
- import { appendTextPart, branchGroupForUser, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages'
5
+ import { getProfiles, transcribeAudio } from '@/hermes'
6
+ import { translateNow, type Translations, useI18n } from '@/i18n'
7
+ import { stripAnsi } from '@/lib/ansi'
8
+ import { branchGroupForUser, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages'
6
9
  import {
7
- attachmentDisplayText,
8
- INTERRUPTED_MARKER,
10
+ optimisticAttachmentRef,
9
11
  parseCommandDispatch,
10
12
  parseSlashCommand,
11
13
  pathLabel,
14
+ sessionTitle,
12
15
  SLASH_COMMAND_RE
13
16
  } from '@/lib/chat-runtime'
14
17
  import {
15
18
  type CommandsCatalogLike,
19
+ type DesktopActionId,
20
+ type DesktopPickerId,
16
21
  desktopSlashUnavailableMessage,
17
22
  filterDesktopCommandsCatalog,
18
- isDesktopSlashCommand
23
+ isDesktopSlashCommand,
24
+ resolveDesktopCommand
19
25
  } from '@/lib/desktop-slash-commands'
20
26
  import { triggerHaptic } from '@/lib/haptics'
27
+ import { setMutableRef } from '@/lib/mutable-ref'
21
28
  import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
29
+ import { setSessionYolo } from '@/lib/yolo-session'
22
30
  import {
23
31
  $composerAttachments,
24
- addComposerAttachment,
25
32
  clearComposerAttachments,
26
33
  type ComposerAttachment,
27
- terminalContextBlocksFromDraft
34
+ setComposerAttachmentUploadState,
35
+ setComposerDraft,
36
+ terminalContextBlocksFromDraft,
37
+ updateComposerAttachment
28
38
  } from '@/store/composer'
39
+ import { resetSessionBackground } from '@/store/composer-status'
29
40
  import { clearNotifications, notify, notifyError } from '@/store/notifications'
30
41
  import { requestDesktopOnboarding } from '@/store/onboarding'
31
- import { $busy, $messages, setAwaitingResponse, setBusy, setMessages } from '@/store/session'
42
+ import { $activeGatewayProfile, $newChatProfile, ensureGatewayProfile, normalizeProfileKey } from '@/store/profile'
43
+ import {
44
+ $busy,
45
+ $connection,
46
+ $messages,
47
+ $sessions,
48
+ $yoloActive,
49
+ setAwaitingResponse,
50
+ setBusy,
51
+ setMessages,
52
+ setModelPickerOpen,
53
+ setSessionPickerOpen,
54
+ setSessions,
55
+ setYoloActive
56
+ } from '@/store/session'
57
+ import { clearSessionSubagents } from '@/store/subagents'
58
+ import { clearSessionTodos } from '@/store/todos'
59
+
60
+ import type {
61
+ ClientSessionState,
62
+ BrowserManageResponse,
63
+ FileAttachResponse,
64
+ HandoffFailResponse,
65
+ HandoffRequestResponse,
66
+ HandoffStateResponse,
67
+ ImageAttachResponse,
68
+ SessionSteerResponse,
69
+ SessionTitleResponse,
70
+ SlashExecResponse
71
+ } from '../../types'
72
+
73
+ interface HandoffResult {
74
+ ok: boolean
75
+ error?: string
76
+ }
77
+
78
+ function delay(ms: number): Promise<void> {
79
+ return new Promise(resolve => setTimeout(resolve, ms))
80
+ }
32
81
 
33
- import type { ClientSessionState, ImageAttachResponse, SlashExecResponse } from '../../types'
82
+ function isSessionIdCandidate(value: string): boolean {
83
+ const trimmed = value.trim()
84
+
85
+ return /^\d{8}_\d{6}_[A-Fa-f0-9]{6}$/.test(trimmed) || /^[A-Fa-f0-9]{32}$/.test(trimmed)
86
+ }
34
87
 
35
88
  function blobToDataUrl(blob: Blob): Promise<string> {
36
89
  return new Promise((resolve, reject) => {
@@ -40,10 +93,10 @@ function blobToDataUrl(blob: Blob): Promise<string> {
40
93
  if (typeof reader.result === 'string') {
41
94
  resolve(reader.result)
42
95
  } else {
43
- reject(new Error('Could not read recorded audio'))
96
+ reject(new Error(translateNow('desktop.audioReadFailed')))
44
97
  }
45
98
  })
46
- reader.addEventListener('error', () => reject(reader.error || new Error('Could not read recorded audio')))
99
+ reader.addEventListener('error', () => reject(reader.error || new Error(translateNow('desktop.audioReadFailed'))))
47
100
  reader.readAsDataURL(blob)
48
101
  })
49
102
  }
@@ -60,14 +113,208 @@ function inlineErrorMessage(error: unknown, fallback: string): string {
60
113
  return (raw.match(/Error invoking remote method '[^']+': Error: (.+)$/)?.[1] ?? raw).replace(/^Error:\s*/, '').trim()
61
114
  }
62
115
 
116
+ function isSessionNotFoundError(error: unknown): boolean {
117
+ const message = error instanceof Error ? error.message : String(error)
118
+
119
+ return /session not found/i.test(message)
120
+ }
121
+
122
+ // The gateway refuses prompt.submit while a turn is running (4009 "session
123
+ // busy"). It's a transient concurrency guard, never a user-facing error: a
124
+ // submit racing the settle edge (or a rewind interrupting mid-turn) just waits
125
+ // a beat for the turn to wind down, then lands. Bounded so a genuinely stuck
126
+ // turn still surfaces eventually.
127
+ const SESSION_BUSY_RETRY_TIMEOUT_MS = 6_000
128
+ const SESSION_BUSY_RETRY_INTERVAL_MS = 150
129
+
130
+ function isSessionBusyError(error: unknown): boolean {
131
+ return /session busy/i.test(error instanceof Error ? error.message : String(error))
132
+ }
133
+
134
+ const sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
135
+
136
+ // Retry a gateway call across transient "session busy" so it never reaches the
137
+ // user — the turn settles within the deadline and the call lands.
138
+ async function withSessionBusyRetry<T>(call: () => Promise<T>): Promise<T> {
139
+ const deadline = Date.now() + SESSION_BUSY_RETRY_TIMEOUT_MS
140
+
141
+ for (;;) {
142
+ try {
143
+ return await call()
144
+ } catch (err) {
145
+ if (isSessionBusyError(err) && Date.now() < deadline) {
146
+ await sleep(SESSION_BUSY_RETRY_INTERVAL_MS)
147
+
148
+ continue
149
+ }
150
+
151
+ throw err
152
+ }
153
+ }
154
+ }
155
+
156
+ function base64FromDataUrl(dataUrl: string): string {
157
+ const comma = dataUrl.indexOf(',')
158
+
159
+ return comma >= 0 ? dataUrl.slice(comma + 1) : ''
160
+ }
161
+
162
+ function imageFilenameFromPath(filePath: string): string {
163
+ return filePath.split(/[\\/]/).filter(Boolean).pop() || 'image.png'
164
+ }
165
+
166
+ // Remote gateway: the local composer-image file lives on THIS machine's disk,
167
+ // not the gateway's, so read the bytes here and upload them via
168
+ // image.attach_bytes. Returns null when the file can't be read.
169
+ async function readImageForRemoteAttach(
170
+ filePath: string
171
+ ): Promise<{ contentBase64: string; filename: string } | null> {
172
+ const dataUrl = await window.hermesDesktop?.readFileDataUrl(filePath)
173
+ const contentBase64 = dataUrl ? base64FromDataUrl(dataUrl) : ''
174
+
175
+ return contentBase64 ? { contentBase64, filename: imageFilenameFromPath(filePath) } : null
176
+ }
177
+
178
+ // Read a non-image file as a data URL for upload via file.attach. Returns null
179
+ // when the desktop bridge can't read the file (e.g. it was moved/deleted).
180
+ async function readFileDataUrlForAttach(filePath: string): Promise<string | null> {
181
+ const reader = window.hermesDesktop?.readFileDataUrl
182
+
183
+ if (!reader) {
184
+ return null
185
+ }
186
+
187
+ const dataUrl = await reader(filePath)
188
+
189
+ return dataUrl || null
190
+ }
191
+
192
+ // The readFileDataUrl IPC base64-loads the whole file into memory and is
193
+ // hard-capped (DATA_URL_READ_MAX_BYTES, 16 MB) in electron/hardening.cjs, which
194
+ // rejects with a raw "file is too large (N bytes; limit M bytes)" string. In
195
+ // remote mode every attachment's bytes go through that read, so a big file
196
+ // surfaces that internal message verbatim in the failure toast. Translate it
197
+ // into a friendly "too large to upload to the remote gateway" line, parsing the
198
+ // limit out of the message so it tracks the real cap. Non-cap errors pass
199
+ // through unchanged.
200
+ function friendlyRemoteAttachError(err: unknown, label: string): Error {
201
+ const message = err instanceof Error ? err.message : String(err)
202
+
203
+ if (!/too large/i.test(message)) {
204
+ return err instanceof Error ? err : new Error(message)
205
+ }
206
+
207
+ const limitBytes = Number(message.match(/limit (\d+) bytes/)?.[1])
208
+ const cap = Number.isFinite(limitBytes) && limitBytes > 0 ? ` (max ${Math.floor(limitBytes / (1024 * 1024))} MB)` : ''
209
+
210
+ return new Error(`${label} is too large to upload to the remote gateway${cap}.`)
211
+ }
212
+
213
+ type GatewayRequest = <T>(method: string, params?: Record<string, unknown>) => Promise<T>
214
+
215
+ /**
216
+ * Stage one file/image attachment into the session workspace and return the
217
+ * attachment rewritten with the gateway-side ref. Images upload their bytes in
218
+ * remote mode (so vision works) and pass the path locally; non-image files
219
+ * upload bytes remotely and pass the path locally. Throws on failure so callers
220
+ * can surface an error. Shared by submit-time sync, the eager drop-time upload,
221
+ * and the message-edit composer drop — keep them in lockstep.
222
+ */
223
+ export async function uploadComposerAttachment(
224
+ attachment: ComposerAttachment,
225
+ opts: { remote: boolean; requestGateway: GatewayRequest; sessionId: string }
226
+ ): Promise<ComposerAttachment> {
227
+ const { remote, requestGateway, sessionId } = opts
228
+ const path = attachment.path ?? ''
229
+ const label = attachment.label || pathLabel(path)
230
+
231
+ if (attachment.kind === 'image') {
232
+ let result: ImageAttachResponse
233
+
234
+ if (remote) {
235
+ let payload: Awaited<ReturnType<typeof readImageForRemoteAttach>>
236
+
237
+ try {
238
+ payload = await readImageForRemoteAttach(path)
239
+ } catch (err) {
240
+ throw friendlyRemoteAttachError(err, label)
241
+ }
242
+
243
+ if (!payload) {
244
+ throw new Error(`Could not read ${label}`)
245
+ }
246
+
247
+ result = await requestGateway<ImageAttachResponse>('image.attach_bytes', {
248
+ session_id: sessionId,
249
+ content_base64: payload.contentBase64,
250
+ filename: payload.filename
251
+ })
252
+ } else {
253
+ result = await requestGateway<ImageAttachResponse>('image.attach', {
254
+ path,
255
+ session_id: sessionId
256
+ })
257
+ }
258
+
259
+ if (!result.attached) {
260
+ throw new Error(result.message || `Could not attach ${label}`)
261
+ }
262
+
263
+ const attachedPath = result.path || path
264
+
265
+ return {
266
+ ...attachment,
267
+ attachedSessionId: sessionId,
268
+ label: attachedPath ? pathLabel(attachedPath) : attachment.label,
269
+ path: attachedPath,
270
+ uploadState: undefined
271
+ }
272
+ }
273
+
274
+ // Non-image file.
275
+ let dataUrl: string | null = null
276
+
277
+ if (remote) {
278
+ try {
279
+ dataUrl = await readFileDataUrlForAttach(path)
280
+ } catch (err) {
281
+ throw friendlyRemoteAttachError(err, label)
282
+ }
283
+
284
+ if (!dataUrl) {
285
+ throw new Error(`Could not read ${label}`)
286
+ }
287
+ }
288
+
289
+ const result = await requestGateway<FileAttachResponse>('file.attach', {
290
+ name: label,
291
+ path,
292
+ session_id: sessionId,
293
+ ...(dataUrl ? { data_url: dataUrl } : {})
294
+ })
295
+
296
+ if (!result.attached || !result.ref_text) {
297
+ throw new Error(result.message || `Could not attach ${label}`)
298
+ }
299
+
300
+ return {
301
+ ...attachment,
302
+ attachedSessionId: sessionId,
303
+ refText: result.ref_text,
304
+ uploadState: undefined
305
+ }
306
+ }
307
+
63
308
  interface PromptActionsOptions {
64
309
  activeSessionId: string | null
65
310
  activeSessionIdRef: MutableRefObject<string | null>
66
311
  busyRef: MutableRefObject<boolean>
67
312
  branchCurrentSession: () => Promise<boolean>
68
- createBackendSessionForSend: () => Promise<string | null>
313
+ createBackendSessionForSend: (preview?: string | null) => Promise<string | null>
69
314
  handleSkinCommand: (arg: string) => string
315
+ refreshSessions: () => Promise<void>
70
316
  requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
317
+ resumeStoredSession: (storedSessionId: string) => Promise<void> | void
71
318
  selectedStoredSessionIdRef: MutableRefObject<string | null>
72
319
  startFreshSessionDraft: () => void
73
320
  sttEnabled: boolean
@@ -83,12 +330,21 @@ interface SubmitTextOptions {
83
330
  fromQueue?: boolean
84
331
  }
85
332
 
86
- function renderCommandsCatalog(catalog: CommandsCatalogLike): string {
333
+ /** Everything a slash handler needs about the invocation it's serving. */
334
+ interface SlashActionCtx {
335
+ arg: string
336
+ command: string
337
+ name: string
338
+ recordInput: boolean
339
+ sessionHint?: string
340
+ }
341
+
342
+ function renderCommandsCatalog(catalog: CommandsCatalogLike, copy: Translations['desktop']): string {
87
343
  const desktopCatalog = filterDesktopCommandsCatalog(catalog)
88
344
 
89
345
  const sections = desktopCatalog.categories?.length
90
346
  ? desktopCatalog.categories
91
- : [{ name: 'Desktop commands', pairs: desktopCatalog.pairs ?? [] }]
347
+ : [{ name: copy.desktopCommands, pairs: desktopCatalog.pairs ?? [] }]
92
348
 
93
349
  const body = sections
94
350
  .filter(section => section.pairs.length > 0)
@@ -100,8 +356,8 @@ function renderCommandsCatalog(catalog: CommandsCatalogLike): string {
100
356
  .join('\n\n')
101
357
 
102
358
  const tail = [
103
- desktopCatalog.skill_count ? `${desktopCatalog.skill_count} skill commands available.` : '',
104
- desktopCatalog.warning ? `warning: ${desktopCatalog.warning}` : ''
359
+ desktopCatalog.skill_count ? copy.skillCommandsAvailable(desktopCatalog.skill_count) : '',
360
+ desktopCatalog.warning ? copy.warningLine(desktopCatalog.warning) : ''
105
361
  ]
106
362
  .filter(Boolean)
107
363
  .join('\n')
@@ -131,15 +387,24 @@ export function usePromptActions({
131
387
  branchCurrentSession,
132
388
  createBackendSessionForSend,
133
389
  handleSkinCommand,
390
+ refreshSessions,
134
391
  requestGateway,
392
+ resumeStoredSession,
135
393
  selectedStoredSessionIdRef,
136
394
  startFreshSessionDraft,
137
395
  sttEnabled,
138
396
  updateSessionState
139
397
  }: PromptActionsOptions) {
398
+ const { t } = useI18n()
399
+ const copy = t.desktop
400
+
140
401
  const appendSessionTextMessage = useCallback(
141
402
  (sessionId: string, role: ChatMessage['role'], text: string) => {
142
- const body = text.trim()
403
+ // Strip ANSI: slash-command output from the backend worker carries SGR
404
+ // color codes (e.g. "Unknown command" in red). The ESC byte is invisible
405
+ // in the chat panel, so without this the `[1;31m…[0m` payload leaks as
406
+ // literal text.
407
+ const body = stripAnsi(text).trim()
143
408
 
144
409
  if (!body) {
145
410
  return
@@ -164,80 +429,173 @@ export function usePromptActions({
164
429
  [selectedStoredSessionIdRef, updateSessionState]
165
430
  )
166
431
 
167
- const syncImageAttachmentsForSubmit = useCallback(
432
+ // In-flight drop-time eager uploads, keyed by attachment id. Submit joins
433
+ // these before re-uploading so a drop-then-immediately-Enter can't fire
434
+ // file.attach twice and stage duplicate copies on the gateway.
435
+ const eagerUploadInFlight = useRef<Map<string, Promise<void>>>(new Map())
436
+
437
+ const syncAttachmentsForSubmit = useCallback(
168
438
  async (
169
439
  sessionId: string,
170
440
  attachments: ComposerAttachment[],
171
441
  options: { updateComposerAttachments?: boolean } = {}
172
- ) => {
442
+ ): Promise<ComposerAttachment[]> => {
173
443
  const updateComposerAttachments = options.updateComposerAttachments ?? true
174
- const images = attachments.filter(attachment => attachment.kind === 'image' && attachment.path)
444
+ const remote = $connection.get()?.mode === 'remote'
445
+ const synced: ComposerAttachment[] = []
446
+
447
+ for (const original of attachments) {
448
+ let attachment = original
449
+
450
+ // Join a drop-time eager upload still in flight for this attachment
451
+ // before deciding anything — otherwise submit and the eager task both
452
+ // call file.attach and stage duplicate files. After it settles, take the
453
+ // store's updated copy (its gateway ref, or its failure) over the stale
454
+ // pre-upload snapshot.
455
+ const inFlight = eagerUploadInFlight.current.get(attachment.id)
456
+
457
+ if (inFlight) {
458
+ await inFlight
459
+ attachment = $composerAttachments.get().find(item => item.id === attachment.id) ?? attachment
460
+ }
461
+
462
+ // Already-synced or pathless refs (terminal, url, etc.) pass through.
463
+ // A drop-time eager upload may already have staged this one (matching
464
+ // attachedSessionId) — don't re-upload it.
465
+ if (!attachment.path || attachment.attachedSessionId === sessionId) {
466
+ synced.push(attachment)
175
467
 
176
- for (const attachment of images) {
177
- if (attachment.attachedSessionId === sessionId) {
178
468
  continue
179
469
  }
180
470
 
181
- const result = await requestGateway<ImageAttachResponse>('image.attach', {
182
- session_id: sessionId,
183
- path: attachment.path
184
- })
471
+ if (attachment.kind === 'image' || attachment.kind === 'file') {
472
+ const nextAttachment = await uploadComposerAttachment(attachment, { remote, requestGateway, sessionId })
185
473
 
186
- if (!result.attached) {
187
- const label = attachment.label || (attachment.path ? pathLabel(attachment.path) : 'image')
188
- throw new Error(result.message || `Could not attach ${label}`)
189
- }
474
+ // Update-only: never resurrect a chip the user removed mid-upload.
475
+ if (updateComposerAttachments) {
476
+ updateComposerAttachment(nextAttachment)
477
+ }
190
478
 
191
- const attachedPath = result.path || attachment.path
479
+ synced.push(nextAttachment)
192
480
 
193
- if (updateComposerAttachments) {
194
- addComposerAttachment({
195
- ...attachment,
196
- id: attachment.id,
197
- label: attachedPath ? pathLabel(attachedPath) : attachment.label,
198
- path: attachedPath,
199
- attachedSessionId: sessionId
200
- })
481
+ continue
201
482
  }
483
+
484
+ synced.push(attachment)
202
485
  }
486
+
487
+ return synced
203
488
  },
204
489
  [requestGateway]
205
490
  )
206
491
 
492
+ // Stage a freshly dropped file as soon as it lands (when a session already
493
+ // exists), so the upload runs while the user is still typing rather than
494
+ // stalling the send. The card shows a spinner via `uploadState`; on success
495
+ // the chip carries its gateway-side ref so submit skips re-uploading.
496
+ //
497
+ // Images are intentionally NOT eager-uploaded: attachImagePath adds the chip
498
+ // and then fills in `previewUrl` (the base64 thumbnail) on a second tick, so
499
+ // an eager upload would race that write — clobbering the thumbnail and
500
+ // swapping `path` to a gateway path the local preview can't read. Images are
501
+ // small and still byte-upload at submit via image.attach_bytes.
502
+ const eagerlyUploadAttachment = useCallback(
503
+ async (sessionId: string, attachment: ComposerAttachment) => {
504
+ const remote = $connection.get()?.mode === 'remote'
505
+
506
+ setComposerAttachmentUploadState(attachment.id, 'uploading')
507
+
508
+ try {
509
+ // Update-only: if the user removed the chip while this was uploading,
510
+ // don't resurrect it — just drop the staged result on the floor.
511
+ updateComposerAttachment(await uploadComposerAttachment(attachment, { remote, requestGateway, sessionId }))
512
+ } catch (err) {
513
+ // Leave the chip in place so submit-time sync can retry (or the user can
514
+ // remove it) and flag the card; also toast so a hard failure (unreadable
515
+ // file, gateway perms) isn't swallowed while the user keeps typing.
516
+ setComposerAttachmentUploadState(attachment.id, 'error')
517
+ notifyError(err, copy.dropFiles)
518
+ }
519
+ },
520
+ [copy.dropFiles, requestGateway]
521
+ )
522
+
523
+ const composerAttachments = useStore($composerAttachments)
524
+
525
+ useEffect(() => {
526
+ if (!activeSessionId) {
527
+ return
528
+ }
529
+
530
+ for (const attachment of composerAttachments) {
531
+ const needsUpload =
532
+ attachment.kind === 'file' &&
533
+ Boolean(attachment.path) &&
534
+ !attachment.attachedSessionId &&
535
+ !attachment.uploadState &&
536
+ !eagerUploadInFlight.current.has(attachment.id)
537
+
538
+ if (!needsUpload) {
539
+ continue
540
+ }
541
+
542
+ const task = eagerlyUploadAttachment(activeSessionId, attachment).finally(() =>
543
+ eagerUploadInFlight.current.delete(attachment.id)
544
+ )
545
+
546
+ eagerUploadInFlight.current.set(attachment.id, task)
547
+ }
548
+ }, [activeSessionId, composerAttachments, eagerlyUploadAttachment])
549
+
207
550
  const submitPromptText = useCallback(
208
551
  async (rawText: string, options?: SubmitTextOptions) => {
209
552
  const visibleText = rawText.trim()
210
553
  const usingComposerAttachments = !options?.attachments
211
554
  const attachments = options?.attachments ?? $composerAttachments.get()
212
555
 
213
- const contextRefs = attachments
214
- .map(a => a.refText)
215
- .filter(Boolean)
216
- .join('\n')
217
-
218
556
  const terminalContextBlocks = terminalContextBlocksFromDraft(rawText).join('\n\n')
219
557
  const hasImage = attachments.some(a => a.kind === 'image')
220
- const attachmentRefs = attachments.map(attachmentDisplayText).filter((r): r is string => Boolean(r))
221
558
 
222
- const text =
223
- [contextRefs, terminalContextBlocks, visibleText].filter(Boolean).join('\n\n') ||
224
- (hasImage ? 'What do you see in this image?' : '')
559
+ // Refs are recomputed after sync (file.attach rewrites @file: refs to
560
+ // workspace-relative paths the remote gateway can resolve). Seed the
561
+ // optimistic message with the pre-sync refs, then rewrite once synced.
562
+ // Images use their base64 preview so the thumbnail renders inline without
563
+ // a (remote-mode 403-prone) /api/media fetch — see optimisticAttachmentRef.
564
+ let attachmentRefs = attachments.map(optimisticAttachmentRef).filter((r): r is string => Boolean(r))
565
+
566
+ const buildContextText = (atts: ComposerAttachment[]): string => {
567
+ const contextRefs = atts
568
+ .map(a => a.refText)
569
+ .filter(Boolean)
570
+ .join('\n')
571
+
572
+ return (
573
+ [contextRefs, terminalContextBlocks, visibleText].filter(Boolean).join('\n\n') ||
574
+ (atts.some(a => a.kind === 'image') ? 'What do you see in this image?' : '')
575
+ )
576
+ }
577
+
578
+ // Queue drains fire on the busy→false settle edge, where busyRef (synced
579
+ // from $busy by a separate effect) may still read true — honoring it would
580
+ // bounce the drained send. The drain lock serializes them; the user path
581
+ // keeps the guard so a stray Enter mid-turn can't double-submit.
582
+ const hasSendable = Boolean(visibleText || terminalContextBlocks || attachments.length || hasImage)
225
583
 
226
- if (!text || busyRef.current) {
584
+ if (!hasSendable || (!options?.fromQueue && busyRef.current)) {
227
585
  return false
228
586
  }
229
587
 
230
588
  const optimisticId = `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
231
589
 
232
- const userMessage: ChatMessage = {
590
+ const buildUserMessage = (): ChatMessage => ({
233
591
  id: optimisticId,
234
592
  role: 'user',
235
593
  parts: [textPart(visibleText || (attachmentRefs.length ? '' : attachments.map(a => a.label).join(', ')))],
236
594
  attachmentRefs
237
- }
595
+ })
238
596
 
239
597
  const releaseBusy = () => {
240
- busyRef.current = false
598
+ setMutableRef(busyRef, false)
241
599
  setBusy(false)
242
600
  setAwaitingResponse(false)
243
601
  }
@@ -251,12 +609,27 @@ export function usePromptActions({
251
609
  ...state,
252
610
  messages: state.messages.some(m => m.id === optimisticId)
253
611
  ? state.messages
254
- : [...state.messages, userMessage],
612
+ : [...state.messages, buildUserMessage()],
255
613
  busy: true,
256
614
  awaitingResponse: true,
257
615
  pendingBranchGroup: null,
258
616
  sawAssistantPayload: false,
259
- interrupted: state.interrupted
617
+ // Fresh submit = new turn — clear any leftover interrupt flag, else
618
+ // mutateStream/completeAssistantMessage drop every delta of this turn
619
+ // (what made drained-after-interrupt sends go silent).
620
+ interrupted: false
621
+ }),
622
+ selectedStoredSessionIdRef.current
623
+ )
624
+
625
+ // After sync rewrites refs, refresh the optimistic message in place so the
626
+ // transcript shows the resolved @file: ref rather than the local path.
627
+ const rewriteOptimistic = (sid: string) =>
628
+ updateSessionState(
629
+ sid,
630
+ state => ({
631
+ ...state,
632
+ messages: state.messages.map(message => (message.id === optimisticId ? buildUserMessage() : message))
260
633
  }),
261
634
  selectedStoredSessionIdRef.current
262
635
  )
@@ -281,7 +654,7 @@ export function usePromptActions({
281
654
  )
282
655
  }
283
656
 
284
- busyRef.current = true
657
+ setMutableRef(busyRef, true)
285
658
  setBusy(true)
286
659
  setAwaitingResponse(true)
287
660
  clearNotifications()
@@ -291,16 +664,16 @@ export function usePromptActions({
291
664
  if (sessionId) {
292
665
  seedOptimistic(sessionId)
293
666
  } else {
294
- setMessages(current => [...current, userMessage])
667
+ setMessages(current => [...current, buildUserMessage()])
295
668
  }
296
669
 
297
670
  if (!sessionId) {
298
671
  try {
299
- sessionId = await createBackendSessionForSend()
672
+ sessionId = await createBackendSessionForSend(visibleText)
300
673
  } catch (err) {
301
674
  dropOptimistic(null)
302
675
  releaseBusy()
303
- notifyError(err, 'Session unavailable')
676
+ notifyError(err, copy.sessionUnavailable)
304
677
 
305
678
  return false
306
679
  }
@@ -308,7 +681,7 @@ export function usePromptActions({
308
681
  if (!sessionId) {
309
682
  dropOptimistic(null)
310
683
  releaseBusy()
311
- notify({ kind: 'error', title: 'Session unavailable', message: 'Could not create a new session' })
684
+ notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
312
685
 
313
686
  return false
314
687
  }
@@ -317,10 +690,47 @@ export function usePromptActions({
317
690
  }
318
691
 
319
692
  try {
320
- await syncImageAttachmentsForSubmit(sessionId, attachments, {
693
+ const syncedAttachments = await syncAttachmentsForSubmit(sessionId, attachments, {
321
694
  updateComposerAttachments: usingComposerAttachments
322
695
  })
323
- await requestGateway('prompt.submit', { session_id: sessionId, text })
696
+
697
+ // Rewrite the optimistic message + prompt text with the synced refs so
698
+ // the gateway receives @file: paths that resolve in its workspace.
699
+ // (Images keep their inline base64 preview — see optimisticAttachmentRef.)
700
+ attachmentRefs = syncedAttachments.map(optimisticAttachmentRef).filter((r): r is string => Boolean(r))
701
+ rewriteOptimistic(sessionId)
702
+ const text = buildContextText(syncedAttachments)
703
+
704
+ // On sleep/wake the gateway's in-memory session may have been cleared
705
+ // while the desktop app still holds the old session ID. Detect this,
706
+ // resume the stored session to re-register it, and retry once.
707
+ let submitErr: unknown = null
708
+
709
+ try {
710
+ await withSessionBusyRetry(() => requestGateway('prompt.submit', { session_id: sessionId, text }))
711
+ } catch (firstErr) {
712
+ if (isSessionNotFoundError(firstErr) && selectedStoredSessionIdRef.current) {
713
+ // Re-register the session in the gateway and get a fresh live ID.
714
+ const resumed = await requestGateway<{ session_id: string }>('session.resume', {
715
+ session_id: selectedStoredSessionIdRef.current
716
+ })
717
+
718
+ const recoveredId = resumed?.session_id
719
+
720
+ if (recoveredId) {
721
+ activeSessionIdRef.current = recoveredId
722
+ await withSessionBusyRetry(() => requestGateway('prompt.submit', { session_id: recoveredId, text }))
723
+ } else {
724
+ submitErr = firstErr
725
+ }
726
+ } else {
727
+ submitErr = firstErr
728
+ }
729
+ }
730
+
731
+ if (submitErr !== null) {
732
+ throw submitErr
733
+ }
324
734
 
325
735
  if (usingComposerAttachments) {
326
736
  clearComposerAttachments()
@@ -328,9 +738,17 @@ export function usePromptActions({
328
738
 
329
739
  return true
330
740
  } catch (err) {
331
- const message = inlineErrorMessage(err, 'Prompt failed')
332
-
333
741
  releaseBusy()
742
+
743
+ // A queued drain that raced a not-yet-settled turn gets a transient
744
+ // "session busy" (4009). Don't surface an error bubble/toast — the entry
745
+ // stays queued and the composer's bounded auto-drain retries when idle.
746
+ if (options?.fromQueue && isSessionBusyError(err)) {
747
+ return false
748
+ }
749
+
750
+ const message = inlineErrorMessage(err, copy.promptFailed)
751
+
334
752
  updateSessionState(sessionId, state => ({
335
753
  ...state,
336
754
  messages: [
@@ -339,7 +757,7 @@ export function usePromptActions({
339
757
  id: `assistant-error-${Date.now()}`,
340
758
  role: 'assistant',
341
759
  parts: [],
342
- error: message || 'Prompt failed',
760
+ error: message || copy.promptFailed,
343
761
  branchGroupId: state.pendingBranchGroup ?? undefined
344
762
  }
345
763
  ],
@@ -350,12 +768,12 @@ export function usePromptActions({
350
768
  }))
351
769
 
352
770
  if (isProviderSetupError(err)) {
353
- requestDesktopOnboarding('Add a provider credential before sending your first message.')
771
+ requestDesktopOnboarding(copy.providerCredentialRequired)
354
772
 
355
773
  return false
356
774
  }
357
775
 
358
- notifyError(err, 'Prompt failed')
776
+ notifyError(err, copy.promptFailed)
359
777
 
360
778
  return false
361
779
  }
@@ -363,82 +781,134 @@ export function usePromptActions({
363
781
  [
364
782
  activeSessionId,
365
783
  busyRef,
784
+ copy,
366
785
  createBackendSessionForSend,
367
786
  requestGateway,
368
787
  selectedStoredSessionIdRef,
369
- syncImageAttachmentsForSubmit,
788
+ syncAttachmentsForSubmit,
370
789
  updateSessionState
371
790
  ]
372
791
  )
373
792
 
374
- const executeSlashCommand = useCallback(
375
- async (rawCommand: string, options?: { sessionId?: string; recordInput?: boolean }) => {
376
- const runSlash = async (commandText: string, sessionHint?: string, recordInput = true): Promise<void> => {
377
- const command = commandText.trim()
378
- const { name, arg } = parseSlashCommand(command)
379
- const normalizedName = name.toLowerCase()
793
+ // Queue a handoff of this session to a messaging platform and watch it to
794
+ // a terminal state. We only write the request through the gateway; the
795
+ // separate `hermes gateway` process performs the actual transfer, so we
796
+ // poll `handoff.state` (mirror of the CLI's block-poll) for the result.
797
+ const handoffSession = useCallback(
798
+ async (
799
+ platform: string,
800
+ options?: { onProgress?: (state: string) => void; sessionId?: string }
801
+ ): Promise<HandoffResult> => {
802
+ const sid = options?.sessionId || activeSessionIdRef.current
380
803
 
381
- if (!name) {
382
- const sessionId = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
804
+ if (!sid) {
805
+ return { error: copy.sessionUnavailable, ok: false }
806
+ }
383
807
 
384
- if (sessionId) {
385
- appendSessionTextMessage(sessionId, 'system', 'empty slash command')
386
- }
808
+ const target = platform.trim().toLowerCase()
387
809
 
388
- return
389
- }
810
+ if (!target) {
811
+ return { error: copy.handoff.failed(''), ok: false }
812
+ }
390
813
 
391
- if (normalizedName === 'new' || normalizedName === 'reset') {
392
- startFreshSessionDraft()
814
+ try {
815
+ options?.onProgress?.('pending')
816
+ await requestGateway<HandoffRequestResponse>('handoff.request', {
817
+ platform: target,
818
+ session_id: sid
819
+ })
820
+ } catch (err) {
821
+ return { error: inlineErrorMessage(err, copy.handoff.failed(target)), ok: false }
822
+ }
393
823
 
394
- return
395
- }
824
+ const deadline = Date.now() + 60_000
825
+ let lastState = 'pending'
396
826
 
397
- if (normalizedName === 'branch' || normalizedName === 'fork') {
398
- await branchCurrentSession()
827
+ while (Date.now() < deadline) {
828
+ await delay(800)
399
829
 
400
- return
830
+ let record: HandoffStateResponse
831
+
832
+ try {
833
+ record = await requestGateway<HandoffStateResponse>('handoff.state', { session_id: sid })
834
+ } catch {
835
+ continue
401
836
  }
402
837
 
403
- if (normalizedName === 'skin' && !sessionHint && !activeSessionIdRef.current) {
404
- notify({ kind: 'success', message: handleSkinCommand(arg) })
838
+ const state = record.state || 'pending'
405
839
 
406
- return
840
+ if (state !== lastState) {
841
+ options?.onProgress?.(state)
842
+ lastState = state
407
843
  }
408
844
 
409
- const sessionId = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
845
+ if (state === 'completed') {
846
+ appendSessionTextMessage(sid, 'system', copy.handoff.systemNote(target))
847
+ notify({ kind: 'success', message: copy.handoff.success(target) })
410
848
 
411
- if (!sessionId) {
412
- notify({
413
- kind: 'error',
414
- title: 'Session unavailable',
415
- message: 'Could not create a new session'
416
- })
849
+ return { ok: true }
850
+ }
417
851
 
418
- return
852
+ if (state === 'failed') {
853
+ return { error: record.error || copy.handoff.failed(target), ok: false }
419
854
  }
855
+ }
420
856
 
421
- const renderSlashOutput = (text: string) =>
422
- appendSessionTextMessage(sessionId, 'system', recordInput ? slashStatusText(command, text) : text)
857
+ const cleanup = await requestGateway<HandoffFailResponse>('handoff.fail', {
858
+ error: copy.handoff.timedOut,
859
+ session_id: sid
860
+ }).catch(() => null)
423
861
 
424
- if (normalizedName === 'skin') {
425
- renderSlashOutput(handleSkinCommand(arg))
862
+ if (cleanup?.state === 'completed') {
863
+ appendSessionTextMessage(sid, 'system', copy.handoff.systemNote(target))
864
+ notify({ kind: 'success', message: copy.handoff.success(target) })
426
865
 
427
- return
866
+ return { ok: true }
867
+ }
868
+
869
+ return { error: copy.handoff.timedOut, ok: false }
870
+ },
871
+ [activeSessionIdRef, appendSessionTextMessage, copy, requestGateway]
872
+ )
873
+
874
+ const executeSlashCommand = useCallback(
875
+ async (rawCommand: string, options?: { sessionId?: string; recordInput?: boolean }) => {
876
+ const ensureSessionId = async (sessionHint?: string) =>
877
+ sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
878
+
879
+ // Resolve the target session plus a writer for inline slash output, or
880
+ // notify + return null when none can be created. Folds the ensure / bail /
881
+ // build-renderSlashOutput boilerplate every exec-style handler repeats.
882
+ const withSlashOutput = async (
883
+ ctx: SlashActionCtx
884
+ ): Promise<{ render: (text: string) => void; sessionId: string } | null> => {
885
+ const sessionId = await ensureSessionId(ctx.sessionHint)
886
+
887
+ if (!sessionId) {
888
+ notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
889
+
890
+ return null
428
891
  }
429
892
 
430
- if (name === 'help' || name === 'commands') {
431
- try {
432
- const catalog = await requestGateway<CommandsCatalogLike>('commands.catalog', { session_id: sessionId })
893
+ const render = (text: string) =>
894
+ appendSessionTextMessage(sessionId, 'system', ctx.recordInput ? slashStatusText(ctx.command, text) : text)
433
895
 
434
- renderSlashOutput(renderCommandsCatalog(catalog))
435
- } catch (err) {
436
- renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
437
- }
896
+ return { render, sessionId }
897
+ }
898
+
899
+ // `exec` commands (and unknown skill / quick commands the backend owns)
900
+ // run on the gateway and render their text output inline. This is the only
901
+ // path that talks to slash.exec / command.dispatch.
902
+ async function runExec(ctx: SlashActionCtx): Promise<void> {
903
+ const { arg, command, name } = ctx
904
+ const resolved = await withSlashOutput(ctx)
438
905
 
906
+ if (!resolved) {
439
907
  return
440
908
  }
441
909
 
910
+ const { render: renderSlashOutput, sessionId } = resolved
911
+
442
912
  if (!isDesktopSlashCommand(name)) {
443
913
  renderSlashOutput(desktopSlashUnavailableMessage(name) || `/${name} is not available in the desktop app.`)
444
914
 
@@ -461,11 +931,7 @@ export function usePromptActions({
461
931
 
462
932
  try {
463
933
  const dispatch = parseCommandDispatch(
464
- await requestGateway<unknown>('command.dispatch', {
465
- session_id: sessionId,
466
- name,
467
- arg
468
- })
934
+ await requestGateway<unknown>('command.dispatch', { session_id: sessionId, name, arg })
469
935
  )
470
936
 
471
937
  if (!dispatch) {
@@ -486,8 +952,26 @@ export function usePromptActions({
486
952
  return
487
953
  }
488
954
 
955
+ // send / prefill carry an optional `notice` (e.g. "⊙ Goal set …")
956
+ // that the backend wants shown as a system line before the message
957
+ // is acted on. Mirrors the TUI's createSlashHandler — without it a
958
+ // `/goal <text>` looked like it did nothing.
959
+ if ((dispatch.type === 'send' || dispatch.type === 'prefill') && dispatch.notice?.trim()) {
960
+ renderSlashOutput(dispatch.notice.trim())
961
+ }
962
+
489
963
  const message = ('message' in dispatch ? dispatch.message : '')?.trim() ?? ''
490
964
 
965
+ // /undo returns a prefill directive: drop the backed-up message into
966
+ // the composer for editing instead of submitting it immediately.
967
+ if (dispatch.type === 'prefill') {
968
+ if (message) {
969
+ setComposerDraft(message)
970
+ }
971
+
972
+ return
973
+ }
974
+
491
975
  if (!message) {
492
976
  renderSlashOutput(
493
977
  `/${name}: ${dispatch.type === 'skill' ? 'skill payload missing message' : 'empty message'}`
@@ -512,6 +996,336 @@ export function usePromptActions({
512
996
  }
513
997
  }
514
998
 
999
+ // One handler per `action` command. Adding a desktop-native command is a
1000
+ // registry row in desktop-slash-commands.ts plus an entry here — never a
1001
+ // new branch in a dispatch ladder.
1002
+ const actionHandlers: Record<DesktopActionId, (ctx: SlashActionCtx) => Promise<void>> = {
1003
+ new: async () => {
1004
+ startFreshSessionDraft()
1005
+ },
1006
+ branch: async () => {
1007
+ await branchCurrentSession()
1008
+ },
1009
+ // /yolo maps to the status-bar YOLO control — a per-session approval
1010
+ // bypass, same scope as the TUI's Shift+Tab. With no session yet we arm
1011
+ // it locally; the session-create path applies it on the first message.
1012
+ yolo: async ({ sessionHint }) => {
1013
+ const sid = sessionHint || activeSessionIdRef.current
1014
+ const next = !$yoloActive.get()
1015
+
1016
+ if (!sid) {
1017
+ setYoloActive(next)
1018
+ notify({ kind: 'success', message: next ? copy.yoloArmed : copy.yoloOff })
1019
+
1020
+ return
1021
+ }
1022
+
1023
+ try {
1024
+ const active = await setSessionYolo(requestGateway, sid, next)
1025
+ appendSessionTextMessage(sid, 'system', copy.yoloSystem(active))
1026
+ } catch {
1027
+ notify({ kind: 'error', title: copy.yoloTitle, message: copy.yoloToggleFailed })
1028
+ }
1029
+ },
1030
+ // /handoff hands this session to a messaging platform. The platform is
1031
+ // completed inline in the slash popover (backend _handoff_completions),
1032
+ // so there is no overlay: `/handoff <platform>` runs the desktop's own
1033
+ // handoff RPC. cli_only on the backend, so it must not reach slash.exec.
1034
+ handoff: async ({ arg, command, recordInput, sessionHint }) => {
1035
+ const platform = arg.trim()
1036
+
1037
+ if (!platform) {
1038
+ notify({ kind: 'success', message: copy.handoff.pickPlatform })
1039
+
1040
+ return
1041
+ }
1042
+
1043
+ const sid = sessionHint || activeSessionIdRef.current
1044
+
1045
+ if (!sid) {
1046
+ notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
1047
+
1048
+ return
1049
+ }
1050
+
1051
+ const result = await handoffSession(platform, { sessionId: sid })
1052
+
1053
+ if (!result.ok && result.error) {
1054
+ appendSessionTextMessage(sid, 'system', recordInput ? slashStatusText(command, result.error) : result.error)
1055
+ }
1056
+ },
1057
+ // /profile selects which profile new chats open in — no app relaunch.
1058
+ // A profile is per-session now, so an existing thread can't change its
1059
+ // profile mid-stream; `/profile <name>` points the next new chat (and
1060
+ // the current empty draft) at that profile's backend.
1061
+ profile: async ({ arg }) => {
1062
+ const target = arg.trim()
1063
+ const current = normalizeProfileKey($activeGatewayProfile.get())
1064
+
1065
+ if (!target) {
1066
+ notify({ kind: 'success', message: copy.profileStatus(current) })
1067
+
1068
+ return
1069
+ }
1070
+
1071
+ try {
1072
+ const { profiles } = await getProfiles()
1073
+ const match = profiles.find(profile => profile.name === target)
1074
+
1075
+ if (!match) {
1076
+ notify({
1077
+ kind: 'error',
1078
+ title: copy.unknownProfile,
1079
+ message: copy.noProfileNamed(target, profiles.map(profile => profile.name).join(', '))
1080
+ })
1081
+
1082
+ return
1083
+ }
1084
+
1085
+ const key = normalizeProfileKey(match.name)
1086
+
1087
+ $newChatProfile.set(key)
1088
+ await ensureGatewayProfile(key)
1089
+ notify({ kind: 'success', message: copy.newChatsProfile(match.name) })
1090
+ } catch (err) {
1091
+ notifyError(err, copy.setProfileFailed)
1092
+ }
1093
+ },
1094
+ skin: async ({ arg, command, recordInput, sessionHint }) => {
1095
+ const sid = sessionHint || activeSessionIdRef.current
1096
+ const message = handleSkinCommand(arg)
1097
+
1098
+ // No session to print into yet — surface it as a toast instead of
1099
+ // spinning up a backend session just to change the theme.
1100
+ if (!sid) {
1101
+ notify({ kind: 'success', message })
1102
+
1103
+ return
1104
+ }
1105
+
1106
+ appendSessionTextMessage(sid, 'system', recordInput ? slashStatusText(command, message) : message)
1107
+ },
1108
+ // /title <name> renames via the gateway's session.title RPC — the same
1109
+ // path the TUI uses, NOT REST renameSession (which 404s on runtime ids)
1110
+ // nor the slash worker (whose DB write can silently fail). Bare /title
1111
+ // shows the current title, which the worker owns, so delegate to exec.
1112
+ title: async ctx => {
1113
+ if (!ctx.arg) {
1114
+ await runExec(ctx)
1115
+
1116
+ return
1117
+ }
1118
+
1119
+ const resolved = await withSlashOutput(ctx)
1120
+
1121
+ if (!resolved) {
1122
+ return
1123
+ }
1124
+
1125
+ const { render: renderSlashOutput, sessionId } = resolved
1126
+ const { arg } = ctx
1127
+
1128
+ try {
1129
+ const result = await requestGateway<SessionTitleResponse>('session.title', {
1130
+ session_id: sessionId,
1131
+ title: arg
1132
+ })
1133
+
1134
+ const finalTitle = (result?.title || arg).trim()
1135
+ const queued = result?.pending === true
1136
+
1137
+ setSessions(prev => prev.map(s => (s.id === sessionId ? { ...s, title: finalTitle || null } : s)))
1138
+ await refreshSessions().catch(() => undefined)
1139
+ renderSlashOutput(
1140
+ finalTitle
1141
+ ? `Session title set: ${finalTitle}${queued ? ' (queued while session initializes)' : ''}`
1142
+ : 'Session title cleared.'
1143
+ )
1144
+ } catch (err) {
1145
+ renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
1146
+ }
1147
+ },
1148
+ help: async ctx => {
1149
+ const resolved = await withSlashOutput(ctx)
1150
+
1151
+ if (!resolved) {
1152
+ return
1153
+ }
1154
+
1155
+ const { render: renderSlashOutput, sessionId } = resolved
1156
+
1157
+ try {
1158
+ const catalog = await requestGateway<CommandsCatalogLike>('commands.catalog', { session_id: sessionId })
1159
+
1160
+ renderSlashOutput(renderCommandsCatalog(catalog, copy))
1161
+ } catch (err) {
1162
+ renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
1163
+ }
1164
+ },
1165
+ // /browser connect|disconnect|status manages the live CDP connection on
1166
+ // the gateway host, mirroring the TUI's browser.manage RPC. It mutates
1167
+ // BROWSER_CDP_URL (and may launch Chrome) in the gateway process — only
1168
+ // meaningful when that process runs on this machine, so it's gated to
1169
+ // local connections. A remote gateway would act on the wrong host.
1170
+ browser: async ctx => {
1171
+ const resolved = await withSlashOutput(ctx)
1172
+
1173
+ if (!resolved) {
1174
+ return
1175
+ }
1176
+
1177
+ const { render: renderSlashOutput, sessionId } = resolved
1178
+
1179
+ if ($connection.get()?.mode === 'remote') {
1180
+ renderSlashOutput(
1181
+ '/browser manages a Chromium-family browser on the gateway host — only available when connected to a local gateway.'
1182
+ )
1183
+
1184
+ return
1185
+ }
1186
+
1187
+ const [rawAction = 'status', ...rest] = ctx.arg.trim().split(/\s+/).filter(Boolean)
1188
+ const cmdAction = rawAction.toLowerCase()
1189
+
1190
+ if (!['connect', 'disconnect', 'status'].includes(cmdAction)) {
1191
+ renderSlashOutput(
1192
+ 'usage: /browser [connect|disconnect|status] [url] · persistent: set browser.cdp_url in config.yaml'
1193
+ )
1194
+
1195
+ return
1196
+ }
1197
+
1198
+ const url = cmdAction === 'connect' ? rest.join(' ').trim() || 'http://127.0.0.1:9222' : undefined
1199
+
1200
+ if (url) {
1201
+ renderSlashOutput(`checking Chromium-family browser remote debugging at ${url}...`)
1202
+ }
1203
+
1204
+ try {
1205
+ const result = await requestGateway<BrowserManageResponse>('browser.manage', {
1206
+ action: cmdAction,
1207
+ session_id: sessionId,
1208
+ ...(url && { url })
1209
+ })
1210
+
1211
+ // Without a streamed session subscription, the gateway bundles its
1212
+ // progress lines into `messages` — flush them inline.
1213
+ result?.messages?.forEach(message => renderSlashOutput(message))
1214
+
1215
+ if (cmdAction === 'status') {
1216
+ renderSlashOutput(
1217
+ result?.connected
1218
+ ? `browser connected: ${result.url || '(url unavailable)'}`
1219
+ : 'browser not connected (try /browser connect <url> or set browser.cdp_url in config.yaml)'
1220
+ )
1221
+
1222
+ return
1223
+ }
1224
+
1225
+ if (cmdAction === 'disconnect') {
1226
+ renderSlashOutput('browser disconnected')
1227
+
1228
+ return
1229
+ }
1230
+
1231
+ if (result?.connected) {
1232
+ renderSlashOutput('Browser connected to live Chromium-family browser via CDP')
1233
+ renderSlashOutput(`Endpoint: ${result.url || '(url unavailable)'}`)
1234
+ renderSlashOutput('next browser tool call will use this CDP endpoint')
1235
+ }
1236
+ } catch (err) {
1237
+ renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ // Picker commands open a desktop overlay; a typed arg is resolved by that
1243
+ // picker so the command never dead-ends or falls through to the backend.
1244
+ const openPicker = async (pickerId: DesktopPickerId, ctx: SlashActionCtx): Promise<void> => {
1245
+ if (pickerId === 'model') {
1246
+ if (!ctx.arg.trim()) {
1247
+ setModelPickerOpen(true)
1248
+
1249
+ return
1250
+ }
1251
+
1252
+ // Power users can still type `/model <name>` — run it on the backend.
1253
+ await runExec(ctx)
1254
+
1255
+ return
1256
+ }
1257
+
1258
+ // session picker — /resume, /sessions, /switch
1259
+ const query = ctx.arg.trim()
1260
+
1261
+ if (!query) {
1262
+ setSessionPickerOpen(true)
1263
+
1264
+ return
1265
+ }
1266
+
1267
+ const sessions = $sessions.get()
1268
+ const lower = query.toLowerCase()
1269
+
1270
+ const match =
1271
+ sessions.find(session => session.id === query) ||
1272
+ sessions.find(session => sessionTitle(session).toLowerCase().includes(lower)) ||
1273
+ sessions.find(session => (session.preview ?? '').toLowerCase().includes(lower))
1274
+
1275
+ if (!match) {
1276
+ if (isSessionIdCandidate(query)) {
1277
+ await resumeStoredSession(query)
1278
+
1279
+ return
1280
+ }
1281
+
1282
+ notify({ kind: 'error', message: copy.resumeFailed })
1283
+
1284
+ return
1285
+ }
1286
+
1287
+ await resumeStoredSession(match.id)
1288
+ }
1289
+
1290
+ // The whole dispatcher: resolve the command's desktop surface, then act on
1291
+ // its kind. No per-command ladder — behavior lives in the registry.
1292
+ async function runSlash(commandText: string, sessionHint?: string, recordInput = true): Promise<void> {
1293
+ const command = commandText.trim()
1294
+ const { name, arg } = parseSlashCommand(command)
1295
+
1296
+ if (!name) {
1297
+ const sessionId = await ensureSessionId(sessionHint)
1298
+
1299
+ if (sessionId) {
1300
+ appendSessionTextMessage(sessionId, 'system', copy.emptySlashCommand)
1301
+ }
1302
+
1303
+ return
1304
+ }
1305
+
1306
+ const ctx: SlashActionCtx = { arg, command, name, recordInput, sessionHint }
1307
+ const surface = resolveDesktopCommand(`/${name}`)?.surface
1308
+
1309
+ switch (surface?.kind) {
1310
+ case 'unavailable': {
1311
+ const resolved = await withSlashOutput(ctx)
1312
+ resolved?.render(desktopSlashUnavailableMessage(name) || `/${name} is not available in the desktop app.`)
1313
+
1314
+ return
1315
+ }
1316
+
1317
+ case 'picker':
1318
+ return openPicker(surface.picker, ctx)
1319
+
1320
+ case 'action':
1321
+ return actionHandlers[surface.action](ctx)
1322
+
1323
+ default:
1324
+ // exec spec, or an unknown skill / quick command the backend owns.
1325
+ return runExec(ctx)
1326
+ }
1327
+ }
1328
+
515
1329
  await runSlash(rawCommand, options?.sessionId, options?.recordInput ?? true)
516
1330
  },
517
1331
  [
@@ -519,9 +1333,13 @@ export function usePromptActions({
519
1333
  appendSessionTextMessage,
520
1334
  branchCurrentSession,
521
1335
  busyRef,
1336
+ copy,
522
1337
  createBackendSessionForSend,
523
1338
  handleSkinCommand,
1339
+ handoffSession,
1340
+ refreshSessions,
524
1341
  requestGateway,
1342
+ resumeStoredSession,
525
1343
  startFreshSessionDraft,
526
1344
  submitPromptText
527
1345
  ]
@@ -547,7 +1365,7 @@ export function usePromptActions({
547
1365
  const transcribeVoiceAudio = useCallback(
548
1366
  async (audio: Blob) => {
549
1367
  if (!sttEnabled) {
550
- throw new Error('Speech-to-text is disabled in settings.')
1368
+ throw new Error(copy.sttDisabled)
551
1369
  }
552
1370
 
553
1371
  const dataUrl = await blobToDataUrl(audio)
@@ -555,30 +1373,30 @@ export function usePromptActions({
555
1373
 
556
1374
  return result.transcript
557
1375
  },
558
- [sttEnabled]
1376
+ [copy.sttDisabled, sttEnabled]
559
1377
  )
560
1378
 
561
1379
  const cancelRun = useCallback(async () => {
562
1380
  const sessionId = activeSessionId || activeSessionIdRef.current
1381
+ const releaseBusy = () => {
1382
+ setMutableRef(busyRef, false)
1383
+ setBusy(false)
1384
+ }
563
1385
 
564
- busyRef.current = false
565
- setBusy(false)
566
1386
  setAwaitingResponse(false)
567
1387
 
568
- const finalizeMessages = (messages: ChatMessage[]) =>
569
- messages.map(message =>
570
- message.pending
571
- ? {
572
- ...message,
573
- parts: chatMessageText(message).trim()
574
- ? appendTextPart(message.parts, INTERRUPTED_MARKER)
575
- : [...message.parts, textPart(INTERRUPTED_MARKER.trim())],
576
- pending: false
577
- }
578
- : message
579
- )
1388
+ const finalizeMessages = (messages: ChatMessage[], streamId?: string | null) =>
1389
+ messages
1390
+ .filter(
1391
+ message =>
1392
+ !((message.pending || message.id === streamId) && !chatMessageText(message).trim())
1393
+ )
1394
+ .map(message =>
1395
+ message.pending || message.id === streamId ? { ...message, pending: false } : message
1396
+ )
580
1397
 
581
1398
  if (!sessionId) {
1399
+ releaseBusy()
582
1400
  setMessages(finalizeMessages($messages.get()))
583
1401
 
584
1402
  return
@@ -586,20 +1404,7 @@ export function usePromptActions({
586
1404
 
587
1405
  updateSessionState(sessionId, state => {
588
1406
  const streamId = state.streamId
589
-
590
- const messages = streamId
591
- ? state.messages.map(message =>
592
- message.id === streamId
593
- ? {
594
- ...message,
595
- parts: chatMessageText(message).trim()
596
- ? appendTextPart(message.parts, INTERRUPTED_MARKER)
597
- : [...message.parts, textPart(INTERRUPTED_MARKER.trim())],
598
- pending: false
599
- }
600
- : message
601
- )
602
- : finalizeMessages(state.messages)
1407
+ const messages = finalizeMessages(state.messages, streamId)
603
1408
 
604
1409
  return {
605
1410
  ...state,
@@ -612,12 +1417,82 @@ export function usePromptActions({
612
1417
  }
613
1418
  })
614
1419
 
1420
+ clearSessionTodos(sessionId)
1421
+ clearSessionSubagents(sessionId)
1422
+ resetSessionBackground(sessionId)
1423
+
615
1424
  try {
616
1425
  await requestGateway('session.interrupt', { session_id: sessionId })
1426
+ releaseBusy()
617
1427
  } catch (err) {
618
- notifyError(err, 'Stop failed')
1428
+ let stopError = err
1429
+
1430
+ if (isSessionNotFoundError(err) && selectedStoredSessionIdRef.current) {
1431
+ try {
1432
+ const resumed = await requestGateway<{ session_id: string }>('session.resume', {
1433
+ session_id: selectedStoredSessionIdRef.current
1434
+ })
1435
+
1436
+ const recoveredId = resumed?.session_id
1437
+
1438
+ if (recoveredId) {
1439
+ activeSessionIdRef.current = recoveredId
1440
+ await requestGateway('session.interrupt', { session_id: recoveredId })
1441
+ releaseBusy()
1442
+
1443
+ return
1444
+ }
1445
+ } catch (resumeErr) {
1446
+ stopError = resumeErr
1447
+ }
1448
+ }
1449
+
1450
+ releaseBusy()
1451
+ notifyError(stopError, copy.stopFailed)
619
1452
  }
620
- }, [activeSessionId, activeSessionIdRef, busyRef, requestGateway, updateSessionState])
1453
+ }, [
1454
+ activeSessionId,
1455
+ activeSessionIdRef,
1456
+ busyRef,
1457
+ copy.stopFailed,
1458
+ requestGateway,
1459
+ selectedStoredSessionIdRef,
1460
+ updateSessionState
1461
+ ])
1462
+
1463
+ // Steer = nudge the live turn without interrupting: the gateway appends the
1464
+ // text to the next tool result so the model reads it on its next iteration
1465
+ // (desktop parity with `/steer`). Returns false on reject (no live tool
1466
+ // window) so the caller can fall back to queueing the words for the next turn.
1467
+ const steerPrompt = useCallback(
1468
+ async (rawText: string): Promise<boolean> => {
1469
+ const text = rawText.trim()
1470
+ const sessionId = activeSessionId || activeSessionIdRef.current
1471
+
1472
+ if (!text || !sessionId) {
1473
+ return false
1474
+ }
1475
+
1476
+ try {
1477
+ const result = await requestGateway<SessionSteerResponse>('session.steer', { session_id: sessionId, text })
1478
+
1479
+ if (result?.status === 'queued') {
1480
+ triggerHaptic('submit')
1481
+ // Inline note (not a toast) so the nudge lives in the transcript next
1482
+ // to the turn it steered. The `steer:` prefix is rendered as a codicon
1483
+ // row by SystemMessage (see STEER_NOTE_RE), same style as slash output.
1484
+ appendSessionTextMessage(sessionId, 'system', `steer:${text}`)
1485
+
1486
+ return true
1487
+ }
1488
+ } catch {
1489
+ // Swallow — caller queues the text so nothing is lost.
1490
+ }
1491
+
1492
+ return false
1493
+ },
1494
+ [activeSessionId, activeSessionIdRef, appendSessionTextMessage, requestGateway]
1495
+ )
621
1496
 
622
1497
  const reloadFromMessage = useCallback(
623
1498
  async (parentId: string | null) => {
@@ -689,10 +1564,98 @@ export function usePromptActions({
689
1564
  busy: false,
690
1565
  awaitingResponse: false
691
1566
  }))
692
- notifyError(err, 'Regenerate failed')
1567
+ notifyError(err, copy.regenerateFailed)
1568
+ }
1569
+ },
1570
+ [activeSessionId, copy.regenerateFailed, requestGateway, updateSessionState]
1571
+ )
1572
+
1573
+ // Cursor-style "restore checkpoint": rewind the conversation to a past user
1574
+ // prompt and run it again from there. Reuses the edit composer's rewind
1575
+ // mechanism — `prompt.submit` with `truncate_before_user_ordinal` drops that
1576
+ // user turn and everything after it from the session history, then the same
1577
+ // text is submitted as a fresh turn. Callers confirm before invoking; errors
1578
+ // are rethrown so the confirmation dialog can surface them inline.
1579
+ // Submit a rewind (truncate-before-ordinal + resubmit). Because edit/restore
1580
+ // can fire while a turn is streaming, interrupt the live turn first — the
1581
+ // cooperative interrupt takes a beat, so the shared busy-retry rides it out.
1582
+ const submitRewindPrompt = useCallback(
1583
+ async (sessionId: string, text: string, truncateOrdinal: number | undefined, wasRunning: boolean) => {
1584
+ if (wasRunning) {
1585
+ try {
1586
+ await requestGateway('session.interrupt', { session_id: sessionId })
1587
+ } catch {
1588
+ // Best-effort — the busy-retry below still gates the submit.
1589
+ }
1590
+ }
1591
+
1592
+ await withSessionBusyRetry(() =>
1593
+ requestGateway('prompt.submit', {
1594
+ session_id: sessionId,
1595
+ text,
1596
+ ...(truncateOrdinal !== undefined && { truncate_before_user_ordinal: truncateOrdinal })
1597
+ })
1598
+ )
1599
+ },
1600
+ [requestGateway]
1601
+ )
1602
+
1603
+ const restoreToMessage = useCallback(
1604
+ async (messageId: string) => {
1605
+ const sessionId = activeSessionId || activeSessionIdRef.current
1606
+
1607
+ if (!sessionId) {
1608
+ return
1609
+ }
1610
+
1611
+ const messages = $messages.get()
1612
+ const sourceIndex = messages.findIndex(m => m.id === messageId)
1613
+ const source = messages[sourceIndex]
1614
+
1615
+ if (!source || source.role !== 'user') {
1616
+ return
1617
+ }
1618
+
1619
+ const text = chatMessageText(source).trim()
1620
+
1621
+ if (!text) {
1622
+ return
1623
+ }
1624
+
1625
+ const wasRunning = $busy.get()
1626
+ const truncateBeforeUserOrdinal = visibleUserOrdinal(messages, sourceIndex)
1627
+
1628
+ // The turns we're discarding may have spawned todos and background
1629
+ // processes; they belong to the abandoned timeline, so wipe their status
1630
+ // rows (and kill the live processes) before the fresh run repopulates.
1631
+ clearSessionTodos(sessionId)
1632
+ resetSessionBackground(sessionId)
1633
+
1634
+ clearNotifications()
1635
+ setMutableRef(busyRef, true)
1636
+ setBusy(true)
1637
+ setAwaitingResponse(true)
1638
+ updateSessionState(sessionId, state => ({
1639
+ ...state,
1640
+ busy: true,
1641
+ awaitingResponse: true,
1642
+ pendingBranchGroup: null,
1643
+ sawAssistantPayload: false,
1644
+ interrupted: false,
1645
+ messages: state.messages.slice(0, sourceIndex + 1)
1646
+ }))
1647
+
1648
+ try {
1649
+ await submitRewindPrompt(sessionId, text, truncateBeforeUserOrdinal, wasRunning)
1650
+ } catch (err) {
1651
+ setMutableRef(busyRef, false)
1652
+ setBusy(false)
1653
+ setAwaitingResponse(false)
1654
+ updateSessionState(sessionId, state => ({ ...state, busy: false, awaitingResponse: false }))
1655
+ throw err
693
1656
  }
694
1657
  },
695
- [activeSessionId, requestGateway, updateSessionState]
1658
+ [activeSessionId, activeSessionIdRef, busyRef, submitRewindPrompt, updateSessionState]
696
1659
  )
697
1660
 
698
1661
  const editMessage = useCallback(
@@ -701,7 +1664,7 @@ export function usePromptActions({
701
1664
  const sourceId = edited.sourceId || edited.parentId
702
1665
  const text = appendText(edited)
703
1666
 
704
- if (!sessionId || !sourceId || !text || edited.role !== 'user' || $busy.get()) {
1667
+ if (!sessionId || !sourceId || !text || edited.role !== 'user') {
705
1668
  return
706
1669
  }
707
1670
 
@@ -713,14 +1676,25 @@ export function usePromptActions({
713
1676
  return
714
1677
  }
715
1678
 
1679
+ // Sending an edit is a revert: rewind to this prompt and re-run with the
1680
+ // new text. It can fire mid-turn, so capture the live state — the submit
1681
+ // helper interrupts first when a turn is running.
1682
+ const wasRunning = $busy.get()
1683
+
716
1684
  // Failed turn: optimistic user msg never reached the gateway, so truncating
717
1685
  // by ordinal would 422. Submit as a plain resend instead.
718
1686
  const nextMessage = messages[sourceIndex + 1]
719
1687
  const isFailedTurn = nextMessage?.role === 'assistant' && Boolean(nextMessage.error)
720
1688
  const editedMessage: ChatMessage = { ...source, parts: [textPart(text)] }
721
1689
 
1690
+ // Editing rewinds the conversation to this prompt — same as restore — so
1691
+ // drop the abandoned timeline's todos/background rows (and kill the live
1692
+ // processes) before the re-run repopulates them.
1693
+ clearSessionTodos(sessionId)
1694
+ resetSessionBackground(sessionId)
1695
+
722
1696
  clearNotifications()
723
- busyRef.current = true
1697
+ setMutableRef(busyRef, true)
724
1698
  setBusy(true)
725
1699
  setAwaitingResponse(true)
726
1700
  updateSessionState(sessionId, state => ({
@@ -733,24 +1707,18 @@ export function usePromptActions({
733
1707
  messages: [...state.messages.slice(0, sourceIndex), editedMessage]
734
1708
  }))
735
1709
 
736
- const submit = (truncateOrdinal?: number) =>
737
- requestGateway('prompt.submit', {
738
- session_id: sessionId,
739
- text,
740
- ...(truncateOrdinal !== undefined && { truncate_before_user_ordinal: truncateOrdinal })
741
- })
742
-
743
1710
  const isStaleTargetError = (err: unknown) =>
744
1711
  /no longer in session history|not in session history/i.test(err instanceof Error ? err.message : String(err))
745
1712
 
746
1713
  try {
747
- await submit(isFailedTurn ? undefined : visibleUserOrdinal(messages, sourceIndex))
1714
+ await submitRewindPrompt(sessionId, text, isFailedTurn ? undefined : visibleUserOrdinal(messages, sourceIndex), wasRunning)
748
1715
  } catch (err) {
749
1716
  let surfaced = err
750
1717
 
751
1718
  if (!isFailedTurn && isStaleTargetError(err)) {
752
1719
  try {
753
- await submit()
1720
+ // Already interrupted on the first attempt — submit as a plain resend.
1721
+ await submitRewindPrompt(sessionId, text, undefined, false)
754
1722
 
755
1723
  return
756
1724
  } catch (retryErr) {
@@ -758,14 +1726,14 @@ export function usePromptActions({
758
1726
  }
759
1727
  }
760
1728
 
761
- busyRef.current = false
1729
+ setMutableRef(busyRef, false)
762
1730
  setBusy(false)
763
1731
  setAwaitingResponse(false)
764
1732
  updateSessionState(sessionId, state => ({ ...state, busy: false, awaitingResponse: false }))
765
- notifyError(surfaced, 'Edit failed')
1733
+ notifyError(surfaced, copy.editFailed)
766
1734
  }
767
1735
  },
768
- [activeSessionId, activeSessionIdRef, busyRef, requestGateway, updateSessionState]
1736
+ [activeSessionId, activeSessionIdRef, busyRef, copy.editFailed, submitRewindPrompt, updateSessionState]
769
1737
  )
770
1738
 
771
1739
  const handleThreadMessagesChange = useCallback(
@@ -802,5 +1770,15 @@ export function usePromptActions({
802
1770
  [activeSessionIdRef, updateSessionState]
803
1771
  )
804
1772
 
805
- return { cancelRun, editMessage, handleThreadMessagesChange, reloadFromMessage, submitText, transcribeVoiceAudio }
1773
+ return {
1774
+ cancelRun,
1775
+ editMessage,
1776
+ handleThreadMessagesChange,
1777
+ handoffSession,
1778
+ reloadFromMessage,
1779
+ restoreToMessage,
1780
+ steerPrompt,
1781
+ submitText,
1782
+ transcribeVoiceAudio
1783
+ }
806
1784
  }