@pheem49/mint 1.14.0 → 1.16.0

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 (925) hide show
  1. package/Cargo.lock +1258 -35
  2. package/Cargo.toml +5 -2
  3. package/LICENSE +21 -654
  4. package/README.md +93 -756
  5. package/Release_Note.md +53 -446
  6. package/apps/mint-companion/src-tauri/Cargo.toml +20 -0
  7. package/apps/mint-companion/src-tauri/build.rs +3 -0
  8. package/apps/mint-companion/src-tauri/src/main.rs +468 -0
  9. package/apps/mint-companion/tsconfig.json +20 -0
  10. package/apps/mint-companion/vite.config.ts +3 -0
  11. package/components.json +19 -0
  12. package/crates/mint-cli/Cargo.toml +9 -1
  13. package/crates/mint-cli/src/agent/approval_prompts.rs +16 -3
  14. package/crates/mint-cli/src/agent/live_status.rs +545 -148
  15. package/crates/mint-cli/src/agent/markdown_render.rs +601 -61
  16. package/crates/mint-cli/src/agent.rs +723 -258
  17. package/crates/mint-cli/src/commands/agent.rs +83 -74
  18. package/crates/mint-cli/src/commands/eval.rs +9 -5
  19. package/crates/mint-cli/src/commands/git.rs +204 -0
  20. package/crates/mint-cli/src/commands/integrations.rs +35 -0
  21. package/crates/mint-cli/src/commands/knowledge.rs +13 -32
  22. package/crates/mint-cli/src/commands/mod.rs +81 -2
  23. package/crates/mint-cli/src/commands/tailnet_web.rs +235 -0
  24. package/crates/mint-cli/src/image.rs +32 -11
  25. package/crates/mint-cli/src/interactive/chat_tui.rs +7853 -0
  26. package/crates/mint-cli/src/interactive/command_ui.rs +477 -0
  27. package/crates/mint-cli/src/interactive/format.rs +8 -0
  28. package/crates/mint-cli/src/interactive/input_box.rs +94 -8
  29. package/crates/mint-cli/src/interactive/picker.rs +180 -37
  30. package/crates/mint-cli/src/interactive/resume_picker.rs +686 -0
  31. package/crates/mint-cli/src/interactive/slash_commands.rs +1071 -501
  32. package/crates/mint-cli/src/interactive/thought_viewer.rs +286 -0
  33. package/crates/mint-cli/src/interactive/tool_activity.rs +328 -0
  34. package/crates/mint-cli/src/interactive.rs +454 -166
  35. package/crates/mint-cli/src/main.rs +93 -26
  36. package/crates/mint-cli/src/markdown.rs +12 -5
  37. package/crates/mint-cli/src/mcp.rs +5 -0
  38. package/crates/mint-cli/src/mod_host.rs +6 -0
  39. package/crates/mint-cli/src/mods_cli.rs +62 -0
  40. package/crates/mint-cli/src/mods_ui.rs +187 -0
  41. package/crates/mint-cli/src/onboard.rs +37 -61
  42. package/crates/mint-cli/src/plugins_cli.rs +8 -5
  43. package/crates/mint-cli/src/setup.rs +10 -8
  44. package/crates/mint-cli/src/terminal_theme.rs +189 -0
  45. package/crates/mint-cli/src/updater.rs +112 -23
  46. package/crates/mint-cli/tests/library_intake.rs +77 -0
  47. package/crates/mint-cli/tests/mods_testing.rs +133 -0
  48. package/crates/mint-companion-protocol/Cargo.toml +10 -0
  49. package/crates/mint-companion-protocol/src/lib.rs +163 -0
  50. package/crates/mint-core/Cargo.toml +6 -3
  51. package/crates/mint-core/build.rs +26 -0
  52. package/crates/mint-core/src/agent/agent_loop.rs +1 -0
  53. package/crates/mint-core/src/agent/chat.rs +1003 -209
  54. package/crates/mint-core/src/agent/code_tools.rs +98 -2
  55. package/crates/mint-core/src/agent/memory/sources.rs +240 -0
  56. package/crates/mint-core/src/agent/memory.rs +805 -33
  57. package/crates/mint-core/src/agent/skill_inventory.rs +325 -0
  58. package/crates/mint-core/src/agent/skills.rs +437 -90
  59. package/crates/mint-core/src/agent.rs +1 -0
  60. package/crates/mint-core/src/api_server/routes/background_jobs.rs +310 -0
  61. package/crates/mint-core/src/api_server/routes/chat.rs +233 -190
  62. package/crates/mint-core/src/api_server/routes/html_preview.rs +77 -0
  63. package/crates/mint-core/src/api_server/routes/library.rs +97 -0
  64. package/crates/mint-core/src/api_server/routes/linked_folders.rs +118 -3
  65. package/crates/mint-core/src/api_server/routes/media_gen.rs +10 -57
  66. package/crates/mint-core/src/api_server/routes/misc.rs +114 -121
  67. package/crates/mint-core/src/api_server/routes/mod.rs +6 -0
  68. package/crates/mint-core/src/api_server/routes/mods.rs +31 -0
  69. package/crates/mint-core/src/api_server/routes/sessions.rs +85 -0
  70. package/crates/mint-core/src/api_server/routes/skills_subagents.rs +12 -42
  71. package/crates/mint-core/src/api_server/routes/slash.rs +50 -1
  72. package/crates/mint-core/src/api_server/routes/sources.rs +101 -0
  73. package/crates/mint-core/src/api_server.rs +140 -12
  74. package/crates/mint-core/src/avatar_bridge.rs +13 -2
  75. package/crates/mint-core/src/browser/cdp.rs +74 -83
  76. package/crates/mint-core/src/browser/evidence.rs +260 -0
  77. package/crates/mint-core/src/browser/input.rs +1 -1
  78. package/crates/mint-core/src/browser/interact.rs +98 -124
  79. package/crates/mint-core/src/browser/lifecycle.rs +75 -4
  80. package/crates/mint-core/src/browser/mod.rs +16 -2
  81. package/crates/mint-core/src/browser/navigate.rs +27 -19
  82. package/crates/mint-core/src/browser/observe.js +40 -0
  83. package/crates/mint-core/src/browser/session.rs +1610 -0
  84. package/crates/mint-core/src/companion.rs +716 -0
  85. package/crates/mint-core/src/cron/scheduler.rs +34 -2
  86. package/crates/mint-core/src/git/branches.rs +348 -0
  87. package/crates/mint-core/src/git/checkpoint.rs +40 -0
  88. package/crates/mint-core/src/git/diff.rs +206 -0
  89. package/crates/mint-core/src/git/mod.rs +10 -2
  90. package/crates/mint-core/src/integrations/channels.rs +88 -345
  91. package/crates/mint-core/src/integrations/mcp.rs +397 -50
  92. package/crates/mint-core/src/integrations/mcp_calls.rs +823 -0
  93. package/crates/mint-core/src/integrations/mcp_catalog.rs +267 -0
  94. package/crates/mint-core/src/integrations/mcp_device.rs +319 -0
  95. package/crates/mint-core/src/integrations/mcp_result.rs +356 -0
  96. package/crates/mint-core/src/integrations/webhooks.rs +539 -0
  97. package/crates/mint-core/src/integrations.rs +2 -0
  98. package/crates/mint-core/src/lib.rs +107 -40
  99. package/crates/mint-core/src/live_sync.rs +28 -9
  100. package/crates/mint-core/src/media/auto_shorts.rs +1 -0
  101. package/crates/mint-core/src/media/gemini_live.rs +145 -14
  102. package/crates/mint-core/src/media/image_models.rs +3 -27
  103. package/crates/mint-core/src/media/image_workflow.rs +216 -0
  104. package/crates/mint-core/src/media/library/intake.rs +144 -0
  105. package/crates/mint-core/src/media/library/migration.rs +56 -0
  106. package/crates/mint-core/src/media/library/removal.rs +55 -0
  107. package/crates/mint-core/src/media/library/tests.rs +730 -0
  108. package/crates/mint-core/src/media/library/thumbnails.rs +72 -0
  109. package/crates/mint-core/src/media/library.rs +545 -0
  110. package/crates/mint-core/src/media/mic_transcribe.rs +1 -0
  111. package/crates/mint-core/src/media/model_catalog.rs +375 -0
  112. package/crates/mint-core/src/media/pictures.rs +79 -222
  113. package/crates/mint-core/src/media/subtitle.rs +1 -0
  114. package/crates/mint-core/src/media/video_models.rs +3 -22
  115. package/crates/mint-core/src/media.rs +3 -0
  116. package/crates/mint-core/src/mods/manager.rs +1124 -0
  117. package/crates/mint-core/src/mods/mod.rs +569 -0
  118. package/crates/mint-core/src/mods/operations.rs +204 -0
  119. package/crates/mint-core/src/mods/session.rs +146 -0
  120. package/crates/mint-core/src/mods/slash.rs +59 -0
  121. package/crates/mint-core/src/mods/telemetry.rs +306 -0
  122. package/crates/mint-core/src/orchestration/approvals.rs +344 -0
  123. package/crates/mint-core/src/orchestration/context_render.rs +105 -0
  124. package/crates/mint-core/src/orchestration/mcp_review.rs +153 -0
  125. package/crates/mint-core/src/orchestration/memory_skill.rs +110 -33
  126. package/crates/mint-core/src/orchestration/mod.rs +3129 -481
  127. package/crates/mint-core/src/orchestration/mod_authoring_regressions.rs +209 -0
  128. package/crates/mint-core/src/orchestration/progress_clock.rs +80 -0
  129. package/crates/mint-core/src/orchestration/repeated_failures.rs +116 -0
  130. package/crates/mint-core/src/orchestration/review_regressions.rs +449 -0
  131. package/crates/mint-core/src/orchestration/run_control.rs +196 -0
  132. package/crates/mint-core/src/orchestration/tools/browser.rs +16 -139
  133. package/crates/mint-core/src/orchestration/tools/files.rs +161 -14
  134. package/crates/mint-core/src/orchestration/tools/media.rs +37 -41
  135. package/crates/mint-core/src/orchestration/tools/misc.rs +5 -3
  136. package/crates/mint-core/src/orchestration/tools/mod.rs +2 -0
  137. package/crates/mint-core/src/orchestration/tools/mods.rs +161 -0
  138. package/crates/mint-core/src/orchestration/tools/plugins_mcp.rs +303 -71
  139. package/crates/mint-core/src/orchestration/tools/shell.rs +95 -2
  140. package/crates/mint-core/src/orchestration/tools/web.rs +1 -1
  141. package/crates/mint-core/src/orchestration/workspace_helpers.rs +4 -1
  142. package/crates/mint-core/src/prompts/agent.rs +75 -31
  143. package/crates/mint-core/src/prompts/chat.rs +1 -1
  144. package/crates/mint-core/src/prompts/persona.rs +1 -1
  145. package/crates/mint-core/src/prompts/tool_catalog.rs +121 -27
  146. package/crates/mint-core/src/search/knowledge.rs +7 -1
  147. package/crates/mint-core/src/search/linked_folders/index.rs +896 -0
  148. package/crates/mint-core/src/search/linked_folders.rs +473 -131
  149. package/crates/mint-core/src/slash/catalog.rs +1 -1
  150. package/crates/mint-core/src/slash/mod.rs +644 -33
  151. package/crates/mint-core/src/slash/model_fetcher.rs +12 -3
  152. package/crates/mint-core/src/slash/models.rs +66 -10
  153. package/crates/mint-core/src/slash/mods.rs +155 -0
  154. package/crates/mint-core/src/system/bg_shell.rs +1129 -95
  155. package/crates/mint-core/src/system/config.rs +235 -10
  156. package/crates/mint-core/src/system/html_preview.rs +398 -0
  157. package/crates/mint-core/src/system/mint_web.rs +164 -0
  158. package/crates/mint-core/src/system/release_notes.rs +18 -0
  159. package/crates/mint-core/src/system/shell.rs +21 -6
  160. package/crates/mint-core/src/system/workspace.rs +292 -0
  161. package/crates/mint-core/src/system/workspace_history.rs +604 -0
  162. package/crates/mint-core/src/system.rs +5 -0
  163. package/crates/mint-core/tests/background_retention.rs +34 -0
  164. package/crates/mint-core/tests/background_shutdown.rs +24 -0
  165. package/crates/mint-core/tests/library_http.rs +209 -0
  166. package/crates/mint-core/tests/mcp_contract.rs +283 -0
  167. package/crates/mint-core/tests/mcp_stdio.rs +11 -10
  168. package/crates/mint-core/tests/media_model_catalog_http.rs +403 -0
  169. package/crates/mint-core/tests/memory_persistence.rs +39 -0
  170. package/crates/mint-core/tests/mods_http.rs +215 -0
  171. package/crates/mint-core/tests/terminal_shutdown.rs +109 -0
  172. package/crates/mint-mod-runtime/Cargo.toml +18 -0
  173. package/crates/mint-mod-runtime/src/lib.rs +507 -0
  174. package/crates/mint-mod-runtime/src/sdk.js +56 -0
  175. package/crates/mint-mod-runtime/src/test_sdk.js +32 -0
  176. package/crates/mint-mod-runtime/src/testing.rs +94 -0
  177. package/crates/mint-mod-runtime/tests/runtime.rs +156 -0
  178. package/docs/mods/AUTHORING.md +61 -0
  179. package/docs/mods/README.md +139 -0
  180. package/docs/mods/TESTING.md +56 -0
  181. package/docs/mods/mint-mods.d.ts +66 -0
  182. package/examples/device-mcp/README.md +22 -0
  183. package/examples/device-mcp/contract.md +49 -0
  184. package/examples/device-mcp/mock-device.mjs +66 -0
  185. package/examples/device-mcp/server.mjs +48 -0
  186. package/examples/device-mcp/server.test.mjs +78 -0
  187. package/examples/mods/task-checklist/README.md +1 -0
  188. package/examples/mods/task-checklist/main.ts +17 -0
  189. package/examples/mods/task-checklist/manifest.json +1 -0
  190. package/examples/mods/task-checklist/tests/checklist.test.ts +15 -0
  191. package/examples/mods/token-meter/README.md +1 -0
  192. package/examples/mods/token-meter/main.ts +8 -0
  193. package/examples/mods/token-meter/manifest.json +1 -0
  194. package/examples/mods/tool-guard/README.md +1 -0
  195. package/examples/mods/tool-guard/main.ts +9 -0
  196. package/examples/mods/tool-guard/manifest.json +1 -0
  197. package/out/web/assets/CliSessionsView-CtzUJpK_.css +1 -0
  198. package/out/web/assets/CliSessionsView-D7hw0Mgj.js +1 -0
  199. package/out/web/assets/CodeReviewPage-C7bEOJfd.js +4 -0
  200. package/out/web/assets/CodeReviewPage-Dte9uLiL.css +1 -0
  201. package/out/web/assets/CommandPalette-0RrF-AOT.js +1 -0
  202. package/out/web/assets/ImageStudioPanel-CdxkAf_r.js +1 -0
  203. package/out/web/assets/ImageStudioPanel-D0lnhQV5.css +1 -0
  204. package/out/web/assets/LibraryPage--Qfisp6D.js +2 -0
  205. package/out/web/assets/LibraryPage-DAxUTAg7.css +1 -0
  206. package/out/web/assets/LinkedFoldersView-2nfZbDgN.js +1 -0
  207. package/out/web/assets/McpRegistryPicker-h9uS8_wT.js +1 -0
  208. package/out/web/assets/McpServersView-slfQJzNB.js +1 -0
  209. package/out/web/assets/MintDashboard-BSR5XpXh.js +25 -0
  210. package/out/web/assets/MintDashboard-CWUcCGhd.css +1 -0
  211. package/out/web/assets/PluginsView-l7LSm503.js +1 -0
  212. package/out/web/assets/ScheduledTasksView-CttmLbq1.js +1 -0
  213. package/out/web/assets/SettingsWindow-BCMLYvEV.js +4 -0
  214. package/out/web/assets/SettingsWindow-DcMVmU9u.css +1 -0
  215. package/out/web/assets/SkillsView-rNs1qr_U.js +2 -0
  216. package/out/web/assets/ToolSurfacePage-B9DzEPWN.js +37 -0
  217. package/out/web/assets/ToolSurfacePage-CTfgggF2.css +1 -0
  218. package/out/web/assets/VeoStudioPanel-DOj7_WR3.css +1 -0
  219. package/out/web/assets/VeoStudioPanel-DiJs_Cg2.js +4 -0
  220. package/out/web/assets/WorkspacePanel-CbJ1PFoZ.js +1 -0
  221. package/out/web/assets/WorkspaceSelector-B2xIp04D.js +1 -0
  222. package/out/web/assets/WorkspaceSelector-Ca8uJOsS.css +1 -0
  223. package/out/web/assets/{abnfDiagram-VCTEODGH-DAOBA7ZG.js → abnfDiagram-VCTEODGH-CD0CjoGl.js} +1 -1
  224. package/out/web/assets/{architecture-7GRP2DOG-CgSwQOPQ.js → architecture-7GRP2DOG-RKV83OeZ.js} +1 -1
  225. package/out/web/assets/{architectureDiagram-5GKGNRK7-CYp3yupP.js → architectureDiagram-5GKGNRK7-nN_HAFct.js} +1 -1
  226. package/out/web/assets/{blockDiagram-NRAW4CY4-CyWybDWr.js → blockDiagram-NRAW4CY4-BrrqzHW5.js} +1 -1
  227. package/out/web/assets/{c4Diagram-UCG6FXSJ-BvY3U709.js → c4Diagram-UCG6FXSJ-DTdV8_zE.js} +1 -1
  228. package/out/web/assets/{chunk-75Z2AOVW-TLXCoZYv.js → chunk-75Z2AOVW-_M0z5olV.js} +1 -1
  229. package/out/web/assets/chunk-CKBBP62Z-BpQEgeHF.js +2 -0
  230. package/out/web/assets/{chunk-CLGD4ZFX-xjsKtNS7.js → chunk-CLGD4ZFX-D54yGvGS.js} +1 -1
  231. package/out/web/assets/chunk-DSNCTWBM-C-gvT2ru.js +2 -0
  232. package/out/web/assets/chunk-DU6HZSFF-CnFngIhW.js +128 -0
  233. package/out/web/assets/{chunk-F27PBJKO-BFpgF55A.js → chunk-F27PBJKO-BqtWrnEO.js} +1 -1
  234. package/out/web/assets/{chunk-G27WJ6UU-DfF7qW-C.js → chunk-G27WJ6UU-Ckoq5mi2.js} +1 -1
  235. package/out/web/assets/{chunk-GMAD6QVW-C-558kh_.js → chunk-GMAD6QVW-BuhEcgHq.js} +1 -1
  236. package/out/web/assets/{chunk-JQ64N6SF-wJmeGKAw.js → chunk-JQ64N6SF-CF8T7GiZ.js} +1 -1
  237. package/out/web/assets/{chunk-LCL6LL3I-C1wHD8Ie.js → chunk-LCL6LL3I-BhXV9zyH.js} +1 -1
  238. package/out/web/assets/{chunk-OBVCFTLP-BsRTPBqs.js → chunk-OBVCFTLP-CRIkunUB.js} +1 -1
  239. package/out/web/assets/{chunk-P2QGCYS3-HZIiM7Ni.js → chunk-P2QGCYS3-UFEU6BxZ.js} +1 -1
  240. package/out/web/assets/{chunk-POPQ4Y6H-Dk9wvVb0.js → chunk-POPQ4Y6H-CQcrNbjr.js} +1 -1
  241. package/out/web/assets/{chunk-PWAF6VOD-BmjYLNbq.js → chunk-PWAF6VOD-Cc2uVDsG.js} +1 -1
  242. package/out/web/assets/{chunk-R7TYR2AO-CaHIf-lK.js → chunk-R7TYR2AO-DiCxzdAY.js} +1 -1
  243. package/out/web/assets/{chunk-RHFEMEQ7-_T561hOU.js → chunk-RHFEMEQ7-b7hnPmrR.js} +1 -1
  244. package/out/web/assets/{chunk-SVP7TREG-CM-GGYoY.js → chunk-SVP7TREG-Bu_yYb-V.js} +1 -1
  245. package/out/web/assets/{chunk-TEH6E4GO-K2A8Vg2C.js → chunk-TEH6E4GO-G4Cdlyrt.js} +1 -1
  246. package/out/web/assets/classDiagram-DTDB5LWJ-RnC2mYO4.js +1 -0
  247. package/out/web/assets/classDiagram-v2-JRS7N3AN-RnC2mYO4.js +1 -0
  248. package/out/web/assets/config-CnvCChUM.js +2 -0
  249. package/out/web/assets/config-DM6oIVO3.css +1 -0
  250. package/out/web/assets/core-DsKkQ7ig.js +1 -0
  251. package/out/web/assets/{cynefin-OW5HDTMX-ClzaB6cc.js → cynefin-OW5HDTMX-CnzQfOGp.js} +1 -1
  252. package/out/web/assets/{cynefinDiagram-5FMLGOSQ-D_JzZBzf.js → cynefinDiagram-5FMLGOSQ-DVtQnfpm.js} +1 -1
  253. package/out/web/assets/{dagre-3AP2YEHR-BDuUeMHj.js → dagre-3AP2YEHR-DG76_k5_.js} +1 -1
  254. package/out/web/assets/{diagram-S7CK7UJ4-DgtcfE8P.js → diagram-S7CK7UJ4-CaouUoze.js} +1 -1
  255. package/out/web/assets/{diagram-UQ7AKVKN-DTLoEYti.js → diagram-UQ7AKVKN-B4bEc26B.js} +1 -1
  256. package/out/web/assets/{diagram-VSXAHHWV-ChGl9pXz.js → diagram-VSXAHHWV-BJ86Ov2Y.js} +1 -1
  257. package/out/web/assets/{diagram-VX7I27RA-Be8DWz0S.js → diagram-VX7I27RA-7hH5IPYf.js} +1 -1
  258. package/out/web/assets/{diagram-Z3DM3KII-BMXjRTOF.js → diagram-Z3DM3KII-CCM4clo8.js} +1 -1
  259. package/out/web/assets/dist-js-BkYNznHX.js +1 -0
  260. package/out/web/assets/{ebnfDiagram-PWID7BFC-8Q9pl1CJ.js → ebnfDiagram-PWID7BFC-Da7WAM3k.js} +1 -1
  261. package/out/web/assets/{erDiagram-SSCWMZ5O-DjH8Lnkn.js → erDiagram-SSCWMZ5O-CzzmnZWT.js} +1 -1
  262. package/out/web/assets/event-Cdt7Byyj.js +1 -0
  263. package/out/web/assets/{eventmodeling-NTZA5JFV-BKUH_DOp.js → eventmodeling-NTZA5JFV-xcfsA0XD.js} +1 -1
  264. package/out/web/assets/fira-code-latin-600-normal-BmSSHshu.woff +0 -0
  265. package/out/web/assets/fira-code-latin-600-normal-CvYZpPHg.woff2 +0 -0
  266. package/out/web/assets/fira-code-symbols2-600-normal-Dvq6IQE5.woff +0 -0
  267. package/out/web/assets/flowDiagram-A5DVABFB-JdtH1673.js +1 -0
  268. package/out/web/assets/{ganttDiagram-EL5Y4UJY-D7fZKJNM.js → ganttDiagram-EL5Y4UJY-B3ClFGmL.js} +1 -1
  269. package/out/web/assets/{gitGraph-4MIJSDKK-BypWaaBU.js → gitGraph-4MIJSDKK-BZ_y_cDU.js} +1 -1
  270. package/out/web/assets/{gitGraphDiagram-WWUBYQGX-DWiw_3bq.js → gitGraphDiagram-WWUBYQGX-drhRSYm3.js} +1 -1
  271. package/out/web/assets/index-BZ9xgmMP.css +1 -0
  272. package/out/web/assets/index-Ca8cCS4h.js +168 -0
  273. package/out/web/assets/{info-A6RAGUB7-KkAUxT4j.js → info-A6RAGUB7-B7I3K0F1.js} +1 -1
  274. package/out/web/assets/{infoDiagram-RXCK75RN-BNb6E48y.js → infoDiagram-RXCK75RN-deX_1MPh.js} +1 -1
  275. package/out/web/assets/{ishikawaDiagram-5VMMS53U-Caa-JoAK.js → ishikawaDiagram-5VMMS53U-s38yo28y.js} +1 -1
  276. package/out/web/assets/{journeyDiagram-EYS64GPL-DOrmFGz6.js → journeyDiagram-EYS64GPL-CZn2kB8b.js} +1 -1
  277. package/out/web/assets/jsx-runtime-DN4NhQUK.js +1 -0
  278. package/out/web/assets/{kanban-definition-3QL26DDD-BOWCYcK0.js → kanban-definition-3QL26DDD-DodjgySL.js} +1 -1
  279. package/out/web/assets/markdown-CMhYIcWB.js +39 -0
  280. package/out/web/assets/{mermaid-parser.core-BQqjCkS_.js → mermaid-parser.core-Whu2rlJC.js} +3 -3
  281. package/out/web/assets/mermaid.core-CKyPo8VH.js +11 -0
  282. package/out/web/assets/{mindmap-definition-FBJOCRG2-jTceWpr0.js → mindmap-definition-FBJOCRG2-260WBDxZ.js} +1 -1
  283. package/out/web/assets/modelManager-CUaGWMWu.js +2 -0
  284. package/out/web/assets/oauthManager-DlNhXJr6.js +1 -0
  285. package/out/web/assets/{packet-AYTQ26CC-CJ5RXgol.js → packet-AYTQ26CC-CEeIRKRG.js} +1 -1
  286. package/out/web/assets/{pegDiagram-XKGWAZYB-y8wMtd0y.js → pegDiagram-XKGWAZYB-BYadI-KO.js} +1 -1
  287. package/out/web/assets/{pie-WAS4IAKB-72fTBbl4.js → pie-WAS4IAKB-D8uf4dh5.js} +1 -1
  288. package/out/web/assets/{pieDiagram-E7YTZNPT-e0O02qWq.js → pieDiagram-E7YTZNPT-CjSDy4hO.js} +1 -1
  289. package/out/web/assets/{quadrantDiagram-AXDQQJYC-C9phYLom.js → quadrantDiagram-AXDQQJYC-B8D3gU5d.js} +1 -1
  290. package/out/web/assets/{radar-RG4KPBEZ-BAU2hzUT.js → radar-RG4KPBEZ-DZJxnokY.js} +1 -1
  291. package/out/web/assets/{railroad-74A4TZTK-B0bCAVHC.js → railroad-74A4TZTK-DBysUiUi.js} +1 -1
  292. package/out/web/assets/{railroad-abnf-HS5TGJTU-WhxO7ndE.js → railroad-abnf-HS5TGJTU-CHWxoJYg.js} +1 -1
  293. package/out/web/assets/{railroad-ebnf-LZEXJU2U-BkA0m7-F.js → railroad-ebnf-LZEXJU2U-DDv7X6bY.js} +1 -1
  294. package/out/web/assets/{railroad-peg-WCYAUIDC-BsVpHIaj.js → railroad-peg-WCYAUIDC-DpfcYYNJ.js} +1 -1
  295. package/out/web/assets/{railroadDiagram-O6MQD6OU-CyOLN65Q.js → railroadDiagram-O6MQD6OU-CYtyv0t3.js} +1 -1
  296. package/out/web/assets/{requirementDiagram-EFPCY7ZU-DQXJ7Dud.js → requirementDiagram-EFPCY7ZU-D2X-Ewik.js} +1 -1
  297. package/out/web/assets/{sankeyDiagram-P5KCCOFB-B2XLyIIc.js → sankeyDiagram-P5KCCOFB-Bn2x6C4Y.js} +1 -1
  298. package/out/web/assets/{sequenceDiagram-WJ2MYXX4-DGQY4wwm.js → sequenceDiagram-WJ2MYXX4-BvFLKtPd.js} +1 -1
  299. package/out/web/assets/{stateDiagram-HBIQ2CUA-DHZpDwNy.js → stateDiagram-HBIQ2CUA-Bx0GWeaY.js} +1 -1
  300. package/out/web/assets/stateDiagram-v2-4QOOHH4V--hol1yg_.js +1 -0
  301. package/out/web/assets/studio-B1mk40wD.js +1 -0
  302. package/out/web/assets/{swimlanes-XN3QIQJK-Bn_jcZz-.js → swimlanes-XN3QIQJK-CCma0T7L.js} +1 -1
  303. package/out/web/assets/swimlanesDiagram-VK2B7HYN-BkohoVsU.js +8 -0
  304. package/out/web/assets/{timeline-definition-24CTP7MA-CdTERuJZ.js → timeline-definition-24CTP7MA-iCRhhJ7C.js} +1 -1
  305. package/out/web/assets/{treeView-Q6P3EWNA-TKBKQG6R.js → treeView-Q6P3EWNA-DZ-y08qp.js} +1 -1
  306. package/out/web/assets/{treemap-WGGIJYW6-BP7gQoiC.js → treemap-WGGIJYW6-De0CxzMx.js} +1 -1
  307. package/out/web/assets/{vennDiagram-4TSXK5OY-BHUmxDuK.js → vennDiagram-4TSXK5OY-w_7aZ6xe.js} +1 -1
  308. package/out/web/assets/{wardley-WFR3VGLG-9bct5AAN.js → wardley-WFR3VGLG-DT0y0xJ4.js} +1 -1
  309. package/out/web/assets/{wardleyDiagram-VM6X3IG4-BfJwDDso.js → wardleyDiagram-VM6X3IG4-DAdXELbQ.js} +1 -1
  310. package/out/web/assets/window-BJCVtyET.js +1 -0
  311. package/out/web/assets/{xychartDiagram-S5SC5T6Z-Dqd9Mvpo.js → xychartDiagram-S5SC5T6Z-IZIXf3Vo.js} +1 -1
  312. package/out/web/index-web.html +4 -4
  313. package/out/web/index.html +4 -4
  314. package/package.json +80 -20
  315. package/shared-modules/.github/workflows/ci.yml +120 -0
  316. package/shared-modules/.github/workflows/update.yaml +28 -0
  317. package/slash-commands.json +16 -2
  318. package/src/bin/auto-build.js +66 -0
  319. package/src/bin/desktop.js +113 -0
  320. package/src/bin/desktop.test.js +113 -0
  321. package/src/bin/index.js +18 -25
  322. package/src/bin/index.test.js +89 -0
  323. package/src/bin/measure-effects.mjs +53 -0
  324. package/src/bin/package.test.js +28 -0
  325. package/src/bin/release-notes.test.js +84 -0
  326. package/src/bin/styles.test.js +20 -0
  327. package/src/bin/web.js +5 -0
  328. package/src/bin/web.test.js +39 -0
  329. package/src/renderer/index-web.html +1 -0
  330. package/src/renderer/index.html +1 -2
  331. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  332. package/src/renderer/shared/agentProgress.ts +201 -3
  333. package/src/renderer/shared/components/AgentActivityDrawer.tsx +9 -8
  334. package/src/renderer/shared/components/AgentActivityTable.tsx +193 -133
  335. package/src/renderer/shared/components/ApprovalCard.test.tsx +24 -0
  336. package/src/renderer/shared/components/ApprovalCard.tsx +10 -3
  337. package/src/renderer/shared/components/ArtifactPreviewPanel.tsx +91 -40
  338. package/src/renderer/shared/components/AuthGate.tsx +79 -48
  339. package/src/renderer/shared/components/BackgroundTerminals.tsx +181 -0
  340. package/src/renderer/shared/components/CalculationCard.tsx +41 -15
  341. package/src/renderer/shared/components/ChatCodeBlock.tsx +8 -4
  342. package/src/renderer/shared/components/ChatMessageItem.test.tsx +68 -0
  343. package/src/renderer/shared/components/ChatMessageItem.tsx +52 -33
  344. package/src/renderer/shared/components/ChatPanel.test.tsx +21 -0
  345. package/src/renderer/shared/components/ChatPanel.tsx +676 -415
  346. package/src/renderer/shared/components/ChunkErrorBoundary.tsx +115 -64
  347. package/src/renderer/shared/components/CliSessionsView.test.tsx +125 -0
  348. package/src/renderer/shared/components/CliSessionsView.tsx +543 -0
  349. package/src/renderer/shared/components/CodeReviewPage.tsx +668 -0
  350. package/src/renderer/shared/components/CollapsiblePasteInput.tsx +90 -0
  351. package/src/renderer/shared/components/CommandPalette.tsx +8 -8
  352. package/src/renderer/shared/components/ContextCompactionStatus.tsx +17 -0
  353. package/src/renderer/shared/components/DashboardSidebar.tsx +853 -229
  354. package/src/renderer/shared/components/DesktopTitlebar.tsx +371 -0
  355. package/src/renderer/shared/components/DiffReviewModal.tsx +3 -3
  356. package/src/renderer/shared/components/ErrorNotice.tsx +29 -0
  357. package/src/renderer/shared/components/FormattedMessage.tsx +11 -0
  358. package/src/renderer/shared/components/GeminiLiveOverlay.css +86 -0
  359. package/src/renderer/shared/components/GeminiLiveOverlay.test.tsx +24 -0
  360. package/src/renderer/shared/components/GeminiLiveOverlay.tsx +85 -101
  361. package/src/renderer/shared/components/GitBranchSelector.tsx +442 -0
  362. package/src/renderer/shared/components/GlideSelect.css +138 -0
  363. package/src/renderer/shared/components/GlideSelect.d.ts +31 -0
  364. package/src/renderer/shared/components/GlideSelect.jsx +266 -0
  365. package/src/renderer/shared/components/ImageGenCard.tsx +31 -12
  366. package/src/renderer/shared/components/ImageSearchCard.tsx +76 -65
  367. package/src/renderer/shared/components/ImageStudioPanel.tsx +49 -49
  368. package/src/renderer/shared/components/InlineThoughtNote.tsx +27 -0
  369. package/src/renderer/shared/components/LibraryItemCard.tsx +72 -0
  370. package/src/renderer/shared/components/LibraryPage.tsx +165 -0
  371. package/src/renderer/shared/components/LibraryPreview.tsx +59 -0
  372. package/src/renderer/shared/components/LinkedFoldersView.tsx +365 -46
  373. package/src/renderer/shared/components/McpRegistryPicker.tsx +1 -1
  374. package/src/renderer/shared/components/McpServersView.tsx +51 -30
  375. package/src/renderer/shared/components/McpToolAllowlist.tsx +2 -4
  376. package/src/renderer/shared/components/McpToolResults.tsx +99 -0
  377. package/src/renderer/shared/components/MermaidCard.tsx +14 -8
  378. package/src/renderer/shared/components/MintDashboard.tsx +1002 -831
  379. package/src/renderer/shared/components/ModSessionControls.tsx +8 -0
  380. package/src/renderer/shared/components/ModUi.test.tsx +65 -0
  381. package/src/renderer/shared/components/ModUi.tsx +26 -0
  382. package/src/renderer/shared/components/ModelSelectorPopover.tsx +547 -219
  383. package/src/renderer/shared/components/ModsPanel.tsx +156 -0
  384. package/src/renderer/shared/components/PlanChecklistWidget.tsx +1 -1
  385. package/src/renderer/shared/components/PluginsView.tsx +1 -1
  386. package/src/renderer/shared/components/ProgressActivity.tsx +17 -0
  387. package/src/renderer/shared/components/ReviewHistory.tsx +42 -0
  388. package/src/renderer/shared/components/RewindModal.tsx +11 -18
  389. package/src/renderer/shared/components/RunSummaryDashboard.tsx +8 -14
  390. package/src/renderer/shared/components/ScheduledTasksView.tsx +11 -10
  391. package/src/renderer/shared/components/ScreenCaptureDialog.css +221 -0
  392. package/src/renderer/shared/components/ScreenCaptureDialog.tsx +199 -0
  393. package/src/renderer/shared/components/SelectField.test.tsx +27 -0
  394. package/src/renderer/shared/components/SelectField.tsx +38 -0
  395. package/src/renderer/shared/components/SessionSources.tsx +48 -0
  396. package/src/renderer/shared/components/SessionSourcesPopover.tsx +61 -0
  397. package/src/renderer/shared/components/Settings/AgentsTab.tsx +41 -45
  398. package/src/renderer/shared/components/Settings/AudioTab.tsx +16 -15
  399. package/src/renderer/shared/components/Settings/AutomationTab.tsx +10 -60
  400. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +368 -0
  401. package/src/renderer/shared/components/Settings/GeneralTab.tsx +368 -113
  402. package/src/renderer/shared/components/Settings/MemoryTab.test.tsx +37 -0
  403. package/src/renderer/shared/components/Settings/MemoryTab.tsx +71 -31
  404. package/src/renderer/shared/components/Settings/NotificationsTab.test.tsx +15 -0
  405. package/src/renderer/shared/components/Settings/NotificationsTab.tsx +84 -0
  406. package/src/renderer/shared/components/Settings/PluginsTab.tsx +20 -12
  407. package/src/renderer/shared/components/Settings/ProfileTab.tsx +29 -5
  408. package/src/renderer/shared/components/Settings/SearchableModelCombobox.tsx +411 -0
  409. package/src/renderer/shared/components/Settings/ThemeTab.tsx +416 -165
  410. package/src/renderer/shared/components/SkillsView.tsx +41 -16
  411. package/src/renderer/shared/components/SlashChoicePanel.tsx +88 -0
  412. package/src/renderer/shared/components/SlashSuggestions.test.tsx +33 -0
  413. package/src/renderer/shared/components/SlashSuggestions.tsx +41 -12
  414. package/src/renderer/shared/components/SourcesBlock.tsx +55 -51
  415. package/src/renderer/shared/components/StockCard.tsx +58 -24
  416. package/src/renderer/shared/components/SubagentDagView.tsx +4 -4
  417. package/src/renderer/shared/components/TerminalDock.tsx +241 -0
  418. package/src/renderer/shared/components/ThinkingBlock.tsx +14 -8
  419. package/src/renderer/shared/components/ToolSurfacePage.test.tsx +32 -0
  420. package/src/renderer/shared/components/ToolSurfacePage.tsx +568 -0
  421. package/src/renderer/shared/components/ToolsPanelToggle.tsx +13 -0
  422. package/src/renderer/shared/components/UiFeatureCard.tsx +267 -0
  423. package/src/renderer/shared/components/UiGridCard.tsx +188 -0
  424. package/src/renderer/shared/components/UiMockupWidget.tsx +149 -0
  425. package/src/renderer/shared/components/VeoStudioPanel.tsx +22 -37
  426. package/src/renderer/shared/components/ViewedImages.tsx +36 -0
  427. package/src/renderer/shared/components/VirtualChatHistory.tsx +82 -0
  428. package/src/renderer/shared/components/VoicePill.css +85 -0
  429. package/src/renderer/shared/components/VoicePill.d.ts +12 -0
  430. package/src/renderer/shared/components/VoicePill.jsx +190 -0
  431. package/src/renderer/shared/components/VoicePill.test.tsx +22 -0
  432. package/src/renderer/shared/components/WeatherCard.tsx +3 -3
  433. package/src/renderer/shared/components/WorkspaceContextPopover.tsx +138 -0
  434. package/src/renderer/shared/components/WorkspacePaneResizer.tsx +92 -0
  435. package/src/renderer/shared/components/WorkspacePreviewActions.test.tsx +44 -0
  436. package/src/renderer/shared/components/WorkspacePreviewActions.tsx +19 -0
  437. package/src/renderer/shared/components/WorkspaceSelector.tsx +154 -0
  438. package/src/renderer/shared/components/background-terminals.css +292 -0
  439. package/src/renderer/shared/components/ui/README.md +52 -0
  440. package/src/renderer/shared/components/ui/button.tsx +37 -0
  441. package/src/renderer/shared/components/ui/demo.tsx +26 -0
  442. package/src/renderer/shared/components/ui/error-recovery.css +35 -0
  443. package/src/renderer/shared/components/ui/error-recovery.tsx +35 -0
  444. package/src/renderer/shared/components/ui/voice-powered-orb.css +6 -0
  445. package/src/renderer/shared/components/ui/voice-powered-orb.tsx +374 -0
  446. package/src/renderer/shared/components/workspace-selector.css +248 -0
  447. package/src/renderer/shared/constants/config.ts +57 -3
  448. package/src/renderer/shared/constants/models.ts +189 -2
  449. package/src/renderer/shared/constants/slashCommands.ts +1 -1
  450. package/src/renderer/shared/conversation/coordinator.test.ts +27 -0
  451. package/src/renderer/shared/conversation/coordinator.ts +82 -0
  452. package/src/renderer/shared/conversation/lifecycle.test.ts +362 -0
  453. package/src/renderer/shared/conversation/lifecycle.ts +418 -0
  454. package/src/renderer/shared/conversation/projectNavigation.test.ts +105 -0
  455. package/src/renderer/shared/conversation/projectNavigation.ts +39 -0
  456. package/src/renderer/shared/conversation/sourcesTransport.ts +27 -0
  457. package/src/renderer/shared/conversation/syncView.test.ts +13 -0
  458. package/src/renderer/shared/conversation/syncView.ts +6 -0
  459. package/src/renderer/shared/conversation/transport.test.ts +163 -0
  460. package/src/renderer/shared/conversation/transport.ts +159 -0
  461. package/src/renderer/shared/conversation/useConversationCoordinator.ts +64 -0
  462. package/src/renderer/shared/css/agent-observability.css +46 -15
  463. package/src/renderer/shared/css/auth-gate.css +114 -15
  464. package/src/renderer/shared/css/chat-ui-cards.css +495 -0
  465. package/src/renderer/shared/css/code-review.css +856 -0
  466. package/src/renderer/shared/css/code-sessions.css +479 -0
  467. package/src/renderer/shared/css/collapsed-paste.css +35 -0
  468. package/src/renderer/shared/css/effects.css +15 -0
  469. package/src/renderer/shared/css/error-notice.css +60 -0
  470. package/src/renderer/shared/css/file-changes.css +132 -0
  471. package/src/renderer/shared/css/image-studio.css +16 -16
  472. package/src/renderer/shared/css/library.css +476 -0
  473. package/src/renderer/shared/css/management-views.css +415 -27
  474. package/src/renderer/shared/css/mcp-results.css +11 -0
  475. package/src/renderer/shared/css/model-selector.css +543 -85
  476. package/src/renderer/shared/css/mods.css +77 -0
  477. package/src/renderer/shared/css/notification-notice.css +28 -0
  478. package/src/renderer/shared/css/session-sources-popover.css +9 -0
  479. package/src/renderer/shared/css/session-sources.css +23 -0
  480. package/src/renderer/shared/css/settings/agents.css +1 -1
  481. package/src/renderer/shared/css/settings/automation.css +13 -13
  482. package/src/renderer/shared/css/settings/general.css +526 -5
  483. package/src/renderer/shared/css/settings/memory.css +154 -0
  484. package/src/renderer/shared/css/settings/notifications.css +11 -0
  485. package/src/renderer/shared/css/settings/plugins.css +5 -5
  486. package/src/renderer/shared/css/settings/profile.css +331 -1
  487. package/src/renderer/shared/css/settings/theme.css +656 -3
  488. package/src/renderer/shared/css/slash-choice.css +95 -0
  489. package/src/renderer/shared/css/slash-suggestions.css +68 -0
  490. package/src/renderer/shared/css/sources-block.css +91 -0
  491. package/src/renderer/shared/css/thinking-status.css +46 -0
  492. package/src/renderer/shared/css/tools-panel.css +7 -0
  493. package/src/renderer/shared/css/ui-tailwind.css +19 -0
  494. package/src/renderer/shared/css/veo-studio.css +16 -15
  495. package/src/renderer/shared/css/viewed-images.css +15 -0
  496. package/src/renderer/shared/fonts.css +46 -0
  497. package/src/renderer/shared/fonts.ts +10 -37
  498. package/src/renderer/shared/lib/utils.ts +6 -0
  499. package/src/renderer/shared/library/controller.test.ts +61 -0
  500. package/src/renderer/shared/library/controller.ts +57 -0
  501. package/src/renderer/shared/library/transport.test.ts +26 -0
  502. package/src/renderer/shared/library/transport.ts +60 -0
  503. package/src/renderer/shared/library/useLibraryFile.ts +38 -0
  504. package/src/renderer/shared/mods.ts +58 -0
  505. package/src/renderer/shared/platform.test.ts +90 -0
  506. package/src/renderer/shared/platform.ts +219 -7
  507. package/src/renderer/shared/theme/themeManager.test.ts +43 -0
  508. package/src/renderer/shared/theme/themeManager.ts +157 -0
  509. package/src/renderer/shared/theme/themes.ts +116 -0
  510. package/src/renderer/shared/theme/tokens.css +318 -0
  511. package/src/renderer/shared/types.ts +137 -2
  512. package/src/renderer/shared/utils/agentActivity.test.ts +75 -0
  513. package/src/renderer/shared/utils/agentActivity.ts +168 -8
  514. package/src/renderer/shared/utils/approval.tsx +7 -0
  515. package/src/renderer/shared/utils/artifactBrowser.test.ts +80 -0
  516. package/src/renderer/shared/utils/artifactBrowser.ts +41 -0
  517. package/src/renderer/shared/utils/cardJson.test.ts +35 -0
  518. package/src/renderer/shared/utils/cardJson.ts +30 -0
  519. package/src/renderer/shared/utils/contextCompaction.test.tsx +48 -0
  520. package/src/renderer/shared/utils/contextCompaction.ts +21 -0
  521. package/src/renderer/shared/utils/effectVisibility.ts +9 -0
  522. package/src/renderer/shared/utils/fileIcons.ts +1 -1
  523. package/src/renderer/shared/utils/markdown.tsx +330 -81
  524. package/src/renderer/shared/utils/markdown.webImages.test.tsx +40 -0
  525. package/src/renderer/shared/utils/mcpResults.test.tsx +91 -0
  526. package/src/renderer/shared/utils/modViewport.ts +7 -0
  527. package/src/renderer/shared/utils/nativeRecordingSession.test.ts +61 -0
  528. package/src/renderer/shared/utils/nativeRecordingSession.ts +46 -0
  529. package/src/renderer/shared/utils/notificationSettings.test.ts +14 -0
  530. package/src/renderer/shared/utils/notificationSettings.ts +23 -0
  531. package/src/renderer/shared/utils/notifications.test.ts +51 -0
  532. package/src/renderer/shared/utils/notifications.ts +54 -0
  533. package/src/renderer/shared/utils/pastedText.test.ts +20 -0
  534. package/src/renderer/shared/utils/pastedText.ts +13 -0
  535. package/src/renderer/shared/utils/progressCache.test.ts +32 -0
  536. package/src/renderer/shared/utils/providers.ts +8 -3
  537. package/src/renderer/shared/utils/screenCapture.ts +43 -0
  538. package/src/renderer/shared/utils/sessionSources.test.ts +31 -0
  539. package/src/renderer/shared/utils/sessionSources.ts +34 -0
  540. package/src/renderer/shared/utils/slashCommandProcessor.ts +5 -2
  541. package/src/renderer/shared/utils/sourceAttachments.ts +28 -0
  542. package/src/renderer/shared/utils/speech.ts +51 -9
  543. package/src/renderer/shared/utils/syntaxHighlight.test.tsx +22 -0
  544. package/src/renderer/shared/utils/syntaxHighlight.tsx +26 -0
  545. package/src/renderer/shared/utils/ui.ts +4 -64
  546. package/src/renderer/shared/utils/useGeminiLiveVoice.ts +49 -5
  547. package/src/renderer/shared/utils/useLatestCallback.ts +9 -0
  548. package/src/renderer/shared/utils/useNativeVoiceInput.ts +80 -72
  549. package/src/renderer/shared/utils/useSessionSources.ts +33 -0
  550. package/src/renderer/shared/utils/viewedImages.ts +26 -0
  551. package/src/renderer/shared/utils/workspacePane.test.ts +33 -0
  552. package/src/renderer/shared/utils/workspacePane.ts +20 -0
  553. package/src/renderer/shared/utils/workspacePreview.test.ts +24 -0
  554. package/src/renderer/shared/utils/workspacePreview.ts +16 -0
  555. package/src/renderer/shared/utils/workspaceSnapshot.test.ts +49 -0
  556. package/src/renderer/shared/utils/workspaceSnapshot.ts +26 -0
  557. package/src/renderer/shared/utils/workspaces.ts +10 -0
  558. package/src/renderer/shared/workspaceUndo.ts +88 -0
  559. package/src/renderer/src/App.tsx +16 -1
  560. package/src/renderer/src/components/BrowserShell.tsx +108 -0
  561. package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +91 -0
  562. package/src/renderer/src/components/LiveTranslateControls.tsx +65 -0
  563. package/src/renderer/src/components/MintDashboard.tsx +4 -2
  564. package/src/renderer/src/components/PicturesLibrary.tsx +2 -287
  565. package/src/renderer/src/components/ScreenPicker.css +227 -0
  566. package/src/renderer/src/components/ScreenPicker.tsx +312 -526
  567. package/src/renderer/src/components/SettingsWindow.tsx +48 -5
  568. package/src/renderer/src/components/SpotlightWindow.tsx +1 -0
  569. package/src/renderer/src/components/WidgetWindow.tsx +27 -3
  570. package/src/renderer/src/components/WorkspacePanel.tsx +269 -61
  571. package/src/renderer/src/components/browser-shell.css +185 -0
  572. package/src/renderer/src/components/browser-suggestions-popup.css +55 -0
  573. package/src/renderer/src/components/liveTranslateFrames.test.ts +44 -0
  574. package/src/renderer/src/components/liveTranslateFrames.ts +45 -0
  575. package/src/renderer/src/components/liveTranslateLanguage.ts +6 -0
  576. package/src/renderer/src/css/app-shell.css +400 -204
  577. package/src/renderer/src/css/chat.css +1666 -594
  578. package/src/renderer/src/css/dashboard.css +11 -0
  579. package/src/renderer/src/css/desktop-tools.css +928 -0
  580. package/src/renderer/src/css/misc.css +5 -60
  581. package/src/renderer/src/css/pictures.css +2 -407
  582. package/src/renderer/src/css/settings/base.css +394 -157
  583. package/src/renderer/src/css/sidebar-workspace.css +1072 -376
  584. package/src/renderer/src/css/spotlight.css +15 -26
  585. package/src/renderer/src/css/tool-surfaces.css +784 -0
  586. package/src/renderer/src/css/widget.css +36 -8
  587. package/src/renderer/src/env.d.ts +4 -9
  588. package/src/renderer/src/hooks/useProviderModels.ts +2 -9
  589. package/src/renderer/src/index.css +53 -46
  590. package/src/renderer/src/main.tsx +12 -0
  591. package/src/renderer/src/proactiveSuggestions.ts +29 -0
  592. package/src/renderer/src/tauri.ts +302 -339
  593. package/src/renderer/src/voiceInput.ts +2 -1
  594. package/src/renderer/src-web/App.tsx +40 -0
  595. package/src/renderer/src-web/components/MintDashboard.tsx +4 -2
  596. package/src/renderer/src-web/components/PicturesLibrary.tsx +2 -284
  597. package/src/renderer/src-web/components/PullToRefresh.tsx +107 -0
  598. package/src/renderer/src-web/components/SettingsWindow.tsx +36 -5
  599. package/src/renderer/src-web/css/app-shell.css +188 -207
  600. package/src/renderer/src-web/css/chat.css +1236 -507
  601. package/src/renderer/src-web/css/dashboard.css +11 -0
  602. package/src/renderer/src-web/css/misc.css +3 -55
  603. package/src/renderer/src-web/css/mobile.css +679 -0
  604. package/src/renderer/src-web/css/pictures.css +2 -415
  605. package/src/renderer/src-web/css/pull-to-refresh.css +54 -0
  606. package/src/renderer/src-web/css/settings/base.css +353 -134
  607. package/src/renderer/src-web/css/settings/mobile.css +320 -0
  608. package/src/renderer/src-web/css/sidebar-workspace.css +546 -187
  609. package/src/renderer/src-web/css/spotlight.css +9 -26
  610. package/src/renderer/src-web/css/widget.css +1 -6
  611. package/src/renderer/src-web/env.d.ts +4 -12
  612. package/src/renderer/src-web/hooks/useProviderModels.ts +2 -9
  613. package/src/renderer/src-web/index.css +53 -96
  614. package/src/renderer/src-web/main.tsx +12 -0
  615. package/src/renderer/src-web/proactiveSuggestions.ts +3 -0
  616. package/src/renderer/src-web/tauri.ts +309 -364
  617. package/src/renderer/tests/backgroundTerminals.browser.html +81 -0
  618. package/src/renderer/tests/chatPanelFixture.ts +23 -0
  619. package/src/renderer/tests/chatPerformance.browser.html +71 -0
  620. package/src/renderer/tests/cliSessions.browser.html +82 -0
  621. package/src/renderer/tests/collapsedPaste.browser.html +70 -0
  622. package/src/renderer/tests/composerMenus.browser.html +116 -0
  623. package/src/renderer/tests/contextCompaction.browser.html +17 -0
  624. package/src/renderer/tests/conversationLifecycle.browser.html +70 -0
  625. package/src/renderer/tests/effectsPerformance.browser.html +13 -0
  626. package/src/renderer/tests/errorRecovery.browser.html +21 -0
  627. package/src/renderer/tests/glideSelect.browser.html +71 -0
  628. package/src/renderer/tests/glideSelect.preview.html +27 -0
  629. package/src/renderer/tests/library.browser.html +1 -0
  630. package/src/renderer/tests/library.browser.tsx +78 -0
  631. package/src/renderer/tests/liveOrb.browser.html +49 -0
  632. package/src/renderer/tests/liveVoice.browser.html +29 -0
  633. package/src/renderer/tests/modInput.browser.html +22 -0
  634. package/src/renderer/tests/sessionSources.browser.html +1 -0
  635. package/src/renderer/tests/sessionSources.browser.tsx +107 -0
  636. package/src/renderer/tests/skills.browser.html +24 -0
  637. package/src/renderer/tests/toolsPanel.browser.html +1 -0
  638. package/src/renderer/tests/toolsPanel.browser.tsx +113 -0
  639. package/src/renderer/tests/viewedImages.browser.html +1 -0
  640. package/src/renderer/tests/viewedImages.browser.tsx +93 -0
  641. package/src/renderer/tests/voicePill.browser.html +80 -0
  642. package/src/renderer/tests/voicePill.preview.html +34 -0
  643. package/src/renderer/tests/webSources.browser.html +1 -0
  644. package/src/renderer/tests/webSources.browser.tsx +98 -0
  645. package/src/renderer/tests/workspaceUndo.browser.html +92 -0
  646. package/src-tauri/Cargo.toml +40 -0
  647. package/src-tauri/build.rs +3 -0
  648. package/src-tauri/src/app/lifecycle.rs +277 -0
  649. package/src-tauri/src/app/mod.rs +4 -0
  650. package/src-tauri/src/app/shortcuts.rs +40 -0
  651. package/src-tauri/src/app/tray.rs +52 -0
  652. package/src-tauri/src/background/events.rs +70 -0
  653. package/src-tauri/src/background/headless.rs +230 -0
  654. package/src-tauri/src/background/mod.rs +4 -0
  655. package/src-tauri/src/background/proactive.rs +262 -0
  656. package/src-tauri/src/commands/approvals.rs +20 -0
  657. package/src-tauri/src/commands/auth.rs +127 -0
  658. package/src-tauri/src/commands/conversation.rs +642 -0
  659. package/src-tauri/src/commands/integrations.rs +157 -0
  660. package/src-tauri/src/commands/media.rs +179 -0
  661. package/src-tauri/src/commands/mod.rs +11 -0
  662. package/src-tauri/src/commands/mods.rs +6 -0
  663. package/src-tauri/src/commands/settings.rs +141 -0
  664. package/src-tauri/src/commands/voice.rs +153 -0
  665. package/src-tauri/src/commands/workspace.rs +501 -0
  666. package/src-tauri/src/lib.rs +10 -0
  667. package/src-tauri/src/main.rs +3 -0
  668. package/src-tauri/src/native/actions.rs +206 -0
  669. package/src-tauri/src/native/browser.rs +252 -0
  670. package/src-tauri/src/native/capture.rs +695 -0
  671. package/src-tauri/src/native/discord_rpc.rs +99 -0
  672. package/src-tauri/src/native/integrations.rs +126 -0
  673. package/src-tauri/src/native/mod.rs +12 -0
  674. package/src-tauri/src/native/notifications.rs +51 -0
  675. package/src-tauri/src/native/plugins.rs +14 -0
  676. package/src-tauri/src/native/system.rs +250 -0
  677. package/src-tauri/src/native/terminal.rs +201 -0
  678. package/src-tauri/src/native/updater.rs +148 -0
  679. package/src-tauri/src/native/windows.rs +143 -0
  680. package/tsconfig.json +2 -0
  681. package/vite.config.ts +4 -1
  682. package/vite.config.web.ts +5 -0
  683. package/.cargo/config.toml +0 -13
  684. package/.claude/settings.local.json +0 -182
  685. package/.codex +0 -0
  686. package/CLAUDE.md +0 -102
  687. package/out/web/Live2DCubismCore.js +0 -9
  688. package/out/web/assets/MintDashboard-DWSqucgb.js +0 -57
  689. package/out/web/assets/SettingsWindow-DFM2aj5B.js +0 -2
  690. package/out/web/assets/chunk-CKBBP62Z-nr1uKPj9.js +0 -2
  691. package/out/web/assets/chunk-DSNCTWBM-Dtj8Rqsd.js +0 -2
  692. package/out/web/assets/chunk-DU6HZSFF-2tv5K1wT.js +0 -128
  693. package/out/web/assets/classDiagram-DTDB5LWJ-BdSFArgw.js +0 -1
  694. package/out/web/assets/classDiagram-v2-JRS7N3AN-BdSFArgw.js +0 -1
  695. package/out/web/assets/core-DImejIUG.js +0 -1
  696. package/out/web/assets/dist-js-BxuKEVWM.js +0 -1
  697. package/out/web/assets/event-BNl8Zahw.js +0 -1
  698. package/out/web/assets/fira-code-cyrillic-400-normal-UC0NFL4U.woff2 +0 -0
  699. package/out/web/assets/fira-code-cyrillic-400-normal-dc1q3VD7.woff +0 -0
  700. package/out/web/assets/fira-code-cyrillic-500-normal-Bu7VCL72.woff +0 -0
  701. package/out/web/assets/fira-code-cyrillic-500-normal-CX35h3Mg.woff2 +0 -0
  702. package/out/web/assets/fira-code-cyrillic-ext-400-normal-Buh61xzB.woff +0 -0
  703. package/out/web/assets/fira-code-cyrillic-ext-400-normal-txZ9Fk_1.woff2 +0 -0
  704. package/out/web/assets/fira-code-cyrillic-ext-500-normal-CYRspHgm.woff2 +0 -0
  705. package/out/web/assets/fira-code-cyrillic-ext-500-normal-CwtLmSFD.woff +0 -0
  706. package/out/web/assets/fira-code-greek-400-normal-B2Gh_Y8s.woff2 +0 -0
  707. package/out/web/assets/fira-code-greek-400-normal-DobhaxvF.woff +0 -0
  708. package/out/web/assets/fira-code-greek-500-normal-CVO0IRNt.woff2 +0 -0
  709. package/out/web/assets/fira-code-greek-500-normal-DEXIWiey.woff +0 -0
  710. package/out/web/assets/fira-code-greek-ext-400-normal-DR7mBgIM.woff2 +0 -0
  711. package/out/web/assets/fira-code-greek-ext-400-normal-DSPUheWg.woff +0 -0
  712. package/out/web/assets/fira-code-greek-ext-500-normal-BO3iq16n.woff2 +0 -0
  713. package/out/web/assets/fira-code-greek-ext-500-normal-CMiNLXER.woff +0 -0
  714. package/out/web/assets/fira-code-latin-ext-400-normal-DG73JjOf.woff +0 -0
  715. package/out/web/assets/fira-code-latin-ext-400-normal-KSMg0QLl.woff2 +0 -0
  716. package/out/web/assets/fira-code-latin-ext-500-normal-CaANqXjI.woff +0 -0
  717. package/out/web/assets/fira-code-latin-ext-500-normal-Crd9e_oI.woff2 +0 -0
  718. package/out/web/assets/flowDiagram-A5DVABFB-BpnHUSU7.js +0 -1
  719. package/out/web/assets/index-CVRg270z.css +0 -1
  720. package/out/web/assets/index-DxZQr9yD.js +0 -10
  721. package/out/web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  722. package/out/web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  723. package/out/web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  724. package/out/web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  725. package/out/web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  726. package/out/web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  727. package/out/web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  728. package/out/web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  729. package/out/web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  730. package/out/web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  731. package/out/web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  732. package/out/web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  733. package/out/web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  734. package/out/web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  735. package/out/web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  736. package/out/web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  737. package/out/web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  738. package/out/web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  739. package/out/web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  740. package/out/web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  741. package/out/web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  742. package/out/web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  743. package/out/web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  744. package/out/web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  745. package/out/web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  746. package/out/web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  747. package/out/web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  748. package/out/web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  749. package/out/web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  750. package/out/web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  751. package/out/web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  752. package/out/web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  753. package/out/web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  754. package/out/web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  755. package/out/web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  756. package/out/web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  757. package/out/web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  758. package/out/web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  759. package/out/web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  760. package/out/web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  761. package/out/web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  762. package/out/web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  763. package/out/web/assets/kanit-latin-400-normal-BUlsQXH3.woff2 +0 -0
  764. package/out/web/assets/kanit-latin-400-normal-HVZWrfev.woff +0 -0
  765. package/out/web/assets/kanit-latin-500-normal-CX_4XcKi.woff2 +0 -0
  766. package/out/web/assets/kanit-latin-500-normal-qBha86O4.woff +0 -0
  767. package/out/web/assets/kanit-latin-600-normal-0lS78yAd.woff2 +0 -0
  768. package/out/web/assets/kanit-latin-600-normal-DlacpeTg.woff +0 -0
  769. package/out/web/assets/kanit-latin-ext-400-normal-B2xPf58u.woff2 +0 -0
  770. package/out/web/assets/kanit-latin-ext-400-normal-CHvNKHHD.woff +0 -0
  771. package/out/web/assets/kanit-latin-ext-500-normal-BFdWrG22.woff +0 -0
  772. package/out/web/assets/kanit-latin-ext-500-normal-EHs3suD6.woff2 +0 -0
  773. package/out/web/assets/kanit-latin-ext-600-normal-BBDzoLaj.woff +0 -0
  774. package/out/web/assets/kanit-latin-ext-600-normal-DAX3wtuq.woff2 +0 -0
  775. package/out/web/assets/kanit-thai-400-normal-D2nliXKa.woff +0 -0
  776. package/out/web/assets/kanit-thai-400-normal-D80Pglro.woff2 +0 -0
  777. package/out/web/assets/kanit-thai-500-normal-BB8gri04.woff2 +0 -0
  778. package/out/web/assets/kanit-thai-500-normal-Iy_w0a80.woff +0 -0
  779. package/out/web/assets/kanit-thai-600-normal-Bj6oiaDc.woff2 +0 -0
  780. package/out/web/assets/kanit-thai-600-normal-wbTlPO7I.woff +0 -0
  781. package/out/web/assets/kanit-vietnamese-400-normal-DRI8YWyb.woff2 +0 -0
  782. package/out/web/assets/kanit-vietnamese-400-normal-dow6Bnto.woff +0 -0
  783. package/out/web/assets/kanit-vietnamese-500-normal-BzTrpCM3.woff2 +0 -0
  784. package/out/web/assets/kanit-vietnamese-500-normal-T1ndT7MF.woff +0 -0
  785. package/out/web/assets/kanit-vietnamese-600-normal-Bi7fw3I4.woff2 +0 -0
  786. package/out/web/assets/kanit-vietnamese-600-normal-DXCRAa_8.woff +0 -0
  787. package/out/web/assets/mali-latin-400-normal-ByPcZ8Gx.woff +0 -0
  788. package/out/web/assets/mali-latin-400-normal-Cuc6Ykl8.woff2 +0 -0
  789. package/out/web/assets/mali-latin-500-normal-B01M-LP6.woff2 +0 -0
  790. package/out/web/assets/mali-latin-500-normal-Thgqw_9f.woff +0 -0
  791. package/out/web/assets/mali-latin-600-normal-B9TiUNpT.woff2 +0 -0
  792. package/out/web/assets/mali-latin-600-normal-CycpQCwU.woff +0 -0
  793. package/out/web/assets/mali-latin-ext-400-normal-DUVdJQO4.woff +0 -0
  794. package/out/web/assets/mali-latin-ext-400-normal-iCG1ALuJ.woff2 +0 -0
  795. package/out/web/assets/mali-latin-ext-500-normal-BfJE4UMZ.woff2 +0 -0
  796. package/out/web/assets/mali-latin-ext-500-normal-FoLEyOKS.woff +0 -0
  797. package/out/web/assets/mali-latin-ext-600-normal-BTvIfiCp.woff2 +0 -0
  798. package/out/web/assets/mali-latin-ext-600-normal-DAQcLVmD.woff +0 -0
  799. package/out/web/assets/mali-thai-400-normal-BO7A3bLZ.woff2 +0 -0
  800. package/out/web/assets/mali-thai-400-normal-CX9VrjTn.woff +0 -0
  801. package/out/web/assets/mali-thai-500-normal-Cph2qha9.woff +0 -0
  802. package/out/web/assets/mali-thai-500-normal-D89rMT77.woff2 +0 -0
  803. package/out/web/assets/mali-thai-600-normal-DPgXT6rZ.woff +0 -0
  804. package/out/web/assets/mali-thai-600-normal-DlTx3OsB.woff2 +0 -0
  805. package/out/web/assets/mali-vietnamese-400-normal-DUKxzXaz.woff2 +0 -0
  806. package/out/web/assets/mali-vietnamese-400-normal-gFDLvsOK.woff +0 -0
  807. package/out/web/assets/mali-vietnamese-500-normal--OY07q2c.woff +0 -0
  808. package/out/web/assets/mali-vietnamese-500-normal-KDrNRwfW.woff2 +0 -0
  809. package/out/web/assets/mali-vietnamese-600-normal-CtTMUU7T.woff +0 -0
  810. package/out/web/assets/mali-vietnamese-600-normal-DFlJr66u.woff2 +0 -0
  811. package/out/web/assets/mermaid.core-DYGGlqOf.js +0 -11
  812. package/out/web/assets/mitr-latin-400-normal-Cb18vPmZ.woff +0 -0
  813. package/out/web/assets/mitr-latin-400-normal-CwbkLQ4a.woff2 +0 -0
  814. package/out/web/assets/mitr-latin-500-normal-B4JOhzAi.woff +0 -0
  815. package/out/web/assets/mitr-latin-500-normal-BmClOow2.woff2 +0 -0
  816. package/out/web/assets/mitr-latin-ext-400-normal-B3CPG7Yo.woff2 +0 -0
  817. package/out/web/assets/mitr-latin-ext-400-normal-DTx65hiC.woff +0 -0
  818. package/out/web/assets/mitr-latin-ext-500-normal-B_Diajz1.woff2 +0 -0
  819. package/out/web/assets/mitr-latin-ext-500-normal-DYOsVaVT.woff +0 -0
  820. package/out/web/assets/mitr-thai-400-normal-CS4Do6aM.woff +0 -0
  821. package/out/web/assets/mitr-thai-400-normal-CXWaAdVj.woff2 +0 -0
  822. package/out/web/assets/mitr-thai-500-normal-CVdBrEVH.woff2 +0 -0
  823. package/out/web/assets/mitr-thai-500-normal-CW_B5D-i.woff +0 -0
  824. package/out/web/assets/mitr-vietnamese-400-normal-CPMGWyoC.woff2 +0 -0
  825. package/out/web/assets/mitr-vietnamese-400-normal-Z6XhIpdY.woff +0 -0
  826. package/out/web/assets/mitr-vietnamese-500-normal-CDrv0585.woff2 +0 -0
  827. package/out/web/assets/mitr-vietnamese-500-normal-CMQWyq-Z.woff +0 -0
  828. package/out/web/assets/noto-sans-thai-latin-400-normal-BaWBrqFP.woff2 +0 -0
  829. package/out/web/assets/noto-sans-thai-latin-400-normal-Dh_FJjOB.woff +0 -0
  830. package/out/web/assets/noto-sans-thai-latin-500-normal-BC04jsmm.woff +0 -0
  831. package/out/web/assets/noto-sans-thai-latin-500-normal-fyBKClF1.woff2 +0 -0
  832. package/out/web/assets/noto-sans-thai-latin-600-normal-CemcjnVR.woff2 +0 -0
  833. package/out/web/assets/noto-sans-thai-latin-600-normal-Dbc-dNO6.woff +0 -0
  834. package/out/web/assets/noto-sans-thai-latin-ext-400-normal-B-1H0YkH.woff +0 -0
  835. package/out/web/assets/noto-sans-thai-latin-ext-400-normal-nBP59pVw.woff2 +0 -0
  836. package/out/web/assets/noto-sans-thai-latin-ext-500-normal-B-ZWb8SZ.woff2 +0 -0
  837. package/out/web/assets/noto-sans-thai-latin-ext-500-normal-CrEHO7C8.woff +0 -0
  838. package/out/web/assets/noto-sans-thai-latin-ext-600-normal-PkMZo0xa.woff +0 -0
  839. package/out/web/assets/noto-sans-thai-latin-ext-600-normal-maHHBsez.woff2 +0 -0
  840. package/out/web/assets/noto-sans-thai-thai-400-normal-BEOmKAI1.woff2 +0 -0
  841. package/out/web/assets/noto-sans-thai-thai-400-normal-Dxb_unuU.woff +0 -0
  842. package/out/web/assets/noto-sans-thai-thai-500-normal-BOGXO2tw.woff +0 -0
  843. package/out/web/assets/noto-sans-thai-thai-500-normal-CU_X86GQ.woff2 +0 -0
  844. package/out/web/assets/noto-sans-thai-thai-600-normal-CmgB8_JK.woff +0 -0
  845. package/out/web/assets/noto-sans-thai-thai-600-normal-Cs73sVjl.woff2 +0 -0
  846. package/out/web/assets/oauthManager-DNiwM8Rn.js +0 -2
  847. package/out/web/assets/oauthManager-DuamS1Av.css +0 -1
  848. package/out/web/assets/outfit-latin-300-normal-AWLfuCom.woff +0 -0
  849. package/out/web/assets/outfit-latin-300-normal-CRYTwY6m.woff2 +0 -0
  850. package/out/web/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
  851. package/out/web/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
  852. package/out/web/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
  853. package/out/web/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
  854. package/out/web/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
  855. package/out/web/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
  856. package/out/web/assets/outfit-latin-ext-300-normal-BYS6Dpvq.woff2 +0 -0
  857. package/out/web/assets/outfit-latin-ext-300-normal-CMIMshEd.woff +0 -0
  858. package/out/web/assets/outfit-latin-ext-400-normal-5tcqmc2S.woff2 +0 -0
  859. package/out/web/assets/outfit-latin-ext-400-normal-DHm7mdGe.woff +0 -0
  860. package/out/web/assets/outfit-latin-ext-500-normal-DrCvqoFD.woff +0 -0
  861. package/out/web/assets/outfit-latin-ext-500-normal-zeox_O30.woff2 +0 -0
  862. package/out/web/assets/outfit-latin-ext-600-normal-B85nYjL1.woff2 +0 -0
  863. package/out/web/assets/outfit-latin-ext-600-normal-CWJcPgd7.woff +0 -0
  864. package/out/web/assets/preload-helper-D0uHx7FM.js +0 -1
  865. package/out/web/assets/prompt-latin-400-normal-BQ9zjSN8.woff2 +0 -0
  866. package/out/web/assets/prompt-latin-400-normal-CxU4ec_r.woff +0 -0
  867. package/out/web/assets/prompt-latin-500-normal-B1edBDQ9.woff +0 -0
  868. package/out/web/assets/prompt-latin-500-normal-CxzxEHZc.woff2 +0 -0
  869. package/out/web/assets/prompt-latin-600-normal-CSDDLorv.woff +0 -0
  870. package/out/web/assets/prompt-latin-600-normal-hKZWXsc1.woff2 +0 -0
  871. package/out/web/assets/prompt-latin-ext-400-normal-1pHbN9uy.woff +0 -0
  872. package/out/web/assets/prompt-latin-ext-400-normal-DdSafGZ9.woff2 +0 -0
  873. package/out/web/assets/prompt-latin-ext-500-normal--EZ1um7s.woff2 +0 -0
  874. package/out/web/assets/prompt-latin-ext-500-normal-D-lI1EVT.woff +0 -0
  875. package/out/web/assets/prompt-latin-ext-600-normal-C8b50x67.woff +0 -0
  876. package/out/web/assets/prompt-latin-ext-600-normal-Cg9L7iJU.woff2 +0 -0
  877. package/out/web/assets/prompt-thai-400-normal-BrkKv8cO.woff2 +0 -0
  878. package/out/web/assets/prompt-thai-400-normal-DJypnfr3.woff +0 -0
  879. package/out/web/assets/prompt-thai-500-normal-C18pDUoL.woff2 +0 -0
  880. package/out/web/assets/prompt-thai-500-normal-D7YNqUJD.woff +0 -0
  881. package/out/web/assets/prompt-thai-600-normal-CYPViXds.woff +0 -0
  882. package/out/web/assets/prompt-thai-600-normal-MrdfU7zR.woff2 +0 -0
  883. package/out/web/assets/prompt-vietnamese-400-normal-BCPzsgPT.woff2 +0 -0
  884. package/out/web/assets/prompt-vietnamese-400-normal-D4pLyeNK.woff +0 -0
  885. package/out/web/assets/prompt-vietnamese-500-normal-DmzxmPwa.woff2 +0 -0
  886. package/out/web/assets/prompt-vietnamese-500-normal-w_sS_j2F.woff +0 -0
  887. package/out/web/assets/prompt-vietnamese-600-normal-1dyohtNh.woff +0 -0
  888. package/out/web/assets/prompt-vietnamese-600-normal-7QWjJBsF.woff2 +0 -0
  889. package/out/web/assets/sarabun-latin-400-normal-DmguIS-7.woff +0 -0
  890. package/out/web/assets/sarabun-latin-400-normal-URPBxl-K.woff2 +0 -0
  891. package/out/web/assets/sarabun-latin-500-normal-BjUTcdxu.woff2 +0 -0
  892. package/out/web/assets/sarabun-latin-500-normal-CJexTAnl.woff +0 -0
  893. package/out/web/assets/sarabun-latin-600-normal-9FonlkKx.woff +0 -0
  894. package/out/web/assets/sarabun-latin-600-normal-DMD3TROr.woff2 +0 -0
  895. package/out/web/assets/sarabun-latin-ext-400-normal-BbP7GpLL.woff2 +0 -0
  896. package/out/web/assets/sarabun-latin-ext-400-normal-BsmTn0SS.woff +0 -0
  897. package/out/web/assets/sarabun-latin-ext-500-normal-Cot16Bd2.woff +0 -0
  898. package/out/web/assets/sarabun-latin-ext-500-normal-DE1LysqI.woff2 +0 -0
  899. package/out/web/assets/sarabun-latin-ext-600-normal-0tXu9x_i.woff +0 -0
  900. package/out/web/assets/sarabun-latin-ext-600-normal-BRxjKUQ6.woff2 +0 -0
  901. package/out/web/assets/sarabun-thai-400-normal-C2DaJlKK.woff2 +0 -0
  902. package/out/web/assets/sarabun-thai-400-normal-CH-UbWgg.woff +0 -0
  903. package/out/web/assets/sarabun-thai-500-normal-BVssjame.woff2 +0 -0
  904. package/out/web/assets/sarabun-thai-500-normal-C3iDLEzi.woff +0 -0
  905. package/out/web/assets/sarabun-thai-600-normal-3dIAQ-_s.woff2 +0 -0
  906. package/out/web/assets/sarabun-thai-600-normal-DWKeTe6a.woff +0 -0
  907. package/out/web/assets/sarabun-vietnamese-400-normal-DPfkSCMy.woff +0 -0
  908. package/out/web/assets/sarabun-vietnamese-400-normal-igdrzAGY.woff2 +0 -0
  909. package/out/web/assets/sarabun-vietnamese-500-normal-CpRVvGRU.woff +0 -0
  910. package/out/web/assets/sarabun-vietnamese-500-normal-Dpe7Br9m.woff2 +0 -0
  911. package/out/web/assets/sarabun-vietnamese-600-normal-BMbOgAW-.woff +0 -0
  912. package/out/web/assets/sarabun-vietnamese-600-normal-DkxrOlLB.woff2 +0 -0
  913. package/out/web/assets/stateDiagram-v2-4QOOHH4V-s6P9g2_b.js +0 -1
  914. package/out/web/assets/swimlanesDiagram-VK2B7HYN-CV9fOLrV.js +0 -8
  915. package/out/web/assets/tauri-CNrHQ2Gb.js +0 -3
  916. package/out/web/assets/window-DkIhgI7B.js +0 -1
  917. package/skills-lock.json +0 -41
  918. package/src/renderer/public/Live2DCubismCore.js +0 -9
  919. package/src/renderer/src/companionWidget.ts +0 -95
  920. package/src/renderer/src/components/Live2DStage.tsx +0 -389
  921. package/src/renderer/src/components/ModelPanel.tsx +0 -154
  922. package/src/renderer/src/css/model.css +0 -627
  923. package/src/renderer/src-web/companionWidget.ts +0 -37
  924. package/src/renderer/src-web/components/ModelPanel.tsx +0 -12
  925. package/src/renderer/src-web/css/model.css +0 -628
@@ -1,3 +1,5 @@
1
+ const ANSI_BLUE: &str = crate::terminal_theme::ANSI_BLUE;
2
+
1
3
  use super::*;
2
4
 
3
5
  fn is_reachable(host: &str, port: u16) -> bool {
@@ -165,31 +167,11 @@ fn first_line_preview(path: &Path) -> String {
165
167
  }
166
168
  }
167
169
 
168
- fn print_rendered_markdown(text: &str) {
169
- let mut table_buffer: Vec<String> = Vec::new();
170
- for line in text.lines() {
171
- if crate::markdown::is_table_line(line) {
172
- table_buffer.push(line.to_string());
173
- } else {
174
- if !table_buffer.is_empty() {
175
- print!("{}", crate::markdown::render_markdown_table(&table_buffer));
176
- table_buffer.clear();
177
- }
178
- if let Some(h) = line.strip_prefix("## ") {
179
- println!("\n{BLUE}{h}{RESET}");
180
- } else if let Some(h) = line.strip_prefix("# ") {
181
- println!("\n{BLUE}{h}{RESET}");
182
- } else {
183
- println!("{line}");
184
- }
185
- }
186
- }
187
- if !table_buffer.is_empty() {
188
- print!("{}", crate::markdown::render_markdown_table(&table_buffer));
189
- }
190
- }
191
-
192
- async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Option<SlashResult> {
170
+ async fn execute_core_slash(
171
+ session: &mut InteractiveSession,
172
+ ui: &mut dyn CommandUi,
173
+ query: &str,
174
+ ) -> Option<SlashResult> {
193
175
  use mint_core::slash::{SlashEffect, SlashRequest, SlashResponse};
194
176
 
195
177
  let trimmed = query.trim();
@@ -202,6 +184,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
202
184
  input: trimmed.to_string(),
203
185
  cwd: Some(session.current_dir.to_string_lossy().to_string()),
204
186
  surface: Some("cli".to_string()),
187
+ chat_id: Some(session.chat_id.clone()),
205
188
  };
206
189
 
207
190
  if matches!(
@@ -213,28 +196,17 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
213
196
  | "/videomodels"
214
197
  | "/video-provider"
215
198
  ) {
216
- print!("{DIM}Fetching available models...{RESET}\r");
217
- let _ = std::io::Write::flush(&mut std::io::stdout());
199
+ ui.set_status(vec!["Fetching available models…".into()]);
218
200
  }
219
201
 
220
202
  let response = mint_core::slash::execute_async(&req, &mut session.config).await;
221
-
222
- if matches!(
223
- cmd,
224
- "/models"
225
- | "/image-models"
226
- | "/image-provider"
227
- | "/video-models"
228
- | "/videomodels"
229
- | "/video-provider"
230
- ) {
231
- print!("\r\x1b[2K");
232
- let _ = std::io::Write::flush(&mut std::io::stdout());
233
- }
203
+ ui.clear_status();
234
204
 
235
205
  match response {
236
206
  SlashResponse::Message { markdown } => {
237
- print_rendered_markdown(&markdown);
207
+ if !markdown.is_empty() {
208
+ ui.push_command_output(markdown);
209
+ }
238
210
  Some(SlashResult::Handled)
239
211
  }
240
212
  SlashResponse::Applied { markdown, effects } => {
@@ -246,7 +218,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
246
218
  }
247
219
  SlashEffect::ProviderChanged { display } => {
248
220
  save_needed = true;
249
- println!("Switched to: {MINT}{display}{RESET}\n");
221
+ ui.push_notice(format!("Switched to: {display}"));
250
222
  }
251
223
  SlashEffect::FastModeChanged { enabled } => {
252
224
  session.fast_mode = *enabled;
@@ -264,6 +236,11 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
264
236
  }
265
237
  SlashEffect::HistoryCleared => {
266
238
  session.history.clear();
239
+ ui.clear_transcript();
240
+ }
241
+ SlashEffect::WorkspaceFilesChanged => {}
242
+ SlashEffect::ThinkingChanged { .. } => {
243
+ save_needed = true;
267
244
  }
268
245
  }
269
246
  }
@@ -271,7 +248,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
271
248
  let _ = mint_core::save_config(&session.config);
272
249
  }
273
250
  if !markdown.is_empty() {
274
- print_rendered_markdown(&markdown);
251
+ ui.push_command_output(markdown);
275
252
  }
276
253
  Some(SlashResult::Handled)
277
254
  }
@@ -280,20 +257,50 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
280
257
  title,
281
258
  options,
282
259
  } => {
283
- let labels: Vec<String> = options.iter().map(|o| o.label.clone()).collect();
284
- let default_choice = labels.first().cloned().unwrap_or_default();
285
- match prompt_interactive_select(&title, &labels, &default_choice) {
286
- Ok(Some(selected_label)) => {
287
- if let Some(opt) = options.iter().find(|o| o.label == selected_label) {
288
- let next_input = format!("{command} {}", opt.value);
289
- return Box::pin(handle_slash_command(session, &next_input)).await;
260
+ let choice_items: Vec<ChoiceItem> = options
261
+ .iter()
262
+ .map(|opt| ChoiceItem::new(opt.label.clone(), opt.value.clone()))
263
+ .collect();
264
+ let (heading, body) = title.split_once('\n').unwrap_or((&title, ""));
265
+ match ui.prompt_choice(heading, body, &choice_items) {
266
+ Ok(Some(idx)) => {
267
+ let opt = &options[idx];
268
+ if opt.value == "__action:search" {
269
+ let text = ui
270
+ .prompt_text("Search models", "Search query", None)
271
+ .ok()
272
+ .flatten()
273
+ .unwrap_or_default();
274
+ let trimmed = text.trim();
275
+ if !trimmed.is_empty() {
276
+ let next_input = format!("{command} __action:search:{trimmed}");
277
+ return Box::pin(handle_slash_command(session, ui, &next_input)).await;
278
+ }
279
+ ui.push_notice("Cancelled.");
280
+ return Some(SlashResult::Handled);
281
+ }
282
+ if opt.value == "__action:custom" {
283
+ let custom = ui
284
+ .prompt_text("Custom model", "Enter custom model ID", None)
285
+ .ok()
286
+ .flatten()
287
+ .unwrap_or_default();
288
+ let trimmed = custom.trim();
289
+ if !trimmed.is_empty() {
290
+ let next_input = format!("{command} {trimmed}");
291
+ return Box::pin(handle_slash_command(session, ui, &next_input)).await;
292
+ }
293
+ ui.push_notice("Cancelled.");
294
+ return Some(SlashResult::Handled);
290
295
  }
296
+ let next_input = format!("{command} {}", opt.value);
297
+ return Box::pin(handle_slash_command(session, ui, &next_input)).await;
291
298
  }
292
299
  Ok(None) => {
293
- println!("Cancelled.\n");
300
+ ui.push_notice("Command cancelled");
294
301
  }
295
302
  Err(e) => {
296
- println!("{ERROR}Selection error:{RESET} {e}\n");
303
+ ui.report_error(&e);
297
304
  }
298
305
  }
299
306
  Some(SlashResult::Handled)
@@ -308,7 +315,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
308
315
  }
309
316
  SlashResponse::Navigate { markdown, .. } => {
310
317
  if !markdown.is_empty() {
311
- print_rendered_markdown(&markdown);
318
+ ui.push_command_output(markdown);
312
319
  }
313
320
  Some(SlashResult::Handled)
314
321
  }
@@ -320,6 +327,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
320
327
  /// Route `/…` commands. Returns `None` if the input is not a slash command.
321
328
  pub async fn handle_slash_command(
322
329
  session: &mut InteractiveSession,
330
+ ui: &mut dyn CommandUi,
323
331
  query: &str,
324
332
  ) -> Option<SlashResult> {
325
333
  let trimmed = query.trim();
@@ -333,14 +341,46 @@ pub async fn handle_slash_command(
333
341
  .map(|(c, r)| (c, r.trim()))
334
342
  .unwrap_or((trimmed, ""));
335
343
 
344
+ if cmd == "/mods" && rest == "run-tools" {
345
+ let chat = mint_core::scoped_chat_id(
346
+ &session.chat_id,
347
+ Some(&session.current_dir.to_string_lossy()),
348
+ );
349
+ match mint_core::mods::pending_requests(&session.current_dir, &chat) {
350
+ Ok(requests) => {
351
+ for request in requests {
352
+ let body = format!(
353
+ "{} requests {}\n{}",
354
+ request["modId"],
355
+ request["action"],
356
+ serde_json::to_string_pretty(&request["input"]).unwrap_or_default()
357
+ );
358
+ if ui.prompt_confirm(&body, false).unwrap_or(false) {
359
+ let operation=serde_json::from_value(serde_json::json!({"workspacePath":session.current_dir,"chatId":chat,"action":"run_request","command":request["id"],"grantPermissions":true})).unwrap();
360
+ if let Err(error) = mint_core::mods::operate_async(operation).await {
361
+ ui.push_command_output(error);
362
+ }
363
+ }
364
+ }
365
+ }
366
+ Err(error) => ui.push_command_output(error),
367
+ }
368
+ return Some(SlashResult::Handled);
369
+ }
370
+
336
371
  // Check CLI-specific commands and interactive bare wizards first:
337
372
  let is_cli_only_or_wizard = matches!(
338
373
  cmd,
339
374
  "/plan"
375
+ | "/thought"
376
+ | "/think"
377
+ | "/resume"
378
+ | "/branch"
340
379
  | "/bg"
341
380
  | "/jobs"
342
381
  | "/shells"
343
382
  | "/palette"
383
+ | "/theme"
344
384
  | "/image"
345
385
  | "/paste"
346
386
  | "/avatar"
@@ -352,28 +392,33 @@ pub async fn handle_slash_command(
352
392
  && rest.is_empty());
353
393
 
354
394
  if !is_cli_only_or_wizard {
355
- if let Some(res) = execute_core_slash(session, trimmed).await {
395
+ if let Some(res) = execute_core_slash(session, ui, trimmed).await {
356
396
  return Some(res);
357
397
  }
358
398
  }
359
399
 
360
400
  match cmd {
361
401
  "/help" => {
362
- println!("\n{BLUE}────────────────────────────────────────────{RESET}");
363
- println!("{MINT} Mint Interactive Commands{RESET}");
364
- println!("{BLUE}────────────────────────────────────────────{RESET}");
402
+ let mut lines = Vec::new();
403
+ lines.push(format!(
404
+ "{BLUE}────────────────────────────────────────────{RESET}"
405
+ ));
406
+ lines.push(format!("{MINT} Mint Interactive Commands{RESET}"));
407
+ lines.push(format!(
408
+ "{BLUE}────────────────────────────────────────────{RESET}"
409
+ ));
365
410
  for spec in SLASH_COMMANDS.iter() {
366
411
  let label = if spec.usage.is_empty() {
367
412
  spec.token.to_string()
368
413
  } else {
369
414
  format!("{} {}", spec.token, spec.usage)
370
415
  };
371
- println!(
416
+ lines.push(format!(
372
417
  " {MINT}{:<30}{RESET} {DIM}{}{RESET}",
373
418
  label, spec.description
374
- );
419
+ ));
375
420
  }
376
- println!();
421
+ ui.push_command_output(lines.join("\n"));
377
422
  Some(SlashResult::Handled)
378
423
  }
379
424
 
@@ -384,34 +429,20 @@ pub async fn handle_slash_command(
384
429
  "/fast" => {
385
430
  let choice = if rest.is_empty() {
386
431
  let options = vec![
387
- "on (hide thinking traces)".to_string(),
388
- "off (show thinking traces)".to_string(),
432
+ ChoiceItem::new("on (hide thinking traces)", "on"),
433
+ ChoiceItem::new("off (show thinking traces)", "off"),
389
434
  ];
390
- let current = if session.fast_mode {
391
- &options[0]
392
- } else {
393
- &options[1]
394
- };
395
- match prompt_interactive_select("Select Fast Mode", &options, current) {
396
- Ok(Some(sel)) => {
397
- if sel.starts_with("on") {
398
- Some(true)
399
- } else {
400
- Some(false)
401
- }
402
- }
403
- Ok(None) => None,
404
- Err(e) => {
405
- println!("{ERROR}Error selecting fast mode:{RESET} {e}\n");
406
- None
407
- }
435
+ match ui.prompt_choice("Select Fast Mode", "", &options) {
436
+ Ok(Some(0)) => Some(true),
437
+ Ok(Some(1)) => Some(false),
438
+ _ => None,
408
439
  }
409
440
  } else {
410
441
  match rest {
411
442
  "on" => Some(true),
412
443
  "off" => Some(false),
413
444
  _ => {
414
- println!("{WARN}/fast usage: /fast [on|off]{RESET}\n");
445
+ ui.push_notice("/fast usage: /fast [on|off]");
415
446
  None
416
447
  }
417
448
  }
@@ -420,16 +451,82 @@ pub async fn handle_slash_command(
420
451
  if let Some(mode) = choice {
421
452
  session.fast_mode = mode;
422
453
  if session.fast_mode {
423
- println!("{DIM}[Fast] mode ON — thinking traces hidden{RESET}\n");
454
+ ui.push_notice("[Fast] mode ON — thinking traces hidden");
424
455
  } else {
425
- println!("{DIM}[Fast] mode OFF{RESET}\n");
456
+ ui.push_notice("[Fast] mode OFF");
426
457
  }
427
458
  }
428
459
  Some(SlashResult::Handled)
429
460
  }
430
461
 
462
+ "/resume" => {
463
+ let target_session_id = if rest.is_empty() {
464
+ prompt_resume_session_picker_with_ui(ui, &session.current_dir, &session.chat_id)
465
+ .ok()
466
+ .flatten()
467
+ } else {
468
+ let trimmed = rest.trim();
469
+ let candidate =
470
+ if !trimmed.starts_with("cli::") && trimmed != mint_core::CHAT_CLI_ID {
471
+ format!("cli::{trimmed}")
472
+ } else {
473
+ trimmed.to_string()
474
+ };
475
+ if let Ok(memory) = MemoryStore::open_default()
476
+ && let Ok(sessions) = memory.list_chat_sessions()
477
+ && sessions.iter().any(|s| s.id == candidate)
478
+ {
479
+ Some(candidate)
480
+ } else {
481
+ Some(trimmed.to_string())
482
+ }
483
+ };
484
+
485
+ if let Some(target_id) = target_session_id {
486
+ if target_id == session.chat_id {
487
+ ui.push_notice(format!("Already in session '{target_id}'."));
488
+ } else {
489
+ session.chat_id = target_id.clone();
490
+ mint_core::live_sync::update_live_sync_chat_id(&session.chat_id);
491
+ ui.reload_transcript(&session.chat_id, &session.current_dir);
492
+ if let Ok(memory) = MemoryStore::open_default() {
493
+ let sessions = memory.list_chat_sessions().unwrap_or_default();
494
+ let title = sessions
495
+ .iter()
496
+ .find(|s| s.id == session.chat_id)
497
+ .map(|s| s.title.as_str())
498
+ .unwrap_or("Conversation");
499
+ ui.push_notice(format!("Switched to session: {title} ({target_id})"));
500
+ if let Ok(recent) = memory.get_session_preview(&session.chat_id, 2)
501
+ && let Some(last) = recent.first()
502
+ {
503
+ let snippet = crate::interactive::truncate_utf8(&last.user_text, 80);
504
+ ui.push_notice(format!("Last turn: {snippet}"));
505
+ }
506
+ } else {
507
+ ui.push_notice(format!("Switched to session: {target_id}"));
508
+ }
509
+ }
510
+ } else {
511
+ ui.push_notice("Resume cancelled.");
512
+ }
513
+ Some(SlashResult::Handled)
514
+ }
515
+
431
516
  "/rewind" => {
432
- let checkpoints = mint_core::git::list_checkpoints(mint_core::CHAT_CLI_ID);
517
+ let (target_rewind_chat_id, checkpoints) = {
518
+ let local_cps = mint_core::git::list_checkpoints(&session.chat_id);
519
+ if local_cps.is_empty() && session.chat_id != mint_core::CHAT_CLI_ID {
520
+ let fallback_cps = mint_core::git::list_checkpoints(mint_core::CHAT_CLI_ID);
521
+ if !fallback_cps.is_empty() {
522
+ (mint_core::CHAT_CLI_ID.to_string(), fallback_cps)
523
+ } else {
524
+ (session.chat_id.clone(), local_cps)
525
+ }
526
+ } else {
527
+ (session.chat_id.clone(), local_cps)
528
+ }
529
+ };
433
530
  if rest.is_empty() {
434
531
  if checkpoints.is_empty() {
435
532
  println!("\n{DIM}No git checkpoints recorded for this CLI session yet.{RESET}");
@@ -472,7 +569,7 @@ pub async fn handle_slash_command(
472
569
  Ok(step) => {
473
570
  match mint_core::git::rollback_to_step(
474
571
  &session.current_dir,
475
- mint_core::CHAT_CLI_ID,
572
+ &target_rewind_chat_id,
476
573
  step,
477
574
  ) {
478
575
  Ok(msg) => {
@@ -493,67 +590,125 @@ pub async fn handle_slash_command(
493
590
  }
494
591
  }
495
592
 
593
+ "/theme" => {
594
+ let current = crate::terminal_theme::TuiTheme::from_config(&session.config.tui_theme);
595
+ match ui.prompt_theme(current.index()) {
596
+ Ok(Some(index)) => {
597
+ let selected = crate::terminal_theme::TuiTheme::from_index(index);
598
+ let previous = std::mem::replace(
599
+ &mut session.config.tui_theme,
600
+ selected.config_value().to_owned(),
601
+ );
602
+ if let Err(error) = mint_core::save_config(&session.config) {
603
+ session.config.tui_theme = previous;
604
+ ui.report_error(&error.into());
605
+ } else {
606
+ ui.set_theme(selected);
607
+ ui.push_notice(format!("Theme: {}", selected.label()));
608
+ }
609
+ }
610
+ Ok(None) => {}
611
+ Err(error) => ui.report_error(&error),
612
+ }
613
+ Some(SlashResult::Handled)
614
+ }
615
+
496
616
  "/palette" => {
497
617
  let options = vec![
498
- "Quick Actions Menu".to_string(),
499
- "Switch AI Model (/models)".to_string(),
500
- "Toggle Plan Mode (/plan)".to_string(),
501
- "Toggle Fast Mode (/fast)".to_string(),
502
- "Git Checkpoint Rollback (/rewind)".to_string(),
503
- "Manage MCP Servers (/mcp)".to_string(),
504
- "Manage Memory & Facts (/memory)".to_string(),
505
- "Change Directory (/cd)".to_string(),
506
- "Clear Conversation (/clear)".to_string(),
507
- "Help & Documentation (/help)".to_string(),
618
+ ChoiceItem::with_description(
619
+ "Quick Actions Menu",
620
+ "Show general quick actions",
621
+ "tip",
622
+ ),
623
+ ChoiceItem::with_description(
624
+ "Switch AI Model (/models)",
625
+ "Change active model or provider",
626
+ "/models",
627
+ ),
628
+ ChoiceItem::with_description(
629
+ "Toggle Plan Mode (/plan)",
630
+ "Turn plan mode on or off",
631
+ "/plan",
632
+ ),
633
+ ChoiceItem::with_description(
634
+ "Toggle Fast Mode (/fast)",
635
+ "Turn fast mode on or off",
636
+ "/fast",
637
+ ),
638
+ ChoiceItem::with_description(
639
+ "Git Checkpoint Rollback (/rewind)",
640
+ "Undo recent AI edits",
641
+ "/rewind",
642
+ ),
643
+ ChoiceItem::with_description(
644
+ "Manage MCP Servers (/mcp)",
645
+ "Add, remove, or configure tools",
646
+ "/mcp",
647
+ ),
648
+ ChoiceItem::with_description(
649
+ "Manage Memory & Facts (/memory)",
650
+ "View or clear project memory",
651
+ "/memory",
652
+ ),
653
+ ChoiceItem::with_description(
654
+ "Change Directory (/cd)",
655
+ "Change workspace directory",
656
+ "/cd",
657
+ ),
658
+ ChoiceItem::with_description(
659
+ "Clear Conversation (/clear)",
660
+ "Clear current conversation history",
661
+ "/clear",
662
+ ),
663
+ ChoiceItem::with_description(
664
+ "Help & Documentation (/help)",
665
+ "List all available slash commands",
666
+ "/help",
667
+ ),
508
668
  ];
509
- match prompt_interactive_select("Universal Command Palette", &options, &options[0]) {
510
- Ok(Some(sel)) => {
511
- let next_cmd = if sel.contains("/models") {
512
- "/models"
513
- } else if sel.contains("/plan") {
514
- "/plan"
515
- } else if sel.contains("/fast") {
516
- "/fast"
517
- } else if sel.contains("/rewind") {
518
- "/rewind"
519
- } else if sel.contains("/mcp") {
520
- "/mcp"
521
- } else if sel.contains("/memory") {
522
- "/memory"
523
- } else if sel.contains("/clear") {
524
- "/clear"
525
- } else if sel.contains("/help") {
526
- "/help"
527
- } else {
528
- println!(
529
- "{MINT}Tip:{RESET} Type {BLUE}/<command>{RESET} or press {MINT}Tab{RESET} to autocomplete commands anytime.\n"
669
+ match ui.prompt_choice(
670
+ "Universal Command Palette",
671
+ "Select command to run",
672
+ &options,
673
+ ) {
674
+ Ok(Some(idx)) => {
675
+ let val = &options[idx].value;
676
+ if val == "tip" {
677
+ ui.push_notice(
678
+ "Tip: Type /<command> or press Tab to autocomplete commands anytime.",
530
679
  );
531
- return Some(SlashResult::Handled);
532
- };
533
- Box::pin(handle_slash_command(session, next_cmd)).await
534
- }
535
- Ok(None) => Some(SlashResult::Handled),
536
- Err(e) => {
537
- println!("{ERROR}Palette error:{RESET} {e}\n");
538
- Some(SlashResult::Handled)
680
+ Some(SlashResult::Handled)
681
+ } else {
682
+ Box::pin(handle_slash_command(session, ui, val)).await
683
+ }
539
684
  }
685
+ _ => Some(SlashResult::Handled),
540
686
  }
541
687
  }
542
688
 
689
+ "/thought" | "/think" => {
690
+ if let Some(record) = get_last_thought() {
691
+ let _ = show_thought_viewer(&record.thought, &record.elapsed_str);
692
+ } else {
693
+ ui.push_notice("No recent thought process recorded for this turn.");
694
+ }
695
+ Some(SlashResult::Handled)
696
+ }
697
+
543
698
  "/plan" if rest == "list" => {
544
699
  let files = list_plan_files(&session.current_dir);
545
700
  if files.is_empty() {
546
- println!("{DIM}No saved plans yet.{RESET}\n");
701
+ ui.push_notice("No saved plans yet.");
547
702
  } else {
548
- println!("\n{BLUE}Saved plans:{RESET}");
703
+ let mut lines = vec![format!("{BLUE}Saved plans:{RESET}")];
549
704
  for (idx, path) in files.iter().enumerate() {
550
- println!(
705
+ lines.push(format!(
551
706
  " {DIM}[{}]{RESET} {BLUE}{}{RESET}",
552
707
  idx + 1,
553
708
  first_line_preview(path)
554
- );
709
+ ));
555
710
  }
556
- println!();
711
+ ui.push_command_output(lines.join("\n"));
557
712
  }
558
713
  Some(SlashResult::Handled)
559
714
  }
@@ -572,12 +727,12 @@ pub async fn handle_slash_command(
572
727
  };
573
728
  match found {
574
729
  Some(path) => match std::fs::read_to_string(path) {
575
- Ok(content) => println!("\n{content}\n"),
576
- Err(e) => println!("{ERROR}Could not read plan file:{RESET} {e}\n"),
730
+ Ok(content) => ui.push_command_output(content),
731
+ Err(e) => ui.push_notice(format!("Could not read plan file: {e}")),
577
732
  },
578
- None => println!(
579
- "{WARN}No saved plan matching \"{arg}\" — run /plan list to see saved plans.{RESET}\n"
580
- ),
733
+ None => ui.push_notice(format!(
734
+ "No saved plan matching \"{arg}\" — run /plan list to see saved plans."
735
+ )),
581
736
  }
582
737
  Some(SlashResult::Handled)
583
738
  }
@@ -585,27 +740,13 @@ pub async fn handle_slash_command(
585
740
  "/plan" => {
586
741
  let choice = if rest.is_empty() {
587
742
  let options = vec![
588
- "on (read-only until plan is approved)".to_string(),
589
- "off (edits/shell run immediately)".to_string(),
743
+ ChoiceItem::new("on (read-only until plan is approved)", "on"),
744
+ ChoiceItem::new("off (edits/shell run immediately)", "off"),
590
745
  ];
591
- let current = if session.plan_mode {
592
- &options[0]
593
- } else {
594
- &options[1]
595
- };
596
- match prompt_interactive_select("Select Plan Mode", &options, current) {
597
- Ok(Some(sel)) => {
598
- if sel.starts_with("on") {
599
- Some(true)
600
- } else {
601
- Some(false)
602
- }
603
- }
604
- Ok(None) => None,
605
- Err(e) => {
606
- println!("{ERROR}Error selecting plan mode:{RESET} {e}\n");
607
- None
608
- }
746
+ match ui.prompt_choice("Select Plan Mode", "", &options) {
747
+ Ok(Some(0)) => Some(true),
748
+ Ok(Some(1)) => Some(false),
749
+ _ => None,
609
750
  }
610
751
  } else {
611
752
  match rest {
@@ -613,9 +754,7 @@ pub async fn handle_slash_command(
613
754
  "off" => Some(false),
614
755
  task => {
615
756
  session.plan_mode = true;
616
- println!(
617
- "{DIM}[Plan] mode ON — agent will investigate read-only and present a plan before editing{RESET}\n"
618
- );
757
+ ui.push_notice("[Plan] mode ON — agent will investigate read-only and present a plan before editing");
619
758
  return Some(SlashResult::ForwardToAgent(task.to_string()));
620
759
  }
621
760
  }
@@ -624,11 +763,9 @@ pub async fn handle_slash_command(
624
763
  if let Some(mode) = choice {
625
764
  session.plan_mode = mode;
626
765
  if session.plan_mode {
627
- println!(
628
- "{DIM}[Plan] mode ON — agent will investigate read-only and present a plan via exit_plan_mode before editing files or running commands{RESET}\n"
629
- );
766
+ ui.push_notice("[Plan] mode ON — agent will investigate read-only and present a plan via exit_plan_mode before editing files or running commands");
630
767
  } else {
631
- println!("{DIM}[Plan] mode OFF{RESET}\n");
768
+ ui.push_notice("[Plan] mode OFF");
632
769
  }
633
770
  }
634
771
  Some(SlashResult::Handled)
@@ -937,7 +1074,7 @@ pub async fn handle_slash_command(
937
1074
  session.config.auto_skill_writing = rest == "on";
938
1075
  match mint_core::save_config(&session.config) {
939
1076
  Ok(()) => println!(
940
- "{DIM}Auto skill writing set to: {}{RESET}\n",
1077
+ "{DIM}Skill suggestions (approval required to save) set to: {}{RESET}\n",
941
1078
  if session.config.auto_skill_writing {
942
1079
  "Enabled"
943
1080
  } else {
@@ -950,7 +1087,7 @@ pub async fn handle_slash_command(
950
1087
  println!("{WARN}Usage: /autoskill [on|off]{RESET}\n");
951
1088
  } else {
952
1089
  println!(
953
- "{DIM}When enabled, the agent may write a new .agents/skills/<name>/SKILL.md after finishing a non-trivial, reusable task.{RESET}"
1090
+ "{DIM}When enabled, the agent proposes a skill after a reusable task. Each proposed diff needs your approval before saving.{RESET}"
954
1091
  );
955
1092
  let options = vec!["on (enable)".to_string(), "off (disable)".to_string()];
956
1093
  let current = if session.config.auto_skill_writing {
@@ -958,12 +1095,12 @@ pub async fn handle_slash_command(
958
1095
  } else {
959
1096
  &options[1]
960
1097
  };
961
- match prompt_interactive_select("Auto Skill Writing", &options, current) {
1098
+ match prompt_interactive_select("Skill Suggestions", &options, current) {
962
1099
  Ok(Some(sel)) => {
963
1100
  session.config.auto_skill_writing = sel.starts_with("on");
964
1101
  match mint_core::save_config(&session.config) {
965
1102
  Ok(()) => println!(
966
- "{DIM}Auto skill writing set to: {}{RESET}\n",
1103
+ "{DIM}Skill suggestions (approval required to save) set to: {}{RESET}\n",
967
1104
  if session.config.auto_skill_writing {
968
1105
  "Enabled"
969
1106
  } else {
@@ -1438,18 +1575,15 @@ pub async fn handle_slash_command(
1438
1575
 
1439
1576
  "/bg" => {
1440
1577
  if rest.is_empty() {
1441
- println!("{ERROR}Usage:{RESET} /bg <query>\n");
1578
+ ui.push_notice("Usage: /bg <query>");
1442
1579
  return Some(SlashResult::Handled);
1443
1580
  }
1444
1581
  let id = session
1445
1582
  .jobs
1446
1583
  .spawn(&session.config, &session.current_dir, rest);
1447
- println!(
1448
- "{DIM}Started background job #{id}. Keep chatting — check on it with {RESET}/jobs{DIM}, or view its result with {RESET}/jobs show {id}{DIM}.{RESET}"
1449
- );
1450
- println!(
1451
- "{DIM}Note: /bg runs non-interactively, so file writes/shell/plugin/MCP actions needing approval are declined automatically.{RESET}\n"
1452
- );
1584
+ ui.push_notice(format!(
1585
+ "Started background job #{id}. Keep chatting — check on it with /jobs, or view its result with /jobs show {id}."
1586
+ ));
1453
1587
  Some(SlashResult::Handled)
1454
1588
  }
1455
1589
 
@@ -1457,9 +1591,9 @@ pub async fn handle_slash_command(
1457
1591
  if rest.is_empty() {
1458
1592
  let list = session.jobs.list();
1459
1593
  if list.is_empty() {
1460
- println!("{DIM}No background jobs yet. Start one with /bg <query>.{RESET}\n");
1594
+ ui.push_notice("No background jobs yet. Start one with /bg <query>.");
1461
1595
  } else {
1462
- println!("\n{BLUE}Background jobs{RESET}");
1596
+ let mut lines = vec![format!("{BLUE}Background jobs{RESET}")];
1463
1597
  for job in &list {
1464
1598
  let status_str = match job.status {
1465
1599
  JobStatus::Running => format!("{MINT}running{RESET}"),
@@ -1473,13 +1607,13 @@ pub async fn handle_slash_command(
1473
1607
  } else {
1474
1608
  ""
1475
1609
  };
1476
- println!(
1610
+ lines.push(format!(
1477
1611
  " #{:<3} [{status_str}] {DIM}{:>4}s{RESET} {preview}{suffix}",
1478
1612
  job.id,
1479
1613
  job.elapsed_secs(),
1480
- );
1614
+ ));
1481
1615
  }
1482
- println!();
1616
+ ui.push_command_output(lines.join("\n"));
1483
1617
  }
1484
1618
  return Some(SlashResult::Handled);
1485
1619
  }
@@ -1492,40 +1626,52 @@ pub async fn handle_slash_command(
1492
1626
  match sub {
1493
1627
  "show" => match arg.parse::<u32>().ok().and_then(|id| session.jobs.show(id)) {
1494
1628
  Some(job) => {
1495
- println!("\n{BLUE}Job #{} — {}{RESET}", job.id, job.status);
1496
- println!("{DIM}Query:{RESET} {}", job.query);
1629
+ let mut lines = vec![
1630
+ format!("{BLUE}Job #{} — {}{RESET}", job.id, job.status),
1631
+ format!("{DIM}Query:{RESET} {}", job.query),
1632
+ ];
1497
1633
  match job.result {
1498
- Some(result) => println!("\n{}\n", result),
1499
- None => println!("{DIM}(still running — no result yet){RESET}\n"),
1634
+ Some(result) => {
1635
+ lines.push(String::new());
1636
+ lines.push(result);
1637
+ }
1638
+ None => {
1639
+ lines.push(format!("{DIM}(still running — no result yet){RESET}"))
1640
+ }
1500
1641
  }
1642
+ ui.push_command_output(lines.join("\n"));
1501
1643
  }
1502
- None => println!("{ERROR}No such job.{RESET}\n"),
1644
+ None => ui.push_notice("No such job."),
1503
1645
  },
1504
1646
  "cancel" => match arg.parse::<u32>().ok() {
1505
1647
  Some(id) if session.jobs.cancel(id) => {
1506
- println!("{DIM}Cancelling job #{id}...{RESET}\n");
1648
+ ui.push_notice(format!("Cancelling job #{id}..."));
1507
1649
  }
1508
- _ => println!("{ERROR}No such running job.{RESET}\n"),
1650
+ _ => ui.push_notice("No such running job."),
1509
1651
  },
1510
- _ => println!("{ERROR}Usage:{RESET} /jobs [show <id>|cancel <id>]\n"),
1652
+ _ => ui.push_notice("Usage: /jobs [show <id>|cancel <id>]"),
1511
1653
  }
1512
1654
  Some(SlashResult::Handled)
1513
1655
  }
1514
1656
 
1515
1657
  "/shells" => {
1516
- if rest.is_empty() {
1517
- let jobs = mint_core::bg_shell::list_jobs();
1658
+ if rest.is_empty() || rest == "all" {
1659
+ let jobs: Vec<_> = mint_core::bg_shell::list_jobs()
1660
+ .into_iter()
1661
+ .filter(|job| rest == "all" || job.workspace == session.current_dir)
1662
+ .collect();
1518
1663
  if jobs.is_empty() {
1519
- println!(
1520
- "{DIM}No background shell jobs. The agent starts one when it calls run_shell with background: true.{RESET}\n"
1664
+ ui.push_notice(
1665
+ "No background shell jobs. The agent starts one when it calls run_shell with background: true."
1521
1666
  );
1522
1667
  } else {
1523
- println!("\n{BLUE}Background shell jobs{RESET}");
1668
+ let mut lines = vec![format!("{BLUE}Background shell jobs{RESET}")];
1524
1669
  for job in &jobs {
1525
1670
  let status_str = match &job.status {
1526
1671
  mint_core::bg_shell::JobStatus::Running => {
1527
1672
  format!("{MINT}running{RESET}")
1528
1673
  }
1674
+ mint_core::bg_shell::JobStatus::Stopping => "stopping".to_string(),
1529
1675
  mint_core::bg_shell::JobStatus::Exited(0) => "exited(0)".to_string(),
1530
1676
  mint_core::bg_shell::JobStatus::Exited(code) => {
1531
1677
  format!("{ERROR}exited({code}){RESET}")
@@ -1543,12 +1689,17 @@ pub async fn handle_slash_command(
1543
1689
  } else {
1544
1690
  ""
1545
1691
  };
1546
- println!(
1547
- " {:<6} [{status_str}] {DIM}{:>4}s{RESET} {preview}{suffix}",
1548
- job.id, job.elapsed_secs,
1549
- );
1692
+ lines.push(format!(
1693
+ " {:<6} [{status_str}] {DIM}{:>4}s{RESET} {preview}{suffix} · {}",
1694
+ job.id,
1695
+ job.elapsed_secs,
1696
+ job.workspace.display(),
1697
+ ));
1698
+ if let Some(url) = &job.server_url {
1699
+ lines.push(format!(" Ready URL: {url}"));
1700
+ }
1550
1701
  }
1551
- println!();
1702
+ ui.push_command_output(lines.join("\n"));
1552
1703
  }
1553
1704
  return Some(SlashResult::Handled);
1554
1705
  }
@@ -1561,48 +1712,55 @@ pub async fn handle_slash_command(
1561
1712
  match sub {
1562
1713
  "show" if !arg.is_empty() => match mint_core::bg_shell::snapshot(arg) {
1563
1714
  Ok(job) => {
1564
- println!("\n{BLUE}Job {} — {}{RESET}", job.id, job.command);
1715
+ let mut lines =
1716
+ vec![format!("{BLUE}Job {} — {}{RESET}", job.id, job.command)];
1717
+ lines.push(format!("Status: {:?} · {}s", job.status, job.elapsed_secs));
1718
+ if let Some(url) = &job.server_url {
1719
+ lines.push(format!("Ready URL: {url}"));
1720
+ }
1565
1721
  if let Some(pid) = job.pid {
1566
- println!("{DIM}pid: {pid} cwd: {}{RESET}", job.cwd.display());
1722
+ lines.push(format!(
1723
+ "{DIM}pid: {pid} cwd: {}{RESET}",
1724
+ job.cwd.display()
1725
+ ));
1567
1726
  }
1568
- println!("{DIM}stdout:{RESET}\n{}", job.stdout);
1569
- println!("{DIM}stderr:{RESET}\n{}\n", job.stderr);
1727
+ lines.push(format!("{DIM}stdout:{RESET}\n{}", job.stdout));
1728
+ lines.push(format!("{DIM}stderr:{RESET}\n{}", job.stderr));
1729
+ if job.truncated {
1730
+ lines.push("[Older output truncated]".into());
1731
+ }
1732
+ ui.push_command_output(lines.join("\n"));
1570
1733
  }
1571
- Err(_) => println!("{ERROR}No such job.{RESET}\n"),
1734
+ Err(_) => ui.push_notice("No such job."),
1572
1735
  },
1573
- "kill" if !arg.is_empty() => match mint_core::bg_shell::kill_job(arg) {
1574
- Ok(msg) => println!("{DIM}{msg}{RESET}\n"),
1575
- Err(_) => println!("{ERROR}No such job.{RESET}\n"),
1736
+ "kill" | "stop" if !arg.is_empty() => match mint_core::bg_shell::kill_job(arg) {
1737
+ Ok(msg) => ui.push_notice(msg),
1738
+ Err(_) => ui.push_notice("No such job."),
1576
1739
  },
1577
- _ => println!("{ERROR}Usage:{RESET} /shells [show <id>|kill <id>]\n"),
1740
+ _ => ui.push_notice("Usage: /shells [all|show <id>|stop <id>|kill <id>]"),
1578
1741
  }
1579
1742
  Some(SlashResult::Handled)
1580
1743
  }
1581
1744
 
1582
1745
  "/clear" | "/reset" => {
1583
- let options = vec![
1584
- "No (keep history)".to_string(),
1585
- "Yes (clear history)".to_string(),
1586
- ];
1587
- let choice = match prompt_interactive_select(
1588
- "Clear conversation history?",
1589
- &options,
1590
- &options[0],
1591
- ) {
1592
- Ok(Some(sel)) => sel == options[1],
1593
- _ => false,
1594
- };
1595
-
1746
+ let choice = ui
1747
+ .prompt_confirm("Clear conversation history?", false)
1748
+ .unwrap_or(false);
1596
1749
  if choice {
1750
+ clear_last_thought();
1751
+ session.history.clear();
1752
+ ui.clear_transcript();
1597
1753
  if let Ok(memory) = MemoryStore::open_default() {
1598
- match memory.clear_interactions() {
1599
- Ok(count) => println!("{DIM}Cleared {count} interactions.{RESET}"),
1600
- Err(error) => println!("{ERROR}Memory error:{RESET} {error}"),
1754
+ match memory.clear_interactions_for_chat(&session.chat_id) {
1755
+ Ok(count) => {
1756
+ ui.push_notice(format!("Cleared {count} interactions for session."));
1757
+ }
1758
+ Err(error) => ui.push_notice(format!("Memory error: {error}")),
1601
1759
  }
1602
1760
  }
1603
- println!("{DIM}Conversation context cleared.{RESET}\n");
1761
+ ui.push_notice("Conversation context cleared.");
1604
1762
  } else {
1605
- println!("{DIM}Cancelled.{RESET}\n");
1763
+ ui.push_notice("Cancelled.");
1606
1764
  }
1607
1765
  Some(SlashResult::Handled)
1608
1766
  }
@@ -1616,7 +1774,7 @@ pub async fn handle_slash_command(
1616
1774
  session.current_dir = new_dir.canonicalize().unwrap_or(new_dir);
1617
1775
  println!(
1618
1776
  "{DIM}Workspace: {}{RESET}\n",
1619
- format_path_with_tilde(&session.current_dir)
1777
+ format_workspace_with_branch(&session.current_dir)
1620
1778
  );
1621
1779
  } else {
1622
1780
  println!("{ERROR}Directory not found:{RESET} {rest}\n");
@@ -1625,6 +1783,8 @@ pub async fn handle_slash_command(
1625
1783
  Some(SlashResult::Handled)
1626
1784
  }
1627
1785
 
1786
+ "/branch" => Some(handle_branch_slash(session, ui, rest)),
1787
+
1628
1788
  "/veo" => {
1629
1789
  if rest.is_empty() {
1630
1790
  println!(
@@ -1746,17 +1906,16 @@ pub async fn handle_slash_command(
1746
1906
  mask_data_uri: None,
1747
1907
  mode: Some("edit".to_owned()),
1748
1908
  };
1749
- match mint_core::generate_images(&session.config, &req).await {
1909
+ match mint_core::image_workflow::generate_and_save_images(&session.config, &req)
1910
+ .await
1911
+ {
1750
1912
  Ok(resp) => {
1751
1913
  if let Some(first) = resp.images.first() {
1752
1914
  println!(
1753
1915
  "{DIM}Image edited successfully using {} ({}){RESET}\n",
1754
1916
  resp.provider, resp.model
1755
1917
  );
1756
- println!(
1757
- "{DIM}Image data URI prefix: {}{RESET}\n",
1758
- &first.data_uri[..first.data_uri.len().min(60)]
1759
- );
1918
+ println!("{DIM}Saved image: {}{RESET}\n", first.path.display());
1760
1919
  }
1761
1920
  }
1762
1921
  Err(e) => {
@@ -1813,49 +1972,34 @@ pub async fn handle_slash_command(
1813
1972
 
1814
1973
  "/learn" => {
1815
1974
  if rest.is_empty() {
1816
- let mut skills = match MemoryStore::open_default() {
1817
- Ok(m) => match m.learned_skills(100) {
1818
- Ok(s) => s,
1819
- Err(e) => {
1820
- println!("{ERROR}Error loading custom skills: {e}{RESET}\n");
1821
- return Some(SlashResult::Handled);
1822
- }
1823
- },
1824
- Err(e) => {
1825
- println!("{ERROR}Memory error: {e}{RESET}\n");
1975
+ let unique_skills = match MemoryStore::open_default()
1976
+ .map_err(|e| e.to_string())
1977
+ .and_then(|memory| {
1978
+ mint_core::skill_inventory::list_available_skills(
1979
+ &memory,
1980
+ Some(&session.current_dir),
1981
+ )
1982
+ .map_err(|e| e.to_string())
1983
+ }) {
1984
+ Ok(skills) => skills,
1985
+ Err(error) => {
1986
+ println!("{ERROR}Error loading skills: {error}{RESET}\n");
1826
1987
  return Some(SlashResult::Handled);
1827
1988
  }
1828
1989
  };
1829
-
1830
- if let Some(home) = dirs::home_dir() {
1831
- let global_skills_path = home.join(".config").join("mint").join("mint-skills");
1832
- mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
1833
- }
1834
- let workspace_skills_path1 = session.current_dir.join(".agents").join("skills");
1835
- mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
1836
- let workspace_skills_path2 = session.current_dir.join("skills");
1837
- mint_core::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
1838
-
1839
- let mut unique_skills = std::collections::BTreeMap::new();
1840
- for skill in skills {
1841
- let loc = if skill.source_path.contains("/.config/mint/mint-skills") {
1842
- "Global"
1843
- } else if skill.source_path.contains("/skills")
1844
- || skill.source_path.contains("/.agents/skills")
1845
- {
1846
- "Workspace"
1847
- } else {
1848
- "Taught"
1849
- };
1850
- unique_skills.insert(skill.name.clone(), (skill, loc));
1851
- }
1852
-
1853
1990
  if unique_skills.is_empty() {
1854
1991
  println!("No learned skills found. Use '/learn <path>' to add one.\n");
1855
1992
  } else {
1856
1993
  println!("Learned AI Skills:");
1857
- for (name, (skill, loc)) in &unique_skills {
1858
- if *loc == "Taught" {
1994
+ for entry in &unique_skills {
1995
+ let skill = &entry.skill;
1996
+ let name = &skill.name;
1997
+ let loc = match entry.location {
1998
+ mint_core::skill_inventory::SkillLocation::Database => "Taught",
1999
+ mint_core::skill_inventory::SkillLocation::Global => "Global",
2000
+ mint_core::skill_inventory::SkillLocation::Workspace => "Workspace",
2001
+ };
2002
+ if loc == "Taught" {
1859
2003
  println!(" ● [{}] {}", loc, name);
1860
2004
  } else {
1861
2005
  println!(" ● [{}] {} (Source: {})", loc, name, skill.source_path);
@@ -2272,8 +2416,35 @@ pub async fn handle_slash_command(
2272
2416
  }
2273
2417
  }
2274
2418
  }
2419
+ "refresh" => match mint_core::refresh_linked_folder(args) {
2420
+ Ok(status) => println!("Indexed {} files in {args}.\n", status.indexed_files),
2421
+ Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
2422
+ },
2423
+ "notes" => match mint_core::list_linked_folder_notes(args) {
2424
+ Ok(notes) => {
2425
+ for note in notes {
2426
+ println!("{} [{}] {}", note.created_at, note.status, note.path);
2427
+ }
2428
+ }
2429
+ Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
2430
+ },
2431
+ "save" => {
2432
+ if let Some((name, content)) = args.split_once('|') {
2433
+ match mint_core::save_linked_folder_note(name.trim(), content.trim()) {
2434
+ Ok(note) if note.status == "saved" => {
2435
+ println!("Saved note: {}", note.path)
2436
+ }
2437
+ Ok(note) => {
2438
+ println!("{ERROR}{}{RESET}", note.error.unwrap_or_default())
2439
+ }
2440
+ Err(e) => println!("{ERROR}Error:{RESET} {e}"),
2441
+ }
2442
+ } else {
2443
+ println!("{WARN}/link save <name> | <text>{RESET}");
2444
+ }
2445
+ }
2275
2446
  _ => println!(
2276
- "{WARN}/link usage: list | add <name> | <path> | <description> | remove <name>{RESET}\n"
2447
+ "{WARN}/link usage: list | add <name> | <path> | <description> | remove <name> | refresh <name> | notes <name> | save <name> | <text>{RESET}\n"
2277
2448
  ),
2278
2449
  }
2279
2450
  Some(SlashResult::Handled)
@@ -2337,7 +2508,7 @@ pub async fn handle_slash_command(
2337
2508
  .split_once(char::is_whitespace)
2338
2509
  .map(|(c, a)| (c, a.trim()))
2339
2510
  .unwrap_or((rest, ""));
2340
- handle_mcp_slash(session, subcmd, args);
2511
+ handle_mcp_slash(session, ui, subcmd, args);
2341
2512
  Some(SlashResult::Handled)
2342
2513
  }
2343
2514
 
@@ -2356,36 +2527,41 @@ pub async fn handle_slash_command(
2356
2527
  })
2357
2528
  .map(|v| v.len())
2358
2529
  .unwrap_or(0);
2359
- println!("\n{BLUE}─ Session Stats ─────────────────────────{RESET}");
2360
- println!(" Provider : {MINT}{provider}{RESET}");
2361
- println!(" Model : {model}");
2362
- println!(
2530
+ let mut out = Vec::new();
2531
+ out.push(format!(
2532
+ "\n{BLUE}─ Session Stats ─────────────────────────{RESET}"
2533
+ ));
2534
+ out.push(format!(" Provider : {MINT}{provider}{RESET}"));
2535
+ out.push(format!(" Model : {model}"));
2536
+ out.push(format!(
2363
2537
  " Workspace: {}",
2364
- format_path_with_tilde(&session.current_dir)
2365
- );
2366
- println!(
2538
+ format_workspace_with_branch(&session.current_dir)
2539
+ ));
2540
+ out.push(format!(
2367
2541
  " Fast mode: {}",
2368
2542
  if session.fast_mode { "on" } else { "off" }
2369
- );
2370
- println!(" Memory : {interactions} interactions");
2543
+ ));
2544
+ out.push(format!(" Memory : {interactions} interactions"));
2371
2545
  if let Some(ref img_data) = session.pending_image {
2372
2546
  let count = img_data.split_whitespace().count();
2373
2547
  if count > 1 {
2374
- println!(" Images : {WARN}{} images attached{RESET}", count);
2548
+ out.push(format!(
2549
+ " Images : {WARN}{} images attached{RESET}",
2550
+ count
2551
+ ));
2375
2552
  } else {
2376
- println!(" Image : {WARN}attached{RESET}");
2553
+ out.push(format!(" Image : {WARN}attached{RESET}"));
2377
2554
  }
2378
2555
  }
2379
-
2380
- println!();
2556
+ ui.push_command_output(out.join("\n"));
2381
2557
  Some(SlashResult::Handled)
2382
2558
  }
2383
2559
 
2384
2560
  "/release-notes" => {
2385
- const RELEASE_NOTES: &str = include_str!("../../../../Release_Note.md");
2386
- println!("\n{BLUE}────────────────────────────────────────────{RESET}");
2387
- println!("{}", RELEASE_NOTES.trim());
2388
- println!("{BLUE}────────────────────────────────────────────{RESET}\n");
2561
+ ui.push_command_output(format!(
2562
+ "\n{BLUE}────────────────────────────────────────────{RESET}\n{}\n{BLUE}────────────────────────────────────────────{RESET}",
2563
+ mint_core::release_notes::text()
2564
+ ));
2389
2565
  Some(SlashResult::Handled)
2390
2566
  }
2391
2567
 
@@ -2541,7 +2717,7 @@ pub async fn handle_slash_command(
2541
2717
  } else if e.is_oauth {
2542
2718
  format!("{DIM}{:<22}{RESET}", plain_status)
2543
2719
  } else {
2544
- format!("\x1b[36m{:<22}\x1b[0m", plain_status)
2720
+ format!("{ANSI_BLUE}{:<22}\x1b[0m", plain_status)
2545
2721
  };
2546
2722
  println!(
2547
2723
  " {BLUE}❯ {:<18}{RESET}{} {DIM}{short_desc}{RESET}",
@@ -2824,6 +3000,136 @@ pub async fn handle_slash_command(
2824
3000
  }
2825
3001
  }
2826
3002
 
3003
+ fn handle_branch_slash(
3004
+ session: &mut InteractiveSession,
3005
+ ui: &mut dyn CommandUi,
3006
+ requested: &str,
3007
+ ) -> SlashResult {
3008
+ use mint_core::git::{BranchChange, BranchChangeOutcome, change_branch, read_branch_info};
3009
+
3010
+ let info = match read_branch_info(&session.current_dir) {
3011
+ Ok(info) if info.is_repository => info,
3012
+ Ok(_) => {
3013
+ ui.push_notice("Current workspace is not a Git repository.");
3014
+ return SlashResult::Handled;
3015
+ }
3016
+ Err(error) => {
3017
+ ui.push_notice(format!("Git error: {error}"));
3018
+ return SlashResult::Handled;
3019
+ }
3020
+ };
3021
+
3022
+ let (remote, branch) = if requested.is_empty() {
3023
+ let mut choices = info
3024
+ .branches
3025
+ .iter()
3026
+ .map(|branch| {
3027
+ (
3028
+ format!(
3029
+ "{branch}{}",
3030
+ if info.current_branch.as_deref() == Some(branch) {
3031
+ " (current)"
3032
+ } else {
3033
+ ""
3034
+ }
3035
+ ),
3036
+ false,
3037
+ branch.clone(),
3038
+ )
3039
+ })
3040
+ .collect::<Vec<_>>();
3041
+ choices.extend(
3042
+ info.remote_branches
3043
+ .iter()
3044
+ .map(|branch| (format!("{branch} (remote · track)"), true, branch.clone())),
3045
+ );
3046
+ if choices.is_empty() {
3047
+ ui.push_notice("No branches found in this workspace.");
3048
+ return SlashResult::Handled;
3049
+ }
3050
+ let items: Vec<ChoiceItem> = choices
3051
+ .iter()
3052
+ .map(|(label, is_remote, name)| {
3053
+ let desc = if *is_remote {
3054
+ "remote · track"
3055
+ } else {
3056
+ "local"
3057
+ };
3058
+ ChoiceItem::with_description(label.clone(), desc, name.clone())
3059
+ })
3060
+ .collect();
3061
+ match ui.prompt_choice("Switch Git Branch", "Select a branch to switch to", &items) {
3062
+ Ok(Some(idx)) => {
3063
+ let (_, is_remote, ref b) = choices[idx];
3064
+ (is_remote, b.clone())
3065
+ }
3066
+ _ => {
3067
+ ui.push_notice("Branch selection cancelled.");
3068
+ return SlashResult::Handled;
3069
+ }
3070
+ }
3071
+ } else if info.branches.iter().any(|branch| branch == requested) {
3072
+ (false, requested.to_string())
3073
+ } else if info
3074
+ .remote_branches
3075
+ .iter()
3076
+ .any(|branch| branch == requested)
3077
+ {
3078
+ (true, requested.to_string())
3079
+ } else {
3080
+ ui.push_notice(format!("Branch not found: {requested}"));
3081
+ return SlashResult::Handled;
3082
+ };
3083
+
3084
+ if !remote && info.current_branch.as_deref() == Some(branch.as_str()) {
3085
+ ui.push_notice(format!("Already on branch {branch}."));
3086
+ return SlashResult::Handled;
3087
+ }
3088
+
3089
+ let change = if remote {
3090
+ BranchChange::Track {
3091
+ remote_branch: branch.clone(),
3092
+ }
3093
+ } else {
3094
+ BranchChange::Switch {
3095
+ branch: branch.clone(),
3096
+ }
3097
+ };
3098
+ let result = match change_branch(&session.current_dir, &change, false) {
3099
+ Ok(BranchChangeOutcome::Changed { info }) => Ok(info),
3100
+ Ok(BranchChangeOutcome::ConfirmationRequired { .. }) => {
3101
+ match ui.prompt_confirm(
3102
+ "Workspace has uncommitted changes. Continue switching branches?",
3103
+ false,
3104
+ ) {
3105
+ Ok(true) => {
3106
+ change_branch(&session.current_dir, &change, true).and_then(|outcome| {
3107
+ match outcome {
3108
+ BranchChangeOutcome::Changed { info } => Ok(info),
3109
+ BranchChangeOutcome::ConfirmationRequired { .. } => {
3110
+ Err("Workspace still requires confirmation.".to_string())
3111
+ }
3112
+ }
3113
+ })
3114
+ }
3115
+ _ => {
3116
+ ui.push_notice("Branch change cancelled.");
3117
+ return SlashResult::Handled;
3118
+ }
3119
+ }
3120
+ }
3121
+ Err(error) => Err(error),
3122
+ };
3123
+ match result {
3124
+ Ok(next) => {
3125
+ let current = next.current_branch.as_deref().unwrap_or("detached HEAD");
3126
+ ui.push_notice(format!("Switched to {current}."));
3127
+ }
3128
+ Err(error) => ui.push_notice(format!("Git error: {error}")),
3129
+ }
3130
+ SlashResult::Handled
3131
+ }
3132
+
2827
3133
  // ── /mcp interactive handling ────────────────────────────────────────────────
2828
3134
 
2829
3135
  /// Pull the just-persisted config back into the session so subsequent turns
@@ -2834,9 +3140,14 @@ fn reload_session_config(session: &mut InteractiveSession) {
2834
3140
  }
2835
3141
  }
2836
3142
 
2837
- fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str) {
3143
+ fn handle_mcp_slash(
3144
+ session: &mut InteractiveSession,
3145
+ ui: &mut dyn CommandUi,
3146
+ subcmd: &str,
3147
+ args: &str,
3148
+ ) {
2838
3149
  match subcmd {
2839
- "list" | "" => mcp_interactive_picker(session),
3150
+ "list" | "" => mcp_interactive_picker(session, ui),
2840
3151
 
2841
3152
  "add" => {
2842
3153
  let mut tokens: Vec<&str> = args.split_whitespace().collect();
@@ -2857,54 +3168,52 @@ fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str)
2857
3168
  if allow_all {
2858
3169
  let _ = crate::mcp::allow(name, "*");
2859
3170
  }
2860
- println!(
2861
- "{MINT}Added MCP server '{name}'{}.{RESET}\n",
3171
+ ui.push_notice(format!(
3172
+ "Added MCP server '{name}'{}.",
2862
3173
  if allow_all {
2863
3174
  " (all tools allowed)"
2864
3175
  } else {
2865
3176
  ""
2866
3177
  }
2867
- );
3178
+ ));
2868
3179
  reload_session_config(session);
2869
3180
  }
2870
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3181
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2871
3182
  }
2872
3183
  }
2873
- _ => println!(
2874
- "{WARN}/mcp add usage:{RESET} <name> <command|url> [args...] [--allow-all]\n"
2875
- ),
3184
+ _ => ui.push_notice("/mcp add usage: <name> <command|url> [args...] [--allow-all]"),
2876
3185
  }
2877
3186
  }
2878
3187
 
2879
3188
  "remove" if !args.is_empty() => match crate::mcp::remove(args) {
2880
3189
  Ok(true) => {
2881
- println!("{MINT}Removed '{args}'.{RESET}\n");
3190
+ ui.push_notice(format!("Removed '{args}'."));
2882
3191
  reload_session_config(session);
2883
3192
  }
2884
- Ok(false) => println!("{WARN}No MCP server named '{args}'.{RESET}\n"),
2885
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3193
+ Ok(false) => ui.push_notice(format!("No MCP server named '{args}'.")),
3194
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2886
3195
  },
2887
3196
 
2888
3197
  "clear" => match crate::mcp::clear() {
2889
3198
  Ok(()) => {
2890
- println!("{MINT}Removed all MCP servers.{RESET}\n");
3199
+ ui.push_notice("Removed all MCP servers.");
2891
3200
  reload_session_config(session);
2892
3201
  }
2893
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3202
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2894
3203
  },
2895
3204
 
2896
3205
  "enable" | "disable" if !args.is_empty() => {
2897
3206
  let disable = subcmd == "disable";
2898
3207
  match crate::mcp::set_disabled(args, disable) {
2899
3208
  Ok(true) => {
2900
- println!(
2901
- "{MINT}{} '{args}'.{RESET}\n",
3209
+ ui.push_notice(format!(
3210
+ "{} '{args}'.",
2902
3211
  if disable { "Disabled" } else { "Enabled" }
2903
- );
3212
+ ));
2904
3213
  reload_session_config(session);
2905
3214
  }
2906
- Ok(false) => println!("{WARN}No MCP server named '{args}'.{RESET}\n"),
2907
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3215
+ Ok(false) => ui.push_notice(format!("No MCP server named '{args}'.")),
3216
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2908
3217
  }
2909
3218
  }
2910
3219
 
@@ -2918,9 +3227,7 @@ fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str)
2918
3227
  crate::mcp::edit(name, Some(value.to_string()), None, None, None)
2919
3228
  }
2920
3229
  "command" => {
2921
- println!(
2922
- "{WARN}/mcp edit <name> command <value> needs a value{RESET}\n"
2923
- );
3230
+ ui.push_notice("/mcp edit <name> command <value> needs a value");
2924
3231
  return;
2925
3232
  }
2926
3233
  "args" => crate::mcp::edit(
@@ -2937,21 +3244,24 @@ fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str)
2937
3244
  None,
2938
3245
  Some((!value.is_empty()).then(|| value.to_string())),
2939
3246
  ),
3247
+ "timeout" | "timeoutsecs" | "timeout-secs" => {
3248
+ crate::mcp::set_timeout(name, value)
3249
+ }
2940
3250
  _ => {
2941
- println!("{WARN}Editable fields:{RESET} command | args | icon\n");
3251
+ ui.push_notice("Editable fields: command | args | icon | timeout");
2942
3252
  return;
2943
3253
  }
2944
3254
  };
2945
3255
  match result {
2946
3256
  Ok(true) => {
2947
- println!("{MINT}Updated '{name}'.{RESET}\n");
3257
+ ui.push_notice(format!("Updated '{name}'."));
2948
3258
  reload_session_config(session);
2949
3259
  }
2950
- Ok(false) => println!("{WARN}No MCP server named '{name}'.{RESET}\n"),
2951
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3260
+ Ok(false) => ui.push_notice(format!("No MCP server named '{name}'.")),
3261
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2952
3262
  }
2953
3263
  }
2954
- _ => println!("{WARN}/mcp edit usage:{RESET} <name> command|args|icon <value>\n"),
3264
+ _ => ui.push_notice("/mcp edit usage: <name> command|args|icon|timeout <value>"),
2955
3265
  }
2956
3266
  }
2957
3267
 
@@ -2966,45 +3276,43 @@ fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str)
2966
3276
  };
2967
3277
  match result {
2968
3278
  Ok(true) => {
2969
- println!("{MINT}{verb} {server}/{tool}.{RESET}\n");
3279
+ ui.push_notice(format!("{verb} {server}/{tool}."));
2970
3280
  reload_session_config(session);
2971
3281
  }
2972
- Ok(false) => println!("{DIM}No change for {server}/{tool}.{RESET}\n"),
2973
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3282
+ Ok(false) => ui.push_notice(format!("No change for {server}/{tool}.")),
3283
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2974
3284
  }
2975
3285
  }
2976
- _ => println!("{WARN}/mcp {subcmd} usage:{RESET} <server> <tool>\n"),
3286
+ _ => ui.push_notice(format!("/mcp {subcmd} usage: <server> <tool>")),
2977
3287
  }
2978
3288
  }
2979
3289
 
2980
3290
  "reauth" if !args.is_empty() => {
2981
- println!(
2982
- "{DIM}Re-authenticating MCP server '{args}'... (a browser tab may open){RESET}\n"
2983
- );
3291
+ ui.push_notice(format!(
3292
+ "Re-authenticating MCP server '{args}'... (a browser tab may open)"
3293
+ ));
2984
3294
  match crate::mcp::reauth(args) {
2985
- Ok(true) => println!("{MINT}Re-authentication succeeded for '{args}'.{RESET}\n"),
2986
- Ok(false) => println!(
2987
- "{ERROR}Re-authentication failed for '{args}' (see output above).{RESET}\n"
2988
- ),
2989
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3295
+ Ok(true) => ui.push_notice(format!("Re-authentication succeeded for '{args}'.")),
3296
+ Ok(false) => ui.push_notice(format!("Re-authentication failed for '{args}'.")),
3297
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2990
3298
  }
2991
3299
  }
2992
3300
 
2993
- _ => println!(
2994
- "{WARN}/mcp usage:{RESET} list | add <name> <cmd> [args...] | remove <name> | \
3301
+ _ => ui.push_notice(
3302
+ "/mcp usage: list | add <name> <cmd> [args...] | remove <name> | \
2995
3303
  enable|disable <name> | edit <name> command|args|icon <value> | \
2996
- allow|disallow <server> <tool> | reauth <server> | clear\n"
3304
+ allow|disallow <server> <tool> | reauth <server> | clear",
2997
3305
  ),
2998
3306
  }
2999
3307
  }
3000
3308
 
3001
- /// The arrow-key `/mcp` flow: an "add" row plus every configured server; picking
3309
+ /// The `/mcp` flow: an "add" row plus every configured server; picking
3002
3310
  /// a server opens a per-server action menu.
3003
- fn mcp_interactive_picker(session: &mut InteractiveSession) {
3311
+ fn mcp_interactive_picker(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3004
3312
  let servers = match crate::mcp::list() {
3005
3313
  Ok(servers) => servers,
3006
3314
  Err(e) => {
3007
- println!("{ERROR}MCP error:{RESET} {e}\n");
3315
+ ui.push_notice(format!("MCP error: {e}"));
3008
3316
  return;
3009
3317
  }
3010
3318
  };
@@ -3019,100 +3327,114 @@ fn mcp_interactive_picker(session: &mut InteractiveSession) {
3019
3327
  .and_then(|v| v.as_object())
3020
3328
  .cloned();
3021
3329
 
3022
- let mut choices = vec![ADD_ROW.to_string()];
3330
+ let mut choices = vec![ChoiceItem::with_description(
3331
+ ADD_ROW,
3332
+ "Connect a new MCP server (catalog, remote SSE, or local stdio)",
3333
+ ADD_ROW,
3334
+ )];
3023
3335
  let mut names = Vec::new();
3024
- let width = servers.keys().map(|k| k.len()).max().unwrap_or(6) + 2;
3025
3336
  for (name, srv) in &servers {
3026
3337
  let tools = allowed
3027
3338
  .as_ref()
3028
3339
  .and_then(|m| m.get(name))
3029
3340
  .and_then(|v| v.as_array());
3030
3341
  let label = match tools {
3031
- Some(a) if a.iter().any(|t| t.as_str() == Some("*")) => {
3032
- format!("{MINT}[tools: *]{RESET}")
3033
- }
3342
+ Some(a) if a.iter().any(|t| t.as_str() == Some("*")) => "[tools: *]".to_string(),
3034
3343
  Some(a) if !a.is_empty() => format!(
3035
- "{MINT}[tools: {}]{RESET}",
3344
+ "[tools: {}]",
3036
3345
  a.iter()
3037
3346
  .filter_map(|t| t.as_str())
3038
3347
  .collect::<Vec<_>>()
3039
3348
  .join(", ")
3040
3349
  ),
3041
- _ => format!("{DIM}[no tools allowed]{RESET}"),
3042
- };
3043
- let disabled = if srv.disabled {
3044
- format!(" {DIM}(disabled){RESET}")
3045
- } else {
3046
- String::new()
3350
+ _ => "[no tools allowed]".to_string(),
3047
3351
  };
3352
+ let disabled = if srv.disabled { " (disabled)" } else { "" };
3048
3353
  let desc = if let Some(url) = srv.remote_url() {
3049
3354
  format!("(url: {url})")
3050
3355
  } else {
3051
3356
  format!("({} {})", srv.command, srv.args.join(" "))
3052
3357
  };
3053
- choices.push(format!(
3054
- "{name:<width$}{label}{disabled} {DIM}{desc}{RESET}",
3358
+ choices.push(ChoiceItem::with_description(
3359
+ name.clone(),
3360
+ format!("{label}{disabled} {desc}"),
3361
+ name.clone(),
3055
3362
  ));
3056
3363
  names.push(name.clone());
3057
3364
  }
3058
- choices.push(CANCEL.to_string());
3059
-
3060
- let Ok(Some(pick)) = prompt_interactive_select("MCP Servers", &choices, ADD_ROW) else {
3365
+ choices.push(ChoiceItem::with_description(
3366
+ CANCEL,
3367
+ "Close this menu",
3368
+ CANCEL,
3369
+ ));
3370
+
3371
+ let Ok(Some(idx)) = ui.prompt_choice(
3372
+ "MCP Servers",
3373
+ "Select a server to manage or add a new one",
3374
+ &choices,
3375
+ ) else {
3061
3376
  return;
3062
3377
  };
3378
+ let pick = &choices[idx].label;
3063
3379
  if pick == CANCEL {
3064
3380
  return;
3065
3381
  }
3066
3382
  if pick == ADD_ROW {
3067
- mcp_add_flow(session);
3383
+ mcp_add_flow(session, ui);
3068
3384
  return;
3069
3385
  }
3070
- let Some(pos) = choices.iter().position(|c| *c == pick) else {
3071
- return;
3072
- };
3073
- let name = names[pos - 1].clone();
3386
+ let name = names[idx - 1].clone();
3074
3387
  let disabled = servers.get(&name).map(|s| s.disabled).unwrap_or(false);
3075
- mcp_server_actions(session, &name, disabled);
3388
+ mcp_server_actions(session, ui, &name, disabled);
3076
3389
  }
3077
3390
 
3078
- fn mcp_add_flow(session: &mut InteractiveSession) {
3391
+ fn mcp_add_flow(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3079
3392
  let choices = vec![
3080
- "From catalog".to_string(),
3081
- "Remote URL (SSE)".to_string(),
3082
- "Local Command (stdio)".to_string(),
3393
+ ChoiceItem::with_description(
3394
+ "From catalog",
3395
+ "Install from known MCP tools catalog",
3396
+ "catalog",
3397
+ ),
3398
+ ChoiceItem::with_description(
3399
+ "Remote URL (SSE)",
3400
+ "Connect to remote HTTP/SSE MCP endpoint",
3401
+ "remote",
3402
+ ),
3403
+ ChoiceItem::with_description(
3404
+ "Local Command (stdio)",
3405
+ "Run local command (e.g. npx, uvx)",
3406
+ "local",
3407
+ ),
3083
3408
  ];
3084
- match prompt_interactive_select("Add MCP server", &choices, &choices[0]) {
3085
- Ok(Some(c)) if c == "From catalog" => mcp_add_from_catalog(session),
3086
- Ok(Some(c)) if c == "Remote URL (SSE)" => mcp_add_remote_flow(session),
3087
- Ok(Some(c)) if c == "Local Command (stdio)" => mcp_add_custom_flow(session),
3409
+ match ui.prompt_choice("Add MCP server", "Select server type", &choices) {
3410
+ Ok(Some(0)) => mcp_add_from_catalog(session, ui),
3411
+ Ok(Some(1)) => mcp_add_remote_flow(session, ui),
3412
+ Ok(Some(2)) => mcp_add_custom_flow(session, ui),
3088
3413
  _ => {}
3089
3414
  }
3090
3415
  }
3091
3416
 
3092
- fn mcp_add_from_catalog(session: &mut InteractiveSession) {
3417
+ fn mcp_add_from_catalog(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3093
3418
  let entries = crate::mcp::registry();
3094
- let labels: Vec<String> = entries
3419
+ let choices: Vec<ChoiceItem> = entries
3095
3420
  .iter()
3096
3421
  .map(|e| {
3097
3422
  let needs = if e.required_env.is_empty() {
3098
3423
  String::new()
3099
3424
  } else {
3100
- format!(" {DIM}(needs a key){RESET}")
3425
+ " (needs a key)".to_string()
3101
3426
  };
3102
- format!("{:<20} {}{needs}", e.name, e.desc)
3427
+ ChoiceItem::with_description(&e.name, format!("{}{needs}", e.desc), &e.key)
3103
3428
  })
3104
3429
  .collect();
3105
- let Ok(Some(picked)) = prompt_interactive_select("MCP server catalog", &labels, &labels[0])
3106
- else {
3107
- return;
3108
- };
3109
- let Some(entry) = labels
3110
- .iter()
3111
- .position(|l| *l == picked)
3112
- .map(|i| &entries[i])
3113
- else {
3430
+ let Ok(Some(idx)) = ui.prompt_choice(
3431
+ "MCP server catalog",
3432
+ "Select an MCP server from catalog",
3433
+ &choices,
3434
+ ) else {
3114
3435
  return;
3115
3436
  };
3437
+ let entry = &entries[idx];
3116
3438
 
3117
3439
  // One prompt per declared argInput (path / connection string / …).
3118
3440
  let mut extra_args = Vec::new();
@@ -3122,10 +3444,14 @@ fn mcp_add_from_catalog(session: &mut InteractiveSession) {
3122
3444
  } else {
3123
3445
  format!("{} (e.g. {})", input.label, input.placeholder)
3124
3446
  };
3125
- let value = crate::onboard::prompt_input(&hint, None).unwrap_or_default();
3447
+ let value = ui
3448
+ .prompt_text(&entry.name, &hint, None)
3449
+ .ok()
3450
+ .flatten()
3451
+ .unwrap_or_default();
3126
3452
  let value = value.trim();
3127
3453
  if value.is_empty() {
3128
- println!("{WARN}Cancelled ('{}' is required).{RESET}\n", input.label);
3454
+ ui.push_notice(format!("Cancelled ('{}' is required).", input.label));
3129
3455
  return;
3130
3456
  }
3131
3457
  extra_args.push(value.to_string());
@@ -3134,65 +3460,96 @@ fn mcp_add_from_catalog(session: &mut InteractiveSession) {
3134
3460
  // One prompt per required env var.
3135
3461
  let mut env = Vec::new();
3136
3462
  for var in &entry.required_env {
3137
- if let Some(help) = &var.help {
3138
- println!("{DIM} {}: get one at {help}{RESET}", var.label);
3139
- }
3140
- let value = crate::onboard::prompt_input(&var.label, None).unwrap_or_default();
3463
+ let prompt_body = if let Some(help) = &var.help {
3464
+ format!("{} (get one at {help})", var.label)
3465
+ } else {
3466
+ var.label.clone()
3467
+ };
3468
+ let value = ui
3469
+ .prompt_text(&entry.name, &prompt_body, None)
3470
+ .ok()
3471
+ .flatten()
3472
+ .unwrap_or_default();
3141
3473
  let value = value.trim();
3142
3474
  if value.is_empty() {
3143
- println!("{WARN}Cancelled ('{}' is required).{RESET}\n", var.label);
3475
+ ui.push_notice(format!("Cancelled ('{}' is required).", var.label));
3144
3476
  return;
3145
3477
  }
3146
3478
  env.push(format!("{}={value}", var.key));
3147
3479
  }
3148
3480
 
3149
- let name = crate::onboard::prompt_input("Server name", Some(&entry.key)).unwrap_or_default();
3481
+ let name = ui
3482
+ .prompt_text(
3483
+ "Server name",
3484
+ "Choose a unique name for this MCP server",
3485
+ Some(&entry.key),
3486
+ )
3487
+ .ok()
3488
+ .flatten()
3489
+ .unwrap_or_default();
3150
3490
  let name = if name.trim().is_empty() {
3151
3491
  entry.key.clone()
3152
3492
  } else {
3153
3493
  name.trim().to_string()
3154
3494
  };
3155
3495
 
3156
- let allow_all = confirm(
3157
- "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3158
- )
3159
- .unwrap_or(false);
3496
+ let allow_all = ui
3497
+ .prompt_confirm(
3498
+ "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3499
+ false,
3500
+ )
3501
+ .unwrap_or(false);
3160
3502
 
3161
3503
  match crate::mcp::registry_add(&entry.key, Some(&name), extra_args, env, allow_all) {
3162
3504
  Ok(saved) => {
3163
- println!(
3164
- "{MINT}Added MCP server '{saved}'{}.{RESET}\n",
3505
+ ui.push_notice(format!(
3506
+ "Added MCP server '{saved}'{}.",
3165
3507
  if allow_all {
3166
3508
  " (all tools allowed)"
3167
3509
  } else {
3168
3510
  ""
3169
3511
  }
3170
- );
3512
+ ));
3171
3513
  reload_session_config(session);
3172
3514
  }
3173
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3515
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3174
3516
  }
3175
3517
  }
3176
3518
 
3177
- fn mcp_add_remote_flow(session: &mut InteractiveSession) {
3178
- let name = crate::onboard::prompt_input("Server name", None).unwrap_or_default();
3519
+ fn mcp_add_remote_flow(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3520
+ let name = ui
3521
+ .prompt_text("Remote MCP Server", "Server name", None)
3522
+ .ok()
3523
+ .flatten()
3524
+ .unwrap_or_default();
3179
3525
  let name = name.trim().to_string();
3180
3526
  if name.is_empty() {
3181
- println!("{WARN}Cancelled (no name).{RESET}\n");
3527
+ ui.push_notice("Cancelled (no name).");
3182
3528
  return;
3183
3529
  }
3184
- let url = crate::onboard::prompt_input("Server URL (e.g. https://example.com/sse)", None)
3530
+ let url = ui
3531
+ .prompt_text(
3532
+ "Remote MCP Server",
3533
+ "Server URL (e.g. https://example.com/sse)",
3534
+ None,
3535
+ )
3536
+ .ok()
3537
+ .flatten()
3185
3538
  .unwrap_or_default();
3186
3539
  let url = url.trim().to_string();
3187
3540
  if url.is_empty() {
3188
- println!("{WARN}Cancelled (no URL).{RESET}\n");
3541
+ ui.push_notice("Cancelled (no URL).");
3189
3542
  return;
3190
3543
  }
3191
- let auth = crate::onboard::prompt_input(
3192
- "Auth header or bearer token (blank for none, e.g. Bearer token)",
3193
- None,
3194
- )
3195
- .unwrap_or_default();
3544
+ let auth = ui
3545
+ .prompt_text(
3546
+ "Remote MCP Server",
3547
+ "Auth header or bearer token (blank for none, e.g. Bearer token)",
3548
+ None,
3549
+ )
3550
+ .ok()
3551
+ .flatten()
3552
+ .unwrap_or_default();
3196
3553
  let headers = if auth.trim().is_empty() {
3197
3554
  None
3198
3555
  } else {
@@ -3212,46 +3569,65 @@ fn mcp_add_remote_flow(session: &mut InteractiveSession) {
3212
3569
 
3213
3570
  match crate::mcp::add_remote(&name, &url, headers) {
3214
3571
  Ok(()) => {
3215
- let allow_all = confirm(
3216
- "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3217
- )
3218
- .unwrap_or(false);
3572
+ let allow_all = ui
3573
+ .prompt_confirm(
3574
+ "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3575
+ false,
3576
+ )
3577
+ .unwrap_or(false);
3219
3578
  if allow_all {
3220
3579
  let _ = crate::mcp::allow(&name, "*");
3221
3580
  }
3222
- println!(
3223
- "{MINT}Added Remote MCP server '{name}'{}.{RESET}\n",
3581
+ ui.push_notice(format!(
3582
+ "Added Remote MCP server '{name}'{}.",
3224
3583
  if allow_all {
3225
3584
  " (all tools allowed)"
3226
3585
  } else {
3227
3586
  ""
3228
3587
  }
3229
- );
3588
+ ));
3230
3589
  reload_session_config(session);
3231
3590
  }
3232
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3591
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3233
3592
  }
3234
3593
  }
3235
3594
 
3236
- fn mcp_add_custom_flow(session: &mut InteractiveSession) {
3237
- let name = crate::onboard::prompt_input("Server name", None).unwrap_or_default();
3595
+ fn mcp_add_custom_flow(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3596
+ let name = ui
3597
+ .prompt_text("Custom MCP Server", "Server name", None)
3598
+ .ok()
3599
+ .flatten()
3600
+ .unwrap_or_default();
3238
3601
  let name = name.trim().to_string();
3239
3602
  if name.is_empty() {
3240
- println!("{WARN}Cancelled (no name).{RESET}\n");
3603
+ ui.push_notice("Cancelled (no name).");
3241
3604
  return;
3242
3605
  }
3243
- let command = crate::onboard::prompt_input("Command or URL (e.g. npx or https://...)", None)
3606
+ let command = ui
3607
+ .prompt_text(
3608
+ "Custom MCP Server",
3609
+ "Command or URL (e.g. npx or https://...)",
3610
+ None,
3611
+ )
3612
+ .ok()
3613
+ .flatten()
3244
3614
  .unwrap_or_default();
3245
3615
  let command = command.trim().to_string();
3246
3616
  if command.is_empty() {
3247
- println!("{WARN}Cancelled (no command).{RESET}\n");
3617
+ ui.push_notice("Cancelled (no command).");
3248
3618
  return;
3249
3619
  }
3250
3620
 
3251
3621
  if command.starts_with("http://") || command.starts_with("https://") {
3252
- let auth =
3253
- crate::onboard::prompt_input("Auth header or bearer token (blank for none)", None)
3254
- .unwrap_or_default();
3622
+ let auth = ui
3623
+ .prompt_text(
3624
+ "Remote MCP Server",
3625
+ "Auth header or bearer token (blank for none)",
3626
+ None,
3627
+ )
3628
+ .ok()
3629
+ .flatten()
3630
+ .unwrap_or_default();
3255
3631
  let headers = if auth.trim().is_empty() {
3256
3632
  None
3257
3633
  } else {
@@ -3270,35 +3646,48 @@ fn mcp_add_custom_flow(session: &mut InteractiveSession) {
3270
3646
  };
3271
3647
  match crate::mcp::add_remote(&name, &command, headers) {
3272
3648
  Ok(()) => {
3273
- let allow_all = confirm(
3274
- "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3275
- )
3276
- .unwrap_or(false);
3649
+ let allow_all = ui
3650
+ .prompt_confirm(
3651
+ "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3652
+ false,
3653
+ )
3654
+ .unwrap_or(false);
3277
3655
  if allow_all {
3278
3656
  let _ = crate::mcp::allow(&name, "*");
3279
3657
  }
3280
- println!(
3281
- "{MINT}Added Remote MCP server '{name}'{}.{RESET}\n",
3658
+ ui.push_notice(format!(
3659
+ "Added Remote MCP server '{name}'{}.",
3282
3660
  if allow_all {
3283
3661
  " (all tools allowed)"
3284
3662
  } else {
3285
3663
  ""
3286
3664
  }
3287
- );
3665
+ ));
3288
3666
  reload_session_config(session);
3289
3667
  }
3290
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3668
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3291
3669
  }
3292
3670
  return;
3293
3671
  }
3294
3672
 
3295
- let args = crate::onboard::prompt_input("Arguments (space-separated, blank for none)", None)
3673
+ let args = ui
3674
+ .prompt_text(
3675
+ "Custom MCP Server",
3676
+ "Arguments (space-separated, blank for none)",
3677
+ None,
3678
+ )
3679
+ .ok()
3680
+ .flatten()
3681
+ .unwrap_or_default();
3682
+ let env_raw = ui
3683
+ .prompt_text(
3684
+ "Custom MCP Server",
3685
+ "Env vars (KEY=VALUE, comma-separated, blank for none)",
3686
+ None,
3687
+ )
3688
+ .ok()
3689
+ .flatten()
3296
3690
  .unwrap_or_default();
3297
- let env_raw = crate::onboard::prompt_input(
3298
- "Env vars (KEY=VALUE, comma-separated, blank for none)",
3299
- None,
3300
- )
3301
- .unwrap_or_default();
3302
3691
 
3303
3692
  let args: Vec<String> = args.split_whitespace().map(str::to_string).collect();
3304
3693
  let env: Vec<String> = env_raw
@@ -3310,79 +3699,115 @@ fn mcp_add_custom_flow(session: &mut InteractiveSession) {
3310
3699
 
3311
3700
  match crate::mcp::add(&name, &command, args, env) {
3312
3701
  Ok(()) => {
3313
- let allow_all = confirm(
3314
- "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3315
- )
3316
- .unwrap_or(false);
3702
+ let allow_all = ui
3703
+ .prompt_confirm(
3704
+ "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3705
+ false,
3706
+ )
3707
+ .unwrap_or(false);
3317
3708
  if allow_all {
3318
3709
  let _ = crate::mcp::allow(&name, "*");
3319
3710
  }
3320
- println!(
3321
- "{MINT}Added MCP server '{name}'{}.{RESET}\n",
3711
+ ui.push_notice(format!(
3712
+ "Added MCP server '{name}'{}.",
3322
3713
  if allow_all {
3323
3714
  " (all tools allowed)"
3324
3715
  } else {
3325
3716
  ""
3326
3717
  }
3327
- );
3718
+ ));
3328
3719
  reload_session_config(session);
3329
3720
  }
3330
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3721
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3331
3722
  }
3332
3723
  }
3333
3724
 
3334
- fn mcp_server_actions(session: &mut InteractiveSession, name: &str, disabled: bool) {
3725
+ fn mcp_server_actions(
3726
+ session: &mut InteractiveSession,
3727
+ ui: &mut dyn CommandUi,
3728
+ name: &str,
3729
+ disabled: bool,
3730
+ ) {
3335
3731
  let toggle_label = if disabled {
3336
3732
  "Enable server"
3337
3733
  } else {
3338
3734
  "Disable server"
3339
3735
  };
3340
3736
  let actions = vec![
3341
- "Check connection".to_string(),
3342
- "Allow all tools (*)".to_string(),
3343
- "Allow a specific tool".to_string(),
3344
- "Disallow a tool".to_string(),
3345
- "Re-authenticate (OAuth)".to_string(),
3346
- toggle_label.to_string(),
3347
- "Edit (command / args / icon)".to_string(),
3348
- "Remove server".to_string(),
3349
- "Back".to_string(),
3737
+ ChoiceItem::with_description("Check connection", "Test tool discovery", "check"),
3738
+ ChoiceItem::with_description(
3739
+ "Allow all tools (*)",
3740
+ "Grant unrestricted tool call access",
3741
+ "allow_all",
3742
+ ),
3743
+ ChoiceItem::with_description(
3744
+ "Allow a specific tool",
3745
+ "Whitelist an individual tool name",
3746
+ "allow_tool",
3747
+ ),
3748
+ ChoiceItem::with_description(
3749
+ "Disallow a tool",
3750
+ "Remove a tool from allowlist",
3751
+ "disallow_tool",
3752
+ ),
3753
+ ChoiceItem::with_description(
3754
+ "Re-authenticate (OAuth)",
3755
+ "Trigger browser OAuth login flow",
3756
+ "reauth",
3757
+ ),
3758
+ ChoiceItem::with_description(toggle_label, "Toggle server active status", "toggle"),
3759
+ ChoiceItem::with_description(
3760
+ "Edit (command / args / icon)",
3761
+ "Update server settings",
3762
+ "edit",
3763
+ ),
3764
+ ChoiceItem::with_description("Remove server", "Delete server from config", "remove"),
3765
+ ChoiceItem::with_description("Back", "Return to MCP servers list", "back"),
3350
3766
  ];
3351
- let Ok(Some(choice)) =
3352
- prompt_interactive_select(&format!("MCP: {name}"), &actions, &actions[0])
3353
- else {
3767
+ let Ok(Some(idx)) = ui.prompt_choice(&format!("MCP: {name}"), "Choose action", &actions) else {
3354
3768
  return;
3355
3769
  };
3356
3770
 
3357
- match choice.as_str() {
3358
- "Check connection" => match mint_core::list_server_tools(&session.config, name) {
3359
- Ok(_) => println!("{MINT}● Connected{RESET}\n"),
3360
- Err(e) => println!("{ERROR}● Not connected:{RESET} {e}\n"),
3771
+ match actions[idx].value.as_str() {
3772
+ "check" => match mint_core::list_server_tools(&session.config, name) {
3773
+ Ok(_) => ui.push_notice("● Connected"),
3774
+ Err(e) => ui.push_notice(format!("● Not connected: {e}")),
3361
3775
  },
3362
- "Allow all tools (*)" => match crate::mcp::allow(name, "*") {
3776
+ "allow_all" => match crate::mcp::allow(name, "*") {
3363
3777
  Ok(_) => {
3364
- println!("{MINT}Allowed all tools for '{name}'.{RESET}\n");
3778
+ ui.push_notice(format!("Allowed all tools for '{name}'."));
3365
3779
  reload_session_config(session);
3366
3780
  }
3367
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3781
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3368
3782
  },
3369
- "Allow a specific tool" => {
3370
- let tool = crate::onboard::prompt_input("Tool name", None).unwrap_or_default();
3783
+ "allow_tool" => {
3784
+ let tool = ui
3785
+ .prompt_text(&format!("MCP: {name}"), "Tool name", None)
3786
+ .ok()
3787
+ .flatten()
3788
+ .unwrap_or_default();
3371
3789
  let tool = tool.trim();
3372
3790
  if tool.is_empty() {
3373
3791
  return;
3374
3792
  }
3375
3793
  match crate::mcp::allow(name, tool) {
3376
3794
  Ok(true) => {
3377
- println!("{MINT}Allowed {name}/{tool}.{RESET}\n");
3795
+ ui.push_notice(format!("Allowed {name}/{tool}."));
3378
3796
  reload_session_config(session);
3379
3797
  }
3380
- Ok(false) => println!("{DIM}{name}/{tool} was already allowed.{RESET}\n"),
3381
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3798
+ Ok(false) => ui.push_notice(format!("{name}/{tool} was already allowed.")),
3799
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3382
3800
  }
3383
3801
  }
3384
- "Disallow a tool" => {
3385
- let tool = crate::onboard::prompt_input("Tool to remove (* to clear all)", None)
3802
+ "disallow_tool" => {
3803
+ let tool = ui
3804
+ .prompt_text(
3805
+ &format!("MCP: {name}"),
3806
+ "Tool to remove (* to clear all)",
3807
+ None,
3808
+ )
3809
+ .ok()
3810
+ .flatten()
3386
3811
  .unwrap_or_default();
3387
3812
  let tool = tool.trim();
3388
3813
  if tool.is_empty() {
@@ -3390,42 +3815,45 @@ fn mcp_server_actions(session: &mut InteractiveSession, name: &str, disabled: bo
3390
3815
  }
3391
3816
  match crate::mcp::disallow(name, tool) {
3392
3817
  Ok(true) => {
3393
- println!("{MINT}Removed {name}/{tool} from the allowlist.{RESET}\n");
3818
+ ui.push_notice(format!("Removed {name}/{tool} from the allowlist."));
3394
3819
  reload_session_config(session);
3395
3820
  }
3396
- Ok(false) => println!("{DIM}{name}/{tool} was not in the allowlist.{RESET}\n"),
3397
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3821
+ Ok(false) => ui.push_notice(format!("{name}/{tool} was not in the allowlist.")),
3822
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3398
3823
  }
3399
3824
  }
3400
- "Re-authenticate (OAuth)" => {
3401
- println!("{DIM}Re-authenticating '{name}'... (a browser tab may open){RESET}\n");
3825
+ "reauth" => {
3826
+ ui.push_notice(format!(
3827
+ "Re-authenticating '{name}'... (a browser tab may open)"
3828
+ ));
3402
3829
  match crate::mcp::reauth(name) {
3403
- Ok(true) => println!("{MINT}Re-authentication succeeded.{RESET}\n"),
3404
- Ok(false) => {
3405
- println!("{ERROR}Re-authentication failed (see output above).{RESET}\n")
3406
- }
3407
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3830
+ Ok(true) => ui.push_notice("Re-authentication succeeded."),
3831
+ Ok(false) => ui.push_notice("Re-authentication failed (see output above)."),
3832
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3408
3833
  }
3409
3834
  }
3410
- other if other == toggle_label => match crate::mcp::set_disabled(name, !disabled) {
3835
+ "toggle" => match crate::mcp::set_disabled(name, !disabled) {
3411
3836
  Ok(_) => {
3412
- println!(
3413
- "{MINT}{} '{name}'.{RESET}\n",
3837
+ ui.push_notice(format!(
3838
+ "{} '{name}'.",
3414
3839
  if disabled { "Enabled" } else { "Disabled" }
3415
- );
3840
+ ));
3416
3841
  reload_session_config(session);
3417
3842
  }
3418
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3843
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3419
3844
  },
3420
- "Edit (command / args / icon)" => mcp_edit_flow(session, name),
3421
- "Remove server" => {
3422
- if confirm(&format!("Remove MCP server '{name}'?")).unwrap_or(false) {
3845
+ "edit" => mcp_edit_flow(session, ui, name),
3846
+ "remove" => {
3847
+ if ui
3848
+ .prompt_confirm(&format!("Remove MCP server '{name}'?"), false)
3849
+ .unwrap_or(false)
3850
+ {
3423
3851
  match crate::mcp::remove(name) {
3424
3852
  Ok(_) => {
3425
- println!("{MINT}Removed '{name}'.{RESET}\n");
3853
+ ui.push_notice(format!("Removed '{name}'."));
3426
3854
  reload_session_config(session);
3427
3855
  }
3428
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3856
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3429
3857
  }
3430
3858
  }
3431
3859
  }
@@ -3433,25 +3861,44 @@ fn mcp_server_actions(session: &mut InteractiveSession, name: &str, disabled: bo
3433
3861
  }
3434
3862
  }
3435
3863
 
3436
- fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3864
+ fn mcp_edit_flow(session: &mut InteractiveSession, ui: &mut dyn CommandUi, name: &str) {
3437
3865
  let Some(srv) = crate::mcp::list().ok().and_then(|m| m.get(name).cloned()) else {
3438
- println!("{WARN}No MCP server named '{name}'.{RESET}\n");
3866
+ ui.push_notice(format!("No MCP server named '{name}'."));
3439
3867
  return;
3440
3868
  };
3441
3869
  let fields = vec![
3442
- "command".to_string(),
3443
- "args".to_string(),
3444
- "icon".to_string(),
3445
- "Back".to_string(),
3870
+ ChoiceItem::with_description("command", &srv.command, "command"),
3871
+ ChoiceItem::with_description("args", srv.args.join(" "), "args"),
3872
+ ChoiceItem::with_description("icon", srv.icon.as_deref().unwrap_or("(none)"), "icon"),
3873
+ ChoiceItem::with_description(
3874
+ "Tool timeout",
3875
+ format!("{} seconds", srv.timeout_secs.unwrap_or(300)),
3876
+ "timeout",
3877
+ ),
3878
+ ChoiceItem::with_description("Back", "Cancel edit", "back"),
3446
3879
  ];
3447
- let Ok(Some(field)) = prompt_interactive_select(&format!("Edit {name}"), &fields, &fields[0])
3880
+ let Ok(Some(idx)) = ui.prompt_choice(&format!("Edit {name}"), "Select field to edit", &fields)
3448
3881
  else {
3449
3882
  return;
3450
3883
  };
3451
- let result = match field.as_str() {
3884
+ let result = match fields[idx].value.as_str() {
3885
+ "timeout" => {
3886
+ let current = srv.timeout_secs.unwrap_or(300).to_string();
3887
+ let Ok(Some(value)) = ui.prompt_text(
3888
+ &format!("Edit {name}"),
3889
+ "Tool timeout (seconds, 1–3600)",
3890
+ Some(&current),
3891
+ ) else {
3892
+ return;
3893
+ };
3894
+ crate::mcp::set_timeout(name, &value)
3895
+ }
3452
3896
  "command" => {
3453
- let value =
3454
- crate::onboard::prompt_input("Command", Some(&srv.command)).unwrap_or_default();
3897
+ let value = ui
3898
+ .prompt_text(&format!("Edit {name}"), "Command", Some(&srv.command))
3899
+ .ok()
3900
+ .flatten()
3901
+ .unwrap_or_default();
3455
3902
  let value = value.trim();
3456
3903
  if value.is_empty() {
3457
3904
  return;
@@ -3460,7 +3907,14 @@ fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3460
3907
  }
3461
3908
  "args" => {
3462
3909
  let current = srv.args.join(" ");
3463
- let value = crate::onboard::prompt_input("Arguments (space-separated)", Some(&current))
3910
+ let value = ui
3911
+ .prompt_text(
3912
+ &format!("Edit {name}"),
3913
+ "Arguments (space-separated)",
3914
+ Some(&current),
3915
+ )
3916
+ .ok()
3917
+ .flatten()
3464
3918
  .unwrap_or_default();
3465
3919
  crate::mcp::edit(
3466
3920
  name,
@@ -3472,7 +3926,14 @@ fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3472
3926
  }
3473
3927
  "icon" => {
3474
3928
  let current = srv.icon.clone().unwrap_or_default();
3475
- let value = crate::onboard::prompt_input("Icon (blank to clear)", Some(&current))
3929
+ let value = ui
3930
+ .prompt_text(
3931
+ &format!("Edit {name}"),
3932
+ "Icon (blank to clear)",
3933
+ Some(&current),
3934
+ )
3935
+ .ok()
3936
+ .flatten()
3476
3937
  .unwrap_or_default();
3477
3938
  let value = value.trim();
3478
3939
  crate::mcp::edit(
@@ -3487,10 +3948,10 @@ fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3487
3948
  };
3488
3949
  match result {
3489
3950
  Ok(_) => {
3490
- println!("{MINT}Updated '{name}'.{RESET}\n");
3951
+ ui.push_notice(format!("Updated '{name}'."));
3491
3952
  reload_session_config(session);
3492
3953
  }
3493
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3954
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3494
3955
  }
3495
3956
  }
3496
3957
 
@@ -3498,9 +3959,114 @@ fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3498
3959
  mod tests {
3499
3960
  use super::*;
3500
3961
 
3962
+ #[test]
3963
+ fn review_tui_timeout_edit_persists_and_invalid_or_cancel_preserves_it() {
3964
+ if std::env::var_os("MINT_TIMEOUT_REVIEW_CHILD").is_none() {
3965
+ let dir = std::env::temp_dir().join(format!("mint-tui-timeout-{}", std::process::id()));
3966
+ std::fs::create_dir_all(&dir).unwrap();
3967
+ let path = dir.join("config.json");
3968
+ let mut cfg = MintConfig::default();
3969
+ cfg.extra.insert(
3970
+ "mcpServers".into(),
3971
+ serde_json::json!({"timer":{"command":"cat"}}),
3972
+ );
3973
+ std::fs::write(&path, serde_json::to_vec(&cfg).unwrap()).unwrap();
3974
+ let output=std::process::Command::new(std::env::current_exe().unwrap())
3975
+ .args(["--exact","interactive::slash_commands::tests::review_tui_timeout_edit_persists_and_invalid_or_cancel_preserves_it","--nocapture"])
3976
+ .env("MINT_TIMEOUT_REVIEW_CHILD","1").env("MINT_CONFIG_PATH",&path).output().unwrap();
3977
+ std::fs::remove_dir_all(dir).unwrap();
3978
+ assert!(
3979
+ output.status.success(),
3980
+ "{}\n{}",
3981
+ String::from_utf8_lossy(&output.stdout),
3982
+ String::from_utf8_lossy(&output.stderr)
3983
+ );
3984
+ return;
3985
+ }
3986
+ struct TimeoutUi {
3987
+ answer: Option<String>,
3988
+ }
3989
+ impl CommandUi for TimeoutUi {
3990
+ fn push_notice_str(&mut self, _: &str) {}
3991
+ fn push_command_output_str(&mut self, _: &str) {}
3992
+ fn set_status(&mut self, _: Vec<String>) {}
3993
+ fn clear_status(&mut self) {}
3994
+ fn prompt_choice(
3995
+ &mut self,
3996
+ _: &str,
3997
+ _: &str,
3998
+ options: &[ChoiceItem],
3999
+ ) -> anyhow::Result<Option<usize>> {
4000
+ Ok(options.iter().position(|o| o.value == "timeout"))
4001
+ }
4002
+ fn prompt_text(
4003
+ &mut self,
4004
+ _: &str,
4005
+ _: &str,
4006
+ _: Option<&str>,
4007
+ ) -> anyhow::Result<Option<String>> {
4008
+ Ok(self.answer.take())
4009
+ }
4010
+ fn prompt_confirm(&mut self, _: &str, _: bool) -> anyhow::Result<bool> {
4011
+ Ok(false)
4012
+ }
4013
+ fn prompt_multi_choice(
4014
+ &mut self,
4015
+ _: &str,
4016
+ _: &str,
4017
+ _: &[ChoiceItem],
4018
+ ) -> anyhow::Result<Option<Vec<usize>>> {
4019
+ Ok(None)
4020
+ }
4021
+ fn report_error(&mut self, _: &anyhow::Error) {}
4022
+ }
4023
+ let mut session = InteractiveSession {
4024
+ chat_id: mint_core::CHAT_CLI_ID.into(),
4025
+ config: mint_core::load_config().unwrap(),
4026
+ current_dir: std::env::current_dir().unwrap(),
4027
+ fast_mode: false,
4028
+ plan_mode: false,
4029
+ pending_image: None,
4030
+ history: Vec::new(),
4031
+ jobs: BackgroundJobs::new(),
4032
+ };
4033
+ for value in [1, 300, 3600] {
4034
+ mcp_edit_flow(
4035
+ &mut session,
4036
+ &mut TimeoutUi {
4037
+ answer: Some(value.to_string()),
4038
+ },
4039
+ "timer",
4040
+ );
4041
+ assert_eq!(
4042
+ session.config.extra["mcpServers"]["timer"]["timeoutSecs"],
4043
+ value
4044
+ );
4045
+ assert_eq!(
4046
+ mint_core::load_config().unwrap().extra["mcpServers"]["timer"]["timeoutSecs"],
4047
+ value
4048
+ );
4049
+ }
4050
+ let before = std::fs::read(mint_core::config_path().unwrap()).unwrap();
4051
+ for answer in [Some("0"), Some("3601"), Some("1.5"), Some("nope"), None] {
4052
+ mcp_edit_flow(
4053
+ &mut session,
4054
+ &mut TimeoutUi {
4055
+ answer: answer.map(str::to_string),
4056
+ },
4057
+ "timer",
4058
+ );
4059
+ assert_eq!(
4060
+ std::fs::read(mint_core::config_path().unwrap()).unwrap(),
4061
+ before
4062
+ );
4063
+ }
4064
+ }
4065
+
3501
4066
  /// Commands that are dispatched but deliberately left undocumented —
3502
4067
  /// shortcuts for another command's own token, not gaps in `SLASH_COMMANDS`.
3503
- const UNDOCUMENTED_ALIASES: &[&str] = &["/quit", "/reset", "/plugin", "/searchProvider"];
4068
+ const UNDOCUMENTED_ALIASES: &[&str] =
4069
+ &["/quit", "/reset", "/plugin", "/searchProvider", "/think"];
3504
4070
 
3505
4071
  /// Regression guard for the exact bug that motivated `SLASH_COMMANDS`:
3506
4072
  /// `/edit-image`, `/gen-image`, `/shells`, and `/subagent` all worked but
@@ -3555,6 +4121,7 @@ mod tests {
3555
4121
  #[tokio::test]
3556
4122
  async fn core_slash_clear_clears_history() {
3557
4123
  let mut session = InteractiveSession {
4124
+ chat_id: mint_core::CHAT_CLI_ID.to_string(),
3558
4125
  config: MintConfig::default(),
3559
4126
  current_dir: std::env::current_dir().unwrap(),
3560
4127
  fast_mode: false,
@@ -3564,7 +4131,8 @@ mod tests {
3564
4131
  jobs: BackgroundJobs::new(),
3565
4132
  };
3566
4133
 
3567
- let res = handle_slash_command(&mut session, "/clear").await;
4134
+ let mut ui = MockCommandUi::new();
4135
+ let res = handle_slash_command(&mut session, &mut ui, "/clear").await;
3568
4136
  assert!(matches!(res, Some(SlashResult::Handled)));
3569
4137
  assert!(session.history.is_empty());
3570
4138
  }
@@ -3572,6 +4140,7 @@ mod tests {
3572
4140
  #[tokio::test]
3573
4141
  async fn core_slash_fast_mode_toggle() {
3574
4142
  let mut session = InteractiveSession {
4143
+ chat_id: mint_core::CHAT_CLI_ID.to_string(),
3575
4144
  config: MintConfig::default(),
3576
4145
  current_dir: std::env::current_dir().unwrap(),
3577
4146
  fast_mode: false,
@@ -3581,7 +4150,8 @@ mod tests {
3581
4150
  jobs: BackgroundJobs::new(),
3582
4151
  };
3583
4152
 
3584
- let res = handle_slash_command(&mut session, "/fast on").await;
4153
+ let mut ui = MockCommandUi::new();
4154
+ let res = handle_slash_command(&mut session, &mut ui, "/fast on").await;
3585
4155
  assert!(matches!(res, Some(SlashResult::Handled)));
3586
4156
  assert!(session.fast_mode);
3587
4157
  }