@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
@@ -6,6 +6,24 @@ use thiserror::Error;
6
6
 
7
7
  use crate::MintConfig;
8
8
 
9
+ /// Provider-neutral pieces produced while a model response is still arriving.
10
+ /// Callers never need to know which vendor-specific field carried the data.
11
+ #[derive(Debug, Clone, PartialEq, Eq)]
12
+ pub enum ChatStreamEvent {
13
+ TextDelta {
14
+ delta: String,
15
+ },
16
+ ReasoningDelta {
17
+ delta: String,
18
+ },
19
+ ToolCallDelta {
20
+ index: usize,
21
+ name: Option<String>,
22
+ arguments: Option<String>,
23
+ input: Option<Value>,
24
+ },
25
+ }
26
+
9
27
  /// `available_providers()`'s configured-providers list, reordered so
10
28
  /// `gemini` is tried first among fallbacks (when it's configured and isn't
11
29
  /// already the primary) — the user's explicit preference, ahead of
@@ -63,8 +81,26 @@ pub async fn send_chat_with_fallback(
63
81
  /// away from `provider` — `None` for an ordinary transient failure, so routine
64
82
  /// hiccups don't get an extra notice while specific, actionable ones do.
65
83
  fn fallback_reason_text(provider: &str, error: &ChatError) -> Option<String> {
84
+ let provider_name = if provider.starts_with("custom:") {
85
+ provider.strip_prefix("custom:").unwrap_or(provider)
86
+ } else {
87
+ provider
88
+ };
66
89
  match error {
67
- ChatError::InsufficientBalance(_) => Some(format!("{provider} ran out of balance")),
90
+ ChatError::InsufficientBalance(_) => Some(format!("{provider_name} ran out of balance")),
91
+ ChatError::Request(e) => {
92
+ if let Some(status) = e.status() {
93
+ if status == reqwest::StatusCode::PAYLOAD_TOO_LARGE {
94
+ Some(format!("{provider_name} payload too large (HTTP 413)"))
95
+ } else if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
96
+ Some(format!("{provider_name} rate limit exceeded (HTTP 429)"))
97
+ } else {
98
+ Some(format!("{provider_name} returned HTTP {status}"))
99
+ }
100
+ } else {
101
+ None
102
+ }
103
+ }
68
104
  _ => None,
69
105
  }
70
106
  }
@@ -107,6 +143,42 @@ where
107
143
  .map(|r| (r, None))
108
144
  }
109
145
 
146
+ /// Streaming equivalent of [`send_chat_with_fallback`] that preserves the
147
+ /// complete response (tool calls, usage, stop reason, and reasoning) while
148
+ /// exposing provider-neutral deltas as they arrive.
149
+ pub async fn stream_chat_events_with_fallback<F>(
150
+ config: &MintConfig,
151
+ request: &ChatRequest,
152
+ mut on_event: F,
153
+ ) -> Result<(ChatResponse, Option<String>), ChatError>
154
+ where
155
+ F: FnMut(ChatStreamEvent),
156
+ {
157
+ let primary_error = match stream_chat_events(config, request, &mut on_event).await {
158
+ Ok(r) => return Ok((r, None)),
159
+ Err(e) if !is_recoverable(&e) => return Err(e),
160
+ Err(e) => e,
161
+ };
162
+ if matches!(primary_error, ChatError::NetworkUnavailable) {
163
+ return Err(primary_error);
164
+ }
165
+ let fallback_reason = fallback_reason_text(&config.ai_provider, &primary_error);
166
+ for provider in fallback_provider_order(config) {
167
+ if provider == config.ai_provider {
168
+ continue;
169
+ }
170
+ let alt = config_for_provider(config, &provider);
171
+ if let Ok(mut response) = stream_chat_events(&alt, request, &mut on_event).await {
172
+ response.fallback_provider = Some(config.ai_provider.clone());
173
+ response.fallback_reason = fallback_reason.clone();
174
+ return Ok((response, Some(provider)));
175
+ }
176
+ }
177
+ stream_chat_events(config, request, &mut on_event)
178
+ .await
179
+ .map(|response| (response, None))
180
+ }
181
+
110
182
  /// Whether an error warrants trying another provider.
111
183
  fn is_recoverable(e: &ChatError) -> bool {
112
184
  matches!(
@@ -167,6 +239,12 @@ pub struct ChatRequest {
167
239
  /// `config.resolved_temperature()`.
168
240
  #[serde(default)]
169
241
  pub temperature: Option<f64>,
242
+ /// Optional per-request thinking/reasoning enabled override.
243
+ #[serde(default)]
244
+ pub thinking_enabled: Option<bool>,
245
+ /// Optional per-request thinking effort override ("low", "medium", "high", "extra_high").
246
+ #[serde(default)]
247
+ pub thinking_effort: Option<String>,
170
248
  }
171
249
 
172
250
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -265,6 +343,43 @@ const MAX_DOCUMENT_CONTEXT_CHARS: usize = 400_000;
265
343
  static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
266
344
 
267
345
  impl ChatRequest {
346
+ pub(crate) fn with_stored_document_context(
347
+ &self,
348
+ _config: &crate::MintConfig,
349
+ path: &std::path::Path,
350
+ ) -> Result<ChatRequest, String> {
351
+ let Some(document) = &self.document_attachment else {
352
+ return Ok(self.clone());
353
+ };
354
+ let extracted =
355
+ crate::knowledge::extract_managed_document_text(path).map_err(|e| e.to_string())?;
356
+ self.with_document_text(document, extracted)
357
+ }
358
+ fn with_document_text(
359
+ &self,
360
+ document: &DocumentAttachment,
361
+ extracted: String,
362
+ ) -> Result<ChatRequest, String> {
363
+ let mut context: String = extracted.chars().take(MAX_DOCUMENT_CONTEXT_CHARS).collect();
364
+ if extracted.chars().count() > MAX_DOCUMENT_CONTEXT_CHARS {
365
+ context.push_str("\n\n[Document text truncated because it is long.]");
366
+ }
367
+
368
+ let filename = document.filename.trim();
369
+ let filename = if filename.is_empty() {
370
+ "attached document"
371
+ } else {
372
+ filename
373
+ };
374
+
375
+ let mut enriched = self.clone();
376
+ enriched.document_attachment = None;
377
+ enriched.message = format!(
378
+ "{}\n\nAttached document: {filename}\nUse the extracted document text below when answering. If the user asks for a summary, summarize this document.\n\n--- Extracted document text ---\n{context}\n--- End extracted document text ---",
379
+ self.message
380
+ );
381
+ Ok(enriched)
382
+ }
268
383
  pub fn with_document_context(&self, config: &crate::MintConfig) -> Result<ChatRequest, String> {
269
384
  let Some(document) = &self.document_attachment else {
270
385
  return Ok(self.clone());
@@ -326,25 +441,7 @@ impl ChatRequest {
326
441
  let _ = std::fs::remove_file(&path);
327
442
  let extracted = extracted?;
328
443
 
329
- let mut context: String = extracted.chars().take(MAX_DOCUMENT_CONTEXT_CHARS).collect();
330
- if extracted.chars().count() > MAX_DOCUMENT_CONTEXT_CHARS {
331
- context.push_str("\n\n[Document text truncated because it is long.]");
332
- }
333
-
334
- let filename = document.filename.trim();
335
- let filename = if filename.is_empty() {
336
- "attached document"
337
- } else {
338
- filename
339
- };
340
-
341
- let mut enriched = self.clone();
342
- enriched.document_attachment = None;
343
- enriched.message = format!(
344
- "{}\n\nAttached document: {filename}\nUse the extracted document text below when answering. If the user asks for a summary, summarize this document.\n\n--- Extracted document text ---\n{context}\n--- End extracted document text ---",
345
- self.message
346
- );
347
- Ok(enriched)
444
+ self.with_document_text(document, extracted)
348
445
  }
349
446
  }
350
447
 
@@ -483,35 +580,54 @@ pub async fn stream_chat<F>(
483
580
  ) -> Result<ChatResponse, ChatError>
484
581
  where
485
582
  F: FnMut(String),
583
+ {
584
+ stream_chat_events(config, request, |event| {
585
+ if let ChatStreamEvent::TextDelta { delta } = event {
586
+ on_chunk(delta);
587
+ }
588
+ })
589
+ .await
590
+ }
591
+
592
+ pub async fn stream_chat_events<F>(
593
+ config: &MintConfig,
594
+ request: &ChatRequest,
595
+ mut on_event: F,
596
+ ) -> Result<ChatResponse, ChatError>
597
+ where
598
+ F: FnMut(ChatStreamEvent),
486
599
  {
487
600
  let client = crate::HTTP_CLIENT.clone();
488
601
  let provider = config.ai_provider.as_str();
489
602
  require_supported_attachments(provider, request)?;
490
- let (model, text) = match provider {
491
- "gemini" => stream_gemini(&client, config, request, &mut on_chunk).await?,
603
+ let reply = match provider {
604
+ "gemini" => stream_gemini_reply(&client, config, request, &mut on_event).await?,
492
605
  "openai" | "local_openai" | "openrouter" | "deepseek" => {
493
- stream_openai(&client, config, request, &mut on_chunk).await?
606
+ stream_openai_reply(&client, config, request, &mut on_event).await?
494
607
  }
495
- "ollama" => stream_ollama(&client, config, request, &mut on_chunk).await?,
496
- "anthropic" => stream_anthropic(&client, config, request, &mut on_chunk).await?,
497
- "huggingface" => stream_huggingface(&client, config, request, &mut on_chunk).await?,
608
+ "ollama" => stream_ollama_reply(&client, config, request, &mut on_event).await?,
609
+ "anthropic" => stream_anthropic_reply(&client, config, request, &mut on_event).await?,
610
+ "huggingface" => stream_huggingface_reply(&client, config, request, &mut on_event).await?,
498
611
  p if p.starts_with("custom:") => {
499
- stream_custom_provider(&client, config, request, &mut on_chunk).await?
612
+ stream_custom_provider_reply(&client, config, request, &mut on_event).await?
500
613
  }
501
614
  other => return Err(ChatError::UnsupportedProvider(other.into())),
502
615
  };
503
616
  Ok(ChatResponse {
504
617
  provider: provider.into(),
505
- model,
506
- text,
507
- thought: None,
618
+ model: reply.model,
619
+ text: reply.text,
620
+ thought: reply.thought,
508
621
  fallback_provider: None,
509
622
  fallback_reason: None,
510
- tool_calls: None,
511
- stop_reason: None,
512
- total_tokens: None,
513
- input_tokens: None,
514
- output_tokens: None,
623
+ tool_calls: reply.tool_calls,
624
+ stop_reason: reply.stop_reason,
625
+ total_tokens: match (reply.input_tokens, reply.output_tokens) {
626
+ (Some(input), Some(output)) => Some(input + output),
627
+ (input, output) => input.or(output),
628
+ },
629
+ input_tokens: reply.input_tokens,
630
+ output_tokens: reply.output_tokens,
515
631
  })
516
632
  }
517
633
 
@@ -529,7 +645,9 @@ async fn call_gemini(
529
645
  let payload = if request.messages.is_some() {
530
646
  let mut p = gemini_native_payload(request)?;
531
647
  let temp = effective_temperature(config, request);
532
- p["generationConfig"] = json!({ "temperature": temp });
648
+ let mut gen_cfg = json!({ "temperature": temp });
649
+ gemini_apply_thinking_config(&mut gen_cfg, config, request, &model);
650
+ p["generationConfig"] = gen_cfg;
533
651
  p
534
652
  } else {
535
653
  gemini_chat_payload(config, request)?
@@ -871,6 +989,15 @@ async fn send_openai_style_request(
871
989
  Ok(response.json().await?)
872
990
  }
873
991
 
992
+ // Preserve malformed arguments so the executor can reject them. Replacing them
993
+ // with {} would silently turn a requested directory into the workspace root.
994
+ fn decode_tool_arguments(arguments: &Value) -> Value {
995
+ match arguments.as_str() {
996
+ Some(raw) => serde_json::from_str(raw).unwrap_or_else(|_| arguments.clone()),
997
+ None => arguments.clone(),
998
+ }
999
+ }
1000
+
874
1001
  /// Parses an OpenAI Chat Completions-shaped response (`choices[0].message`),
875
1002
  /// extracting both plain text and any native `tool_calls`. Shared by OpenAI,
876
1003
  /// custom OpenAI-compatible providers, and (for its response shape only) reused
@@ -900,10 +1027,7 @@ fn parse_openai_style_reply(model: String, response: &Value) -> Result<ProviderR
900
1027
  .iter()
901
1028
  .filter_map(|call| {
902
1029
  let name = call["function"]["name"].as_str()?.to_string();
903
- let input = call["function"]["arguments"]
904
- .as_str()
905
- .and_then(|raw| serde_json::from_str(raw).ok())
906
- .unwrap_or_else(|| json!({}));
1030
+ let input = decode_tool_arguments(&call["function"]["arguments"]);
907
1031
  Some(ToolCall {
908
1032
  id: call["id"].as_str().unwrap_or_default().to_string(),
909
1033
  name,
@@ -965,11 +1089,10 @@ async fn call_ollama(
965
1089
  let native = request.messages.is_some()
966
1090
  && config.tool_calling_mode() == crate::config::ToolCallingMode::Native;
967
1091
  let mut body = if native {
968
- let messages = request.messages.as_deref().unwrap_or(&[]);
969
1092
  let mut payload = json!({
970
1093
  "model": model,
971
1094
  "stream": false,
972
- "messages": ollama_native_messages(messages),
1095
+ "messages": ollama_request_messages(request),
973
1096
  });
974
1097
  if let Some(tools) = &request.tools {
975
1098
  payload["tools"] = json!(
@@ -1009,6 +1132,10 @@ async fn call_ollama(
1009
1132
  "num_ctx": config.ollama_num_ctx,
1010
1133
  "temperature": temp,
1011
1134
  });
1135
+ let (thinking_enabled, _) = effective_thinking(config, request, &model);
1136
+ if MintConfig::is_thinking_supported_for_model("ollama", &model) {
1137
+ body["think"] = json!(thinking_enabled);
1138
+ }
1012
1139
  let response: Value = client
1013
1140
  .post(format!("{host}/api/chat"))
1014
1141
  .json(&body)
@@ -1024,6 +1151,19 @@ async fn call_ollama(
1024
1151
  /// Unlike OpenAI, Ollama expects tool-call `arguments` as a JSON object (not a
1025
1152
  /// stringified JSON blob) and doesn't require a `tool_call_id` on tool-result
1026
1153
  /// messages.
1154
+ fn ollama_request_messages(request: &ChatRequest) -> Vec<Value> {
1155
+ let history = request.messages.as_deref().unwrap_or(&[]);
1156
+ let mut result = ollama_native_messages(history);
1157
+ if !request.system_instruction.is_empty() && !history.iter().any(|m| m.role == ChatRole::System)
1158
+ {
1159
+ result.insert(
1160
+ 0,
1161
+ json!({"role":"system","content":request.system_instruction}),
1162
+ );
1163
+ }
1164
+ result
1165
+ }
1166
+
1027
1167
  fn ollama_native_messages(messages: &[ChatMessage]) -> Vec<Value> {
1028
1168
  let mut result = Vec::new();
1029
1169
  for message in messages {
@@ -1125,11 +1265,7 @@ fn parse_ollama_reply(model: String, response: &Value) -> Result<ProviderReply,
1125
1265
  .filter_map(|(index, call)| {
1126
1266
  let name = call["function"]["name"].as_str()?.to_string();
1127
1267
  let arguments = &call["function"]["arguments"];
1128
- let input = if let Some(raw) = arguments.as_str() {
1129
- serde_json::from_str(raw).unwrap_or_else(|_| json!({}))
1130
- } else {
1131
- arguments.clone()
1132
- };
1268
+ let input = decode_tool_arguments(arguments);
1133
1269
  let id = call["id"]
1134
1270
  .as_str()
1135
1271
  .map(str::to_owned)
@@ -1203,11 +1339,27 @@ fn anthropic_chat_payload(
1203
1339
  stream: bool,
1204
1340
  ) -> Result<Value, ChatError> {
1205
1341
  let system = anthropic_system_blocks(&request.system_instruction);
1206
- let temp = effective_temperature(config, request);
1342
+ let (thinking_enabled, thinking_effort) = effective_thinking(config, request, model);
1343
+ let is_thinking_supported = MintConfig::is_thinking_supported_for_model("anthropic", model);
1344
+ let use_thinking = thinking_enabled && is_thinking_supported;
1345
+
1346
+ let (budget, max_tokens, temp) = if use_thinking {
1347
+ let b = match thinking_effort.as_str() {
1348
+ "low" => 1024,
1349
+ "medium" => 4096,
1350
+ "high" => 16384,
1351
+ "extra_high" => 32768,
1352
+ _ => 4096,
1353
+ };
1354
+ (Some(b), b + 8192, 1.0)
1355
+ } else {
1356
+ (None, 8192, effective_temperature(config, request))
1357
+ };
1358
+
1207
1359
  let mut payload = if let Some(messages) = &request.messages {
1208
1360
  json!({
1209
1361
  "model": model,
1210
- "max_tokens": 8192,
1362
+ "max_tokens": max_tokens,
1211
1363
  "system": system,
1212
1364
  "messages": anthropic_messages(messages)?,
1213
1365
  "temperature": temp,
@@ -1215,7 +1367,7 @@ fn anthropic_chat_payload(
1215
1367
  } else {
1216
1368
  json!({
1217
1369
  "model": model,
1218
- "max_tokens": 8192,
1370
+ "max_tokens": max_tokens,
1219
1371
  "system": system,
1220
1372
  "messages": [{
1221
1373
  "role": "user",
@@ -1224,6 +1376,12 @@ fn anthropic_chat_payload(
1224
1376
  "temperature": temp,
1225
1377
  })
1226
1378
  };
1379
+ if let Some(b) = budget {
1380
+ payload["thinking"] = json!({
1381
+ "type": "enabled",
1382
+ "budget_tokens": b,
1383
+ });
1384
+ }
1227
1385
  if stream {
1228
1386
  payload["stream"] = json!(true);
1229
1387
  }
@@ -1373,6 +1531,19 @@ fn anthropic_content_block(block: &ContentBlock) -> Result<Value, ChatError> {
1373
1531
  /// `tool_use` content blocks (Anthropic may return a `tool_use` block before,
1374
1532
  /// after, or interleaved with text — unlike the old code, this does not assume
1375
1533
  /// the first block is text).
1534
+ // Anthropic reports uncached, cache-read and cache-created input separately.
1535
+ fn anthropic_input_tokens(usage: &Value) -> Option<u32> {
1536
+ [
1537
+ "input_tokens",
1538
+ "cache_read_input_tokens",
1539
+ "cache_creation_input_tokens",
1540
+ ]
1541
+ .into_iter()
1542
+ .filter_map(|key| usage[key].as_u64())
1543
+ .reduce(u64::saturating_add)
1544
+ .map(|n| n.min(u32::MAX as u64) as u32)
1545
+ }
1546
+
1376
1547
  fn parse_anthropic_reply(model: String, response: &Value) -> Result<ProviderReply, ChatError> {
1377
1548
  let blocks = response["content"].as_array().cloned().unwrap_or_default();
1378
1549
  let mut text = String::new();
@@ -1419,7 +1590,7 @@ fn parse_anthropic_reply(model: String, response: &Value) -> Result<ProviderRepl
1419
1590
  thought: Some(th.clone()),
1420
1591
  tool_calls: None,
1421
1592
  stop_reason: response["stop_reason"].as_str().map(str::to_owned),
1422
- input_tokens: response["usage"]["input_tokens"].as_u64().map(|n| n as u32),
1593
+ input_tokens: anthropic_input_tokens(&response["usage"]),
1423
1594
  output_tokens: response["usage"]["output_tokens"]
1424
1595
  .as_u64()
1425
1596
  .map(|n| n as u32),
@@ -1433,7 +1604,7 @@ fn parse_anthropic_reply(model: String, response: &Value) -> Result<ProviderRepl
1433
1604
  thought,
1434
1605
  tool_calls: (!tool_calls.is_empty()).then_some(tool_calls),
1435
1606
  stop_reason: response["stop_reason"].as_str().map(str::to_owned),
1436
- input_tokens: response["usage"]["input_tokens"].as_u64().map(|n| n as u32),
1607
+ input_tokens: anthropic_input_tokens(&response["usage"]),
1437
1608
  output_tokens: response["usage"]["output_tokens"]
1438
1609
  .as_u64()
1439
1610
  .map(|n| n as u32),
@@ -1469,41 +1640,43 @@ async fn call_huggingface(
1469
1640
  parse_openai_style_reply(model, &response)
1470
1641
  }
1471
1642
 
1472
- async fn stream_gemini<F>(
1643
+ async fn stream_gemini_reply<F>(
1473
1644
  client: &Client,
1474
1645
  config: &MintConfig,
1475
1646
  request: &ChatRequest,
1476
- on_chunk: &mut F,
1477
- ) -> Result<(String, String), ChatError>
1647
+ on_event: &mut F,
1648
+ ) -> Result<ProviderReply, ChatError>
1478
1649
  where
1479
- F: FnMut(String),
1650
+ F: FnMut(ChatStreamEvent),
1480
1651
  {
1481
1652
  let api_key = provider_key(&config.api_key, "GEMINI_API_KEY");
1482
1653
  required_key("gemini", &api_key)?;
1483
1654
  let model = config.gemini_model.clone();
1484
-
1655
+ let payload = if request.messages.is_some() {
1656
+ gemini_native_payload(request)?
1657
+ } else {
1658
+ gemini_chat_payload(config, request)?
1659
+ };
1485
1660
  let response = client
1486
1661
  .post(format!(
1487
1662
  "https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent?alt=sse"
1488
1663
  ))
1489
1664
  .header("x-goog-api-key", api_key)
1490
- .json(&gemini_chat_payload(config, request)?)
1665
+ .json(&payload)
1491
1666
  .send()
1492
1667
  .await?
1493
1668
  .error_for_status()?;
1494
- collect_stream(response, StreamFormat::Gemini, on_chunk)
1495
- .await
1496
- .map(|text| (model, text))
1669
+ collect_stream_reply(response, StreamFormat::Gemini, model, on_event).await
1497
1670
  }
1498
1671
 
1499
- async fn stream_openai<F>(
1672
+ async fn stream_openai_reply<F>(
1500
1673
  client: &Client,
1501
1674
  config: &MintConfig,
1502
1675
  request: &ChatRequest,
1503
- on_chunk: &mut F,
1504
- ) -> Result<(String, String), ChatError>
1676
+ on_event: &mut F,
1677
+ ) -> Result<ProviderReply, ChatError>
1505
1678
  where
1506
- F: FnMut(String),
1679
+ F: FnMut(ChatStreamEvent),
1507
1680
  {
1508
1681
  let local = config.ai_provider == "local_openai";
1509
1682
  let openrouter = config.ai_provider == "openrouter";
@@ -1551,19 +1724,71 @@ where
1551
1724
  let body = response.text().await.unwrap_or_default();
1552
1725
  return Err(ChatError::Api(format!("HTTP {status}: {body}")));
1553
1726
  }
1554
- collect_stream(response, StreamFormat::OpenAi, on_chunk)
1555
- .await
1556
- .map(|text| (model, text))
1727
+ collect_stream_reply(response, StreamFormat::OpenAi, model, on_event).await
1728
+ }
1729
+
1730
+ fn ollama_stream_payload(
1731
+ config: &MintConfig,
1732
+ request: &ChatRequest,
1733
+ model: &str,
1734
+ ) -> Result<Value, ChatError> {
1735
+ let native = request.messages.is_some()
1736
+ && config.tool_calling_mode() == crate::config::ToolCallingMode::Native;
1737
+ let mut payload = if native {
1738
+ let mut value = json!({
1739
+ "model": model,
1740
+ "stream": true,
1741
+ "messages": ollama_request_messages(request),
1742
+ });
1743
+ if let Some(tools) = &request.tools {
1744
+ value["tools"] = json!(
1745
+ tools
1746
+ .iter()
1747
+ .map(|tool| json!({
1748
+ "type": "function",
1749
+ "function": {
1750
+ "name": tool.name,
1751
+ "description": tool.description,
1752
+ "parameters": tool.parameters,
1753
+ }
1754
+ }))
1755
+ .collect::<Vec<_>>()
1756
+ );
1757
+ }
1758
+ value
1759
+ } else {
1760
+ let mut user = json!({ "role": "user", "content": request.message });
1761
+ if let Some(images) = ollama_images(request) {
1762
+ user["images"] = json!(images);
1763
+ }
1764
+ json!({
1765
+ "model": model,
1766
+ "stream": true,
1767
+ "messages": [
1768
+ { "role": "system", "content": request.system_instruction },
1769
+ user
1770
+ ]
1771
+ })
1772
+ };
1773
+ payload["options"] = json!({
1774
+ "num_ctx": config.ollama_num_ctx,
1775
+ "temperature": effective_temperature(config, request),
1776
+ });
1777
+ let (thinking_enabled, _) = effective_thinking(config, request, model);
1778
+ if MintConfig::is_thinking_supported_for_model("ollama", model) {
1779
+ payload["think"] = json!(thinking_enabled);
1780
+ }
1781
+ Ok(payload)
1557
1782
  }
1558
1783
 
1559
- async fn stream_ollama<F>(
1784
+ async fn stream_ollama_reply<F>(
1560
1785
  client: &Client,
1561
1786
  config: &MintConfig,
1562
1787
  request: &ChatRequest,
1563
- on_chunk: &mut F,
1564
- ) -> Result<(String, String), ChatError>
1788
+ on_event: &mut F,
1789
+ ) -> Result<ProviderReply, ChatError>
1565
1790
  where
1566
- F: FnMut(String),
1791
+ F: FnMut(ChatStreamEvent),
1567
1792
  {
1568
1793
  let host = if config.ollama_host.trim().is_empty() {
1569
1794
  "http://localhost:11434"
@@ -1571,41 +1796,23 @@ where
1571
1796
  config.ollama_host.trim_end_matches('/')
1572
1797
  };
1573
1798
  let model = config.ollama_model.clone();
1574
- let mut user_message = json!({ "role": "user", "content": request.message });
1575
- if let Some(images) = ollama_images(request) {
1576
- user_message["images"] = json!(images);
1577
- }
1578
1799
  let response = client
1579
1800
  .post(format!("{host}/api/chat"))
1580
- .json(&json!({
1581
- "model": model,
1582
- "stream": true,
1583
- "messages": [
1584
- { "role": "system", "content": request.system_instruction },
1585
- user_message
1586
- ],
1587
- // See `call_ollama`: pin the window instead of Ollama's ~4K default.
1588
- "options": {
1589
- "num_ctx": config.ollama_num_ctx,
1590
- "temperature": effective_temperature(config, request),
1591
- }
1592
- }))
1801
+ .json(&ollama_stream_payload(config, request, &model)?)
1593
1802
  .send()
1594
1803
  .await?
1595
1804
  .error_for_status()?;
1596
- collect_stream(response, StreamFormat::Ollama, on_chunk)
1597
- .await
1598
- .map(|text| (model, text))
1805
+ collect_stream_reply(response, StreamFormat::Ollama, model, on_event).await
1599
1806
  }
1600
1807
 
1601
- async fn stream_anthropic<F>(
1808
+ async fn stream_anthropic_reply<F>(
1602
1809
  client: &Client,
1603
1810
  config: &MintConfig,
1604
1811
  request: &ChatRequest,
1605
- on_chunk: &mut F,
1606
- ) -> Result<(String, String), ChatError>
1812
+ on_event: &mut F,
1813
+ ) -> Result<ProviderReply, ChatError>
1607
1814
  where
1608
- F: FnMut(String),
1815
+ F: FnMut(ChatStreamEvent),
1609
1816
  {
1610
1817
  let api_key = provider_key(&config.anthropic_api_key, "ANTHROPIC_API_KEY");
1611
1818
  required_key("anthropic", &api_key)?;
@@ -1615,37 +1822,25 @@ where
1615
1822
  .header("x-api-key", api_key)
1616
1823
  .header("anthropic-version", "2023-06-01")
1617
1824
  .header("anthropic-beta", "prompt-caching-2024-07-31")
1618
- .json(&json!({
1619
- "model": model,
1620
- "max_tokens": 8192,
1621
- "stream": true,
1622
- "system": request.system_instruction,
1623
- "messages": [{ "role": "user", "content": request.message }],
1624
- "temperature": effective_temperature(config, request),
1625
- }))
1825
+ .json(&anthropic_chat_payload(config, &model, request, true)?)
1626
1826
  .send()
1627
1827
  .await?
1628
1828
  .error_for_status()?;
1629
- collect_stream(response, StreamFormat::Anthropic, on_chunk)
1630
- .await
1631
- .map(|text| (model, text))
1829
+ collect_stream_reply(response, StreamFormat::Anthropic, model, on_event).await
1632
1830
  }
1633
1831
 
1634
- async fn stream_huggingface<F>(
1832
+ async fn stream_huggingface_reply<F>(
1635
1833
  client: &Client,
1636
1834
  config: &MintConfig,
1637
1835
  request: &ChatRequest,
1638
- on_chunk: &mut F,
1639
- ) -> Result<(String, String), ChatError>
1836
+ on_event: &mut F,
1837
+ ) -> Result<ProviderReply, ChatError>
1640
1838
  where
1641
- F: FnMut(String),
1839
+ F: FnMut(ChatStreamEvent),
1642
1840
  {
1643
1841
  let api_key = provider_key(&config.hf_api_key, "HF_TOKEN");
1644
1842
  required_key("huggingface", &api_key)?;
1645
1843
  let model = config.hf_model.clone();
1646
- // Same fix as `call_huggingface`: build the real `messages`/`tools`
1647
- // payload instead of a bare `{system, user: request.message}` request
1648
- // that dropped the agent loop's tool-call history on a fallback.
1649
1844
  let response = client
1650
1845
  .post("https://router.huggingface.co/v1/chat/completions")
1651
1846
  .bearer_auth(api_key)
@@ -1653,12 +1848,41 @@ where
1653
1848
  .send()
1654
1849
  .await?
1655
1850
  .error_for_status()?;
1656
- collect_stream(response, StreamFormat::OpenAi, on_chunk)
1657
- .await
1658
- .map(|text| (model, text))
1851
+ collect_stream_reply(response, StreamFormat::OpenAi, model, on_event).await
1852
+ }
1853
+
1854
+ async fn stream_custom_provider_reply<F>(
1855
+ client: &Client,
1856
+ config: &MintConfig,
1857
+ request: &ChatRequest,
1858
+ on_event: &mut F,
1859
+ ) -> Result<ProviderReply, ChatError>
1860
+ where
1861
+ F: FnMut(ChatStreamEvent),
1862
+ {
1863
+ let provider = config
1864
+ .resolve_custom_provider(&config.ai_provider)
1865
+ .ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
1866
+ let model = resolve_custom_model(config);
1867
+ let mut builder = client
1868
+ .post(format!(
1869
+ "{}/chat/completions",
1870
+ provider.base_url.trim_end_matches('/')
1871
+ ))
1872
+ .json(&openai_chat_payload(config, &model, request, true)?);
1873
+ if !provider.api_key.is_empty() {
1874
+ builder = builder.bearer_auth(&provider.api_key);
1875
+ }
1876
+ for header in &provider.headers {
1877
+ if !header.name.is_empty() {
1878
+ builder = builder.header(header.name.as_str(), header.value.as_str());
1879
+ }
1880
+ }
1881
+ let response = builder.send().await?.error_for_status()?;
1882
+ collect_stream_reply(response, StreamFormat::OpenAi, model, on_event).await
1659
1883
  }
1660
1884
 
1661
- #[derive(Clone, Copy)]
1885
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1662
1886
  enum StreamFormat {
1663
1887
  Gemini,
1664
1888
  OpenAi,
@@ -1666,55 +1890,442 @@ enum StreamFormat {
1666
1890
  Anthropic,
1667
1891
  }
1668
1892
 
1669
- async fn collect_stream<F>(
1893
+ #[derive(Default)]
1894
+ struct StreamToolCall {
1895
+ id: String,
1896
+ name: String,
1897
+ arguments: String,
1898
+ input: Option<Value>,
1899
+ thought_signature: Option<String>,
1900
+ }
1901
+
1902
+ #[derive(Default)]
1903
+ struct StreamReplyAccumulator {
1904
+ text: String,
1905
+ thought: String,
1906
+ tag_buffer: String,
1907
+ inside_think_tag: bool,
1908
+ tool_calls: Vec<StreamToolCall>,
1909
+ stop_reason: Option<String>,
1910
+ input_tokens: Option<u32>,
1911
+ output_tokens: Option<u32>,
1912
+ anthropic_blocks: std::collections::HashMap<usize, usize>,
1913
+ }
1914
+
1915
+ impl StreamReplyAccumulator {
1916
+ fn tool_at(&mut self, index: usize) -> &mut StreamToolCall {
1917
+ while self.tool_calls.len() <= index {
1918
+ self.tool_calls.push(StreamToolCall::default());
1919
+ }
1920
+ &mut self.tool_calls[index]
1921
+ }
1922
+
1923
+ fn emit_text<F>(&mut self, delta: &str, on_event: &mut F)
1924
+ where
1925
+ F: FnMut(ChatStreamEvent),
1926
+ {
1927
+ if !delta.is_empty() {
1928
+ on_event(ChatStreamEvent::TextDelta {
1929
+ delta: delta.to_owned(),
1930
+ });
1931
+ }
1932
+ }
1933
+
1934
+ /// Route inline `<think>` content without assuming tag boundaries line up
1935
+ /// with provider chunks. A possible partial tag is retained until the next
1936
+ /// chunk; ordinary text is emitted immediately.
1937
+ fn push_text<F>(&mut self, delta: &str, on_event: &mut F)
1938
+ where
1939
+ F: FnMut(ChatStreamEvent),
1940
+ {
1941
+ if delta.is_empty() {
1942
+ return;
1943
+ }
1944
+ self.text.push_str(delta);
1945
+ self.tag_buffer.push_str(delta);
1946
+ loop {
1947
+ let tag = if self.inside_think_tag {
1948
+ "</think>"
1949
+ } else {
1950
+ "<think>"
1951
+ };
1952
+ if let Some(index) = self.tag_buffer.find(tag) {
1953
+ let before = self.tag_buffer[..index].to_owned();
1954
+ self.tag_buffer.drain(..index + tag.len());
1955
+ if self.inside_think_tag {
1956
+ self.push_reasoning(&before, on_event);
1957
+ } else {
1958
+ self.emit_text(&before, on_event);
1959
+ }
1960
+ self.inside_think_tag = !self.inside_think_tag;
1961
+ continue;
1962
+ }
1963
+
1964
+ let retain_from = self
1965
+ .tag_buffer
1966
+ .rfind('<')
1967
+ .and_then(|index| tag.starts_with(&self.tag_buffer[index..]).then_some(index));
1968
+ let emit_end = retain_from.unwrap_or(self.tag_buffer.len());
1969
+ if emit_end > 0 {
1970
+ let ready = self.tag_buffer[..emit_end].to_owned();
1971
+ self.tag_buffer.drain(..emit_end);
1972
+ if self.inside_think_tag {
1973
+ self.push_reasoning(&ready, on_event);
1974
+ } else {
1975
+ self.emit_text(&ready, on_event);
1976
+ }
1977
+ }
1978
+ break;
1979
+ }
1980
+ }
1981
+
1982
+ fn flush_tag_buffer<F>(&mut self, on_event: &mut F)
1983
+ where
1984
+ F: FnMut(ChatStreamEvent),
1985
+ {
1986
+ if self.tag_buffer.is_empty() {
1987
+ return;
1988
+ }
1989
+ let remaining = std::mem::take(&mut self.tag_buffer);
1990
+ if self.inside_think_tag {
1991
+ self.push_reasoning(&remaining, on_event);
1992
+ } else {
1993
+ self.emit_text(&remaining, on_event);
1994
+ }
1995
+ }
1996
+
1997
+ fn push_reasoning<F>(&mut self, delta: &str, on_event: &mut F)
1998
+ where
1999
+ F: FnMut(ChatStreamEvent),
2000
+ {
2001
+ if !delta.is_empty() {
2002
+ self.thought.push_str(delta);
2003
+ on_event(ChatStreamEvent::ReasoningDelta {
2004
+ delta: delta.to_owned(),
2005
+ });
2006
+ }
2007
+ }
2008
+
2009
+ fn consume<F>(&mut self, format: StreamFormat, value: &Value, on_event: &mut F)
2010
+ where
2011
+ F: FnMut(ChatStreamEvent),
2012
+ {
2013
+ match format {
2014
+ StreamFormat::OpenAi => self.consume_openai(value, on_event),
2015
+ StreamFormat::Gemini => self.consume_gemini(value, on_event),
2016
+ StreamFormat::Ollama => self.consume_ollama(value, on_event),
2017
+ StreamFormat::Anthropic => self.consume_anthropic(value, on_event),
2018
+ }
2019
+ }
2020
+
2021
+ fn consume_openai<F>(&mut self, value: &Value, on_event: &mut F)
2022
+ where
2023
+ F: FnMut(ChatStreamEvent),
2024
+ {
2025
+ let choice = &value["choices"][0];
2026
+ let delta = &choice["delta"];
2027
+ if let Some(text) = delta["content"].as_str() {
2028
+ self.push_text(text, on_event);
2029
+ }
2030
+ if let Some(reasoning) = delta["reasoning_content"]
2031
+ .as_str()
2032
+ .or_else(|| delta["reasoning"].as_str())
2033
+ .or_else(|| delta["thinking"].as_str())
2034
+ {
2035
+ self.push_reasoning(reasoning, on_event);
2036
+ }
2037
+ if let Some(details) = delta["reasoning_details"].as_array() {
2038
+ for detail in details {
2039
+ if let Some(reasoning) = detail["text"].as_str() {
2040
+ self.push_reasoning(reasoning, on_event);
2041
+ }
2042
+ }
2043
+ }
2044
+ if let Some(calls) = delta["tool_calls"].as_array() {
2045
+ for (fallback_index, call) in calls.iter().enumerate() {
2046
+ let index = call["index"]
2047
+ .as_u64()
2048
+ .map(|n| n as usize)
2049
+ .unwrap_or(fallback_index);
2050
+ let name_delta = call["function"]["name"].as_str().map(str::to_owned);
2051
+ let arguments = call["function"]["arguments"].as_str().map(str::to_owned);
2052
+ let current_name = {
2053
+ let tool = self.tool_at(index);
2054
+ if let Some(id) = call["id"].as_str() {
2055
+ tool.id.push_str(id);
2056
+ }
2057
+ if let Some(name) = &name_delta {
2058
+ tool.name.push_str(name);
2059
+ }
2060
+ if let Some(arguments) = &arguments {
2061
+ tool.arguments.push_str(arguments);
2062
+ }
2063
+ tool.name.clone()
2064
+ };
2065
+ if name_delta.is_some() || arguments.is_some() {
2066
+ on_event(ChatStreamEvent::ToolCallDelta {
2067
+ index,
2068
+ name: name_delta.map(|_| current_name),
2069
+ arguments,
2070
+ input: None,
2071
+ });
2072
+ }
2073
+ }
2074
+ }
2075
+ if let Some(reason) = choice["finish_reason"].as_str() {
2076
+ self.stop_reason = Some(reason.to_owned());
2077
+ }
2078
+ self.input_tokens = value["usage"]["prompt_tokens"]
2079
+ .as_u64()
2080
+ .map(|n| n as u32)
2081
+ .or(self.input_tokens);
2082
+ self.output_tokens = value["usage"]["completion_tokens"]
2083
+ .as_u64()
2084
+ .map(|n| n as u32)
2085
+ .or(self.output_tokens);
2086
+ }
2087
+
2088
+ fn consume_gemini<F>(&mut self, value: &Value, on_event: &mut F)
2089
+ where
2090
+ F: FnMut(ChatStreamEvent),
2091
+ {
2092
+ if let Some(parts) = value["candidates"][0]["content"]["parts"].as_array() {
2093
+ for (index, part) in parts.iter().enumerate() {
2094
+ if let Some(text) = part["text"].as_str() {
2095
+ if part["thought"].as_bool().unwrap_or(false) {
2096
+ self.push_reasoning(text, on_event);
2097
+ } else {
2098
+ self.push_text(text, on_event);
2099
+ }
2100
+ }
2101
+ if let Some(call) = part.get("functionCall") {
2102
+ let tool = self.tool_at(index);
2103
+ tool.id = format!("call_{index}");
2104
+ tool.name = call["name"].as_str().unwrap_or_default().to_owned();
2105
+ tool.input = Some(call["args"].clone());
2106
+ tool.thought_signature = part["thoughtSignature"].as_str().map(str::to_owned);
2107
+ on_event(ChatStreamEvent::ToolCallDelta {
2108
+ index,
2109
+ name: call["name"].as_str().map(str::to_owned),
2110
+ arguments: None,
2111
+ input: Some(call["args"].clone()),
2112
+ });
2113
+ }
2114
+ }
2115
+ }
2116
+ self.stop_reason = value["candidates"][0]["finishReason"]
2117
+ .as_str()
2118
+ .map(str::to_owned)
2119
+ .or_else(|| self.stop_reason.take());
2120
+ self.input_tokens = value["usageMetadata"]["promptTokenCount"]
2121
+ .as_u64()
2122
+ .map(|n| n as u32)
2123
+ .or(self.input_tokens);
2124
+ self.output_tokens = value["usageMetadata"]["candidatesTokenCount"]
2125
+ .as_u64()
2126
+ .map(|n| n as u32)
2127
+ .or(self.output_tokens);
2128
+ }
2129
+
2130
+ fn consume_ollama<F>(&mut self, value: &Value, on_event: &mut F)
2131
+ where
2132
+ F: FnMut(ChatStreamEvent),
2133
+ {
2134
+ let message = &value["message"];
2135
+ if let Some(text) = message["content"].as_str() {
2136
+ self.push_text(text, on_event);
2137
+ }
2138
+ if let Some(reasoning) = message["thinking"]
2139
+ .as_str()
2140
+ .or_else(|| message["reasoning_content"].as_str())
2141
+ {
2142
+ self.push_reasoning(reasoning, on_event);
2143
+ }
2144
+ if let Some(calls) = message["tool_calls"].as_array() {
2145
+ for (index, call) in calls.iter().enumerate() {
2146
+ let tool = self.tool_at(index);
2147
+ tool.id = call["id"]
2148
+ .as_str()
2149
+ .map(str::to_owned)
2150
+ .unwrap_or_else(|| format!("call_{index}"));
2151
+ tool.name = call["function"]["name"]
2152
+ .as_str()
2153
+ .unwrap_or_default()
2154
+ .to_owned();
2155
+ tool.input = Some(call["function"]["arguments"].clone());
2156
+ on_event(ChatStreamEvent::ToolCallDelta {
2157
+ index,
2158
+ name: call["function"]["name"].as_str().map(str::to_owned),
2159
+ arguments: None,
2160
+ input: Some(call["function"]["arguments"].clone()),
2161
+ });
2162
+ }
2163
+ }
2164
+ self.input_tokens = value["prompt_eval_count"]
2165
+ .as_u64()
2166
+ .map(|n| n as u32)
2167
+ .or(self.input_tokens);
2168
+ self.output_tokens = value["eval_count"]
2169
+ .as_u64()
2170
+ .map(|n| n as u32)
2171
+ .or(self.output_tokens);
2172
+ if value["done"].as_bool().unwrap_or(false) {
2173
+ self.stop_reason = value["done_reason"]
2174
+ .as_str()
2175
+ .map(str::to_owned)
2176
+ .or_else(|| Some("stop".to_owned()));
2177
+ }
2178
+ }
2179
+
2180
+ fn consume_anthropic<F>(&mut self, value: &Value, on_event: &mut F)
2181
+ where
2182
+ F: FnMut(ChatStreamEvent),
2183
+ {
2184
+ match value["type"].as_str() {
2185
+ Some("message_start") => {
2186
+ self.input_tokens = anthropic_input_tokens(&value["message"]["usage"]);
2187
+ }
2188
+ Some("content_block_start") => {
2189
+ let block_index = value["index"].as_u64().unwrap_or(0) as usize;
2190
+ let block = &value["content_block"];
2191
+ if block["type"].as_str() == Some("tool_use") {
2192
+ let tool_index = self.tool_calls.len();
2193
+ let tool = self.tool_at(tool_index);
2194
+ tool.id = block["id"].as_str().unwrap_or_default().to_owned();
2195
+ tool.name = block["name"].as_str().unwrap_or_default().to_owned();
2196
+ self.anthropic_blocks.insert(block_index, tool_index);
2197
+ on_event(ChatStreamEvent::ToolCallDelta {
2198
+ index: tool_index,
2199
+ name: block["name"].as_str().map(str::to_owned),
2200
+ arguments: None,
2201
+ input: None,
2202
+ });
2203
+ }
2204
+ }
2205
+ Some("content_block_delta") => {
2206
+ let delta = &value["delta"];
2207
+ match delta["type"].as_str() {
2208
+ Some("text_delta") => {
2209
+ if let Some(text) = delta["text"].as_str() {
2210
+ self.push_text(text, on_event);
2211
+ }
2212
+ }
2213
+ Some("thinking_delta") => {
2214
+ if let Some(thinking) = delta["thinking"].as_str() {
2215
+ self.push_reasoning(thinking, on_event);
2216
+ }
2217
+ }
2218
+ Some("input_json_delta") => {
2219
+ let block_index = value["index"].as_u64().unwrap_or(0) as usize;
2220
+ if let Some(tool_index) = self.anthropic_blocks.get(&block_index).copied()
2221
+ && let Some(json) = delta["partial_json"].as_str()
2222
+ {
2223
+ self.tool_at(tool_index).arguments.push_str(json);
2224
+ on_event(ChatStreamEvent::ToolCallDelta {
2225
+ index: tool_index,
2226
+ name: None,
2227
+ arguments: Some(json.to_owned()),
2228
+ input: None,
2229
+ });
2230
+ }
2231
+ }
2232
+ _ => {}
2233
+ }
2234
+ }
2235
+ Some("message_delta") => {
2236
+ self.stop_reason = value["delta"]["stop_reason"]
2237
+ .as_str()
2238
+ .map(str::to_owned)
2239
+ .or_else(|| self.stop_reason.take());
2240
+ self.output_tokens = value["usage"]["output_tokens"]
2241
+ .as_u64()
2242
+ .map(|n| n as u32)
2243
+ .or(self.output_tokens);
2244
+ }
2245
+ _ => {}
2246
+ }
2247
+ }
2248
+
2249
+ fn finish(self, model: String) -> Result<ProviderReply, ChatError> {
2250
+ let (tag_thought, clean_text) = extract_think_tag(&self.text);
2251
+ let thought = if self.thought.trim().is_empty() {
2252
+ tag_thought
2253
+ } else {
2254
+ Some(self.thought.trim().to_owned())
2255
+ };
2256
+ let calls: Vec<ToolCall> = self
2257
+ .tool_calls
2258
+ .into_iter()
2259
+ .filter(|tool| !tool.name.is_empty())
2260
+ .map(|tool| {
2261
+ let input = tool
2262
+ .input
2263
+ .unwrap_or_else(|| decode_tool_arguments(&Value::String(tool.arguments)));
2264
+ ToolCall {
2265
+ id: tool.id,
2266
+ name: tool.name,
2267
+ input,
2268
+ thought_signature: tool.thought_signature,
2269
+ }
2270
+ })
2271
+ .collect();
2272
+ if clean_text.is_empty() && calls.is_empty() && thought.is_none() {
2273
+ return Err(ChatError::MissingResponseText);
2274
+ }
2275
+ Ok(ProviderReply {
2276
+ model,
2277
+ text: if clean_text.is_empty() && calls.is_empty() {
2278
+ thought.clone().unwrap_or_default()
2279
+ } else {
2280
+ clean_text
2281
+ },
2282
+ thought,
2283
+ tool_calls: (!calls.is_empty()).then_some(calls),
2284
+ stop_reason: self.stop_reason,
2285
+ input_tokens: self.input_tokens,
2286
+ output_tokens: self.output_tokens,
2287
+ })
2288
+ }
2289
+ }
2290
+
2291
+ async fn collect_stream_reply<F>(
1670
2292
  response: reqwest::Response,
1671
2293
  format: StreamFormat,
1672
- on_chunk: &mut F,
1673
- ) -> Result<String, ChatError>
2294
+ model: String,
2295
+ on_event: &mut F,
2296
+ ) -> Result<ProviderReply, ChatError>
1674
2297
  where
1675
- F: FnMut(String),
2298
+ F: FnMut(ChatStreamEvent),
1676
2299
  {
1677
2300
  let mut bytes = response.bytes_stream();
1678
2301
  let mut buffer = String::new();
1679
- let mut text = String::new();
2302
+ let mut accumulator = StreamReplyAccumulator::default();
1680
2303
  while let Some(chunk) = bytes.next().await {
1681
2304
  buffer.push_str(&String::from_utf8_lossy(&chunk?));
1682
2305
  while let Some(index) = buffer.find('\n') {
1683
2306
  let line = buffer[..index].trim().to_owned();
1684
2307
  buffer.drain(..=index);
1685
- if let Some(chunk) = parse_stream_line(format, &line) {
1686
- on_chunk(chunk.clone());
1687
- text.push_str(&chunk);
2308
+ if let Some(value) = parse_stream_value(format, &line) {
2309
+ accumulator.consume(format, &value, on_event);
1688
2310
  }
1689
2311
  }
1690
2312
  }
1691
- if let Some(chunk) = parse_stream_line(format, buffer.trim()) {
1692
- on_chunk(chunk.clone());
1693
- text.push_str(&chunk);
1694
- }
1695
- if text.is_empty() {
1696
- Err(ChatError::MissingResponseText)
1697
- } else {
1698
- Ok(text)
2313
+ if let Some(value) = parse_stream_value(format, buffer.trim()) {
2314
+ accumulator.consume(format, &value, on_event);
1699
2315
  }
2316
+ accumulator.flush_tag_buffer(on_event);
2317
+ accumulator.finish(model)
1700
2318
  }
1701
2319
 
1702
- fn parse_stream_line(format: StreamFormat, line: &str) -> Option<String> {
2320
+ fn parse_stream_value(format: StreamFormat, line: &str) -> Option<Value> {
1703
2321
  let payload = match format {
1704
2322
  StreamFormat::Ollama => line,
1705
- _ => line.strip_prefix("data: ")?.trim(),
2323
+ _ => line.strip_prefix("data:")?.trim(),
1706
2324
  };
1707
2325
  if payload.is_empty() || payload == "[DONE]" {
1708
2326
  return None;
1709
2327
  }
1710
- let value: Value = serde_json::from_str(payload).ok()?;
1711
- let text = match format {
1712
- StreamFormat::Gemini => value["candidates"][0]["content"]["parts"][0]["text"].as_str(),
1713
- StreamFormat::OpenAi => value["choices"][0]["delta"]["content"].as_str(),
1714
- StreamFormat::Ollama => value["message"]["content"].as_str(),
1715
- StreamFormat::Anthropic => value["delta"]["text"].as_str(),
1716
- }?;
1717
- (!text.is_empty()).then(|| text.to_owned())
2328
+ serde_json::from_str(payload).ok()
1718
2329
  }
1719
2330
 
1720
2331
  fn required_key(provider: &str, key: &str) -> Result<(), ChatError> {
@@ -1786,42 +2397,6 @@ async fn call_custom_provider(
1786
2397
  parse_openai_style_reply(model, &response)
1787
2398
  }
1788
2399
 
1789
- async fn stream_custom_provider<F>(
1790
- client: &Client,
1791
- config: &crate::MintConfig,
1792
- request: &ChatRequest,
1793
- on_chunk: &mut F,
1794
- ) -> Result<(String, String), ChatError>
1795
- where
1796
- F: FnMut(String),
1797
- {
1798
- let provider = config
1799
- .resolve_custom_provider(&config.ai_provider)
1800
- .ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
1801
-
1802
- let base_url = provider.base_url.trim_end_matches('/');
1803
- let model = resolve_custom_model(config);
1804
- let payload = openai_chat_payload(config, &model, request, true)?;
1805
-
1806
- let mut builder = client
1807
- .post(format!("{base_url}/chat/completions"))
1808
- .json(&payload);
1809
-
1810
- if !provider.api_key.is_empty() {
1811
- builder = builder.bearer_auth(&provider.api_key);
1812
- }
1813
- for h in &provider.headers {
1814
- if !h.name.is_empty() {
1815
- builder = builder.header(h.name.as_str(), h.value.as_str());
1816
- }
1817
- }
1818
-
1819
- let response = builder.send().await?.error_for_status()?;
1820
- collect_stream(response, StreamFormat::OpenAi, on_chunk)
1821
- .await
1822
- .map(|text| (model, text))
1823
- }
1824
-
1825
2400
  fn provider_key(configured: &str, environment_variable: &str) -> String {
1826
2401
  if configured.trim().is_empty() {
1827
2402
  std::env::var(environment_variable).unwrap_or_default()
@@ -1913,13 +2488,15 @@ fn gemini_chat_payload(config: &MintConfig, request: &ChatRequest) -> Result<Val
1913
2488
  "systemInstruction": { "parts": [{ "text": request.system_instruction }] },
1914
2489
  "contents": [{ "role": "user", "parts": gemini_parts(request)? }]
1915
2490
  });
1916
- if wants_agent_json(request) {
1917
- let mut gen_cfg = gemini_agent_generation_config(config);
1918
- gen_cfg["temperature"] = json!(temp);
1919
- payload["generationConfig"] = gen_cfg;
2491
+ let mut gen_cfg = if wants_agent_json(request) {
2492
+ let mut cfg = gemini_agent_generation_config(config);
2493
+ cfg["temperature"] = json!(temp);
2494
+ cfg
1920
2495
  } else {
1921
- payload["generationConfig"] = json!({ "temperature": temp });
1922
- }
2496
+ json!({ "temperature": temp })
2497
+ };
2498
+ gemini_apply_thinking_config(&mut gen_cfg, config, request, &config.gemini_model);
2499
+ payload["generationConfig"] = gen_cfg;
1923
2500
  Ok(payload)
1924
2501
  }
1925
2502
 
@@ -2042,6 +2619,47 @@ pub(crate) fn effective_temperature(config: &MintConfig, request: &ChatRequest)
2042
2619
  .unwrap_or_else(|| config.resolved_temperature())
2043
2620
  }
2044
2621
 
2622
+ /// Resolves (enabled, effort) for this request and model, prioritizing request overrides.
2623
+ pub(crate) fn effective_thinking(
2624
+ config: &MintConfig,
2625
+ request: &ChatRequest,
2626
+ model: &str,
2627
+ ) -> (bool, String) {
2628
+ let enabled = request
2629
+ .thinking_enabled
2630
+ .unwrap_or_else(|| config.resolved_thinking_enabled_for_model(&config.ai_provider, model));
2631
+ let effort = request
2632
+ .thinking_effort
2633
+ .as_deref()
2634
+ .map(str::to_ascii_lowercase)
2635
+ .unwrap_or_else(|| config.resolved_thinking_effort_for_model(model).to_string());
2636
+ (enabled, effort)
2637
+ }
2638
+
2639
+ pub(crate) fn gemini_apply_thinking_config(
2640
+ gen_cfg: &mut Value,
2641
+ config: &MintConfig,
2642
+ request: &ChatRequest,
2643
+ model: &str,
2644
+ ) {
2645
+ if !MintConfig::is_thinking_supported_for_model("gemini", model) {
2646
+ return;
2647
+ }
2648
+ let (enabled, effort) = effective_thinking(config, request, model);
2649
+ if !enabled {
2650
+ gen_cfg["thinkingConfig"] = json!({ "thinkingBudget": 0 });
2651
+ } else {
2652
+ let budget = match effort.as_str() {
2653
+ "low" => 1024,
2654
+ "medium" => 4096,
2655
+ "high" => 16384,
2656
+ "extra_high" => 32768,
2657
+ _ => 4096,
2658
+ };
2659
+ gen_cfg["thinkingConfig"] = json!({ "thinkingBudget": budget });
2660
+ }
2661
+ }
2662
+
2045
2663
  fn openai_chat_payload(
2046
2664
  config: &MintConfig,
2047
2665
  model: &str,
@@ -2070,21 +2688,50 @@ fn openai_chat_payload(
2070
2688
  }),
2071
2689
  ]
2072
2690
  };
2691
+ // Left unset, several OpenAI-compatible providers (DeepSeek in
2692
+ // particular) default to a much lower output cap than the model can
2693
+ // actually produce, which silently truncates long structured answers
2694
+ // (a code block followed by a couple of markdown tables is enough to
2695
+ // hit it) — matches the explicit cap already used for Anthropic in
2696
+ // `anthropic_chat_payload`/`stream_anthropic`.
2697
+ // For Groq endpoints, free tiers enforce a tight 6,000–8,000 TPM limit
2698
+ // where a static 8192 max_tokens triggers immediate HTTP 413 / rate limit errors.
2699
+ let max_tokens = {
2700
+ let is_groq = config
2701
+ .resolve_custom_provider(&config.ai_provider)
2702
+ .map(|cp| cp.base_url.contains("groq.com"))
2703
+ .unwrap_or(false);
2704
+ if is_groq {
2705
+ if request.tools.is_some() { 2048 } else { 4096 }
2706
+ } else {
2707
+ 8192
2708
+ }
2709
+ };
2073
2710
  let mut payload = json!({
2074
2711
  "model": model,
2075
2712
  "stream": stream,
2076
2713
  "messages": messages,
2077
- // Left unset, several OpenAI-compatible providers (DeepSeek in
2078
- // particular) default to a much lower output cap than the model can
2079
- // actually produce, which silently truncates long structured answers
2080
- // (a code block followed by a couple of markdown tables is enough to
2081
- // hit it) — matches the explicit cap already used for Anthropic in
2082
- // `anthropic_chat_payload`/`stream_anthropic`.
2083
- "max_tokens": 8192,
2714
+ "max_tokens": max_tokens,
2084
2715
  });
2716
+ if stream {
2717
+ payload["stream_options"] = json!({ "include_usage": true });
2718
+ }
2085
2719
  // OpenAI reasoning models (o1, o3, etc.) reject explicit temperature parameter.
2086
2720
  let is_reasoning_model = model.starts_with("o1") || model.starts_with("o3");
2087
- if !is_reasoning_model {
2721
+ let (thinking_enabled, thinking_effort) = effective_thinking(config, request, model);
2722
+ if is_reasoning_model {
2723
+ if thinking_enabled {
2724
+ let effort_str = match thinking_effort.as_str() {
2725
+ "low" => "low",
2726
+ "medium" => "medium",
2727
+ "high" | "extra_high" => "high",
2728
+ _ => "medium",
2729
+ };
2730
+ payload["reasoning_effort"] = json!(effort_str);
2731
+ } else {
2732
+ payload["reasoning_effort"] = json!("low");
2733
+ }
2734
+ } else {
2088
2735
  payload["temperature"] = json!(effective_temperature(config, request));
2089
2736
  }
2090
2737
  if wants_agent_json(request) {
@@ -2369,6 +3016,7 @@ mod tests {
2369
3016
  messages: None,
2370
3017
  tools: None,
2371
3018
  temperature: None,
3019
+ ..Default::default()
2372
3020
  };
2373
3021
 
2374
3022
  let error = require_supported_attachments("openai", &request).unwrap_err();
@@ -2379,28 +3027,117 @@ mod tests {
2379
3027
 
2380
3028
  #[test]
2381
3029
  fn parses_stream_provider_formats() {
2382
- assert_eq!(
2383
- parse_stream_line(
2384
- StreamFormat::OpenAi,
2385
- r#"data: {"choices":[{"delta":{"content":"hello"}}]}"#
2386
- )
2387
- .as_deref(),
2388
- Some("hello")
3030
+ let openai = parse_stream_value(
3031
+ StreamFormat::OpenAi,
3032
+ r#"data: {"choices":[{"delta":{"content":"hello"}}]}"#,
3033
+ )
3034
+ .unwrap();
3035
+ assert_eq!(openai["choices"][0]["delta"]["content"], "hello");
3036
+ let ollama =
3037
+ parse_stream_value(StreamFormat::Ollama, r#"{"message":{"content":"hi"}}"#).unwrap();
3038
+ assert_eq!(ollama["message"]["content"], "hi");
3039
+ assert!(parse_stream_value(StreamFormat::Anthropic, "data: [DONE]").is_none());
3040
+ }
3041
+
3042
+ #[test]
3043
+ fn malformed_tool_arguments_survive_provider_parsing_for_rejection() {
3044
+ let malformed = r#"{"path":"~/Downloads/YOUTUBE_DL""#;
3045
+ let function = json!({"name": "list_files", "arguments": malformed});
3046
+ let openai = parse_openai_style_reply(
3047
+ "test".into(),
3048
+ &json!({
3049
+ "choices": [{"message": {"tool_calls": [{"id": "call_1", "function": function}]}}]
3050
+ }),
3051
+ )
3052
+ .unwrap();
3053
+ let ollama = parse_ollama_reply(
3054
+ "test".into(),
3055
+ &json!({
3056
+ "message": {"tool_calls": [{"function": function}]}
3057
+ }),
3058
+ )
3059
+ .unwrap();
3060
+ let mut accumulator = StreamReplyAccumulator::default();
3061
+ accumulator.consume_openai(&json!({
3062
+ "choices": [{"delta": {"tool_calls": [{"index": 0, "id": "call_1", "function": function}]}}]
3063
+ }), &mut |_| {});
3064
+ let streamed = accumulator.finish("test".into()).unwrap();
3065
+ for reply in [openai, ollama, streamed] {
3066
+ let calls = reply.tool_calls.unwrap();
3067
+ assert_eq!(calls.len(), 1);
3068
+ assert_eq!(calls[0].input, Value::String(malformed.into()));
3069
+ }
3070
+ assert_eq!(decode_tool_arguments(&json!("{}")), json!({}));
3071
+ assert_eq!(decode_tool_arguments(&Value::Null), Value::Null);
3072
+ }
3073
+
3074
+ #[test]
3075
+ fn accumulates_openai_reasoning_tools_and_usage() {
3076
+ let mut accumulator = StreamReplyAccumulator::default();
3077
+ let mut events = Vec::new();
3078
+ accumulator.consume_openai(
3079
+ &json!({
3080
+ "choices": [{"delta": {"reasoning_content": "inspect "}}]
3081
+ }),
3082
+ &mut |event| events.push(event),
2389
3083
  );
3084
+ accumulator.consume_openai(&json!({
3085
+ "choices": [{"delta": {"reasoning_content": "files", "tool_calls": [{
3086
+ "index": 0, "id": "call_1", "function": {"name": "read_file", "arguments": "{\"path\":"}
3087
+ }]}}]
3088
+ }), &mut |event| events.push(event));
3089
+ accumulator.consume_openai(
3090
+ &json!({
3091
+ "choices": [{"delta": {"tool_calls": [{
3092
+ "index": 0, "function": {"arguments": "\"README.md\"}"}
3093
+ }]}, "finish_reason": "tool_calls"}],
3094
+ "usage": {"prompt_tokens": 10, "completion_tokens": 5}
3095
+ }),
3096
+ &mut |event| events.push(event),
3097
+ );
3098
+ accumulator.flush_tag_buffer(&mut |event| events.push(event));
3099
+ let reply = accumulator.finish("test".into()).unwrap();
3100
+ assert_eq!(reply.thought.as_deref(), Some("inspect files"));
3101
+ assert_eq!(reply.stop_reason.as_deref(), Some("tool_calls"));
3102
+ assert_eq!(reply.input_tokens, Some(10));
3103
+ assert_eq!(reply.tool_calls.unwrap()[0].input["path"], "README.md");
2390
3104
  assert_eq!(
2391
- parse_stream_line(StreamFormat::Ollama, r#"{"message":{"content":"hi"}}"#).as_deref(),
2392
- Some("hi")
3105
+ events
3106
+ .iter()
3107
+ .filter(|event| matches!(event, ChatStreamEvent::ReasoningDelta { .. }))
3108
+ .count(),
3109
+ 2
2393
3110
  );
2394
3111
  assert_eq!(
2395
- parse_stream_line(
2396
- StreamFormat::Anthropic,
2397
- r#"data: {"delta":{"type":"text_delta","text":"hey"}}"#
2398
- )
2399
- .as_deref(),
2400
- Some("hey")
3112
+ events
3113
+ .iter()
3114
+ .filter(|event| matches!(event, ChatStreamEvent::ToolCallDelta { .. }))
3115
+ .count(),
3116
+ 2
2401
3117
  );
2402
3118
  }
2403
3119
 
3120
+ #[test]
3121
+ fn streams_think_tags_split_across_chunks() {
3122
+ let mut accumulator = StreamReplyAccumulator::default();
3123
+ let mut events = Vec::new();
3124
+ for chunk in ["ก่อน<thi", "nk>คิด", "อยู่</th", "ink>หลัง"] {
3125
+ accumulator.push_text(chunk, &mut |event| events.push(event));
3126
+ }
3127
+ accumulator.flush_tag_buffer(&mut |event| events.push(event));
3128
+ let reply = accumulator.finish("test".into()).unwrap();
3129
+ assert_eq!(reply.text, "ก่อนหลัง");
3130
+ assert_eq!(reply.thought.as_deref(), Some("คิดอยู่"));
3131
+ let streamed_reasoning: String = events
3132
+ .iter()
3133
+ .filter_map(|event| match event {
3134
+ ChatStreamEvent::ReasoningDelta { delta } => Some(delta.as_str()),
3135
+ _ => None,
3136
+ })
3137
+ .collect();
3138
+ assert_eq!(streamed_reasoning, "คิดอยู่");
3139
+ }
3140
+
2404
3141
  #[test]
2405
3142
  fn builds_gemini_multimodal_parts() {
2406
3143
  let parts = gemini_parts(&ChatRequest {
@@ -2418,6 +3155,7 @@ mod tests {
2418
3155
  messages: None,
2419
3156
  tools: None,
2420
3157
  temperature: None,
3158
+ ..Default::default()
2421
3159
  })
2422
3160
  .unwrap();
2423
3161
  assert_eq!(parts.len(), 3);
@@ -2453,6 +3191,7 @@ mod tests {
2453
3191
  messages: Some(messages),
2454
3192
  tools,
2455
3193
  temperature: None,
3194
+ ..Default::default()
2456
3195
  }
2457
3196
  }
2458
3197
 
@@ -2505,6 +3244,32 @@ mod tests {
2505
3244
  assert_eq!(payload["system"][0]["cache_control"]["type"], "ephemeral");
2506
3245
  }
2507
3246
 
3247
+ #[test]
3248
+ fn review_anthropic_tool_results_preserve_failure_and_success_flags() {
3249
+ for (text, is_error) in [
3250
+ ("User denied MCP tool call", true),
3251
+ ("MCP call was not approved; user feedback", true),
3252
+ ("Error: literal diagnostic label", false),
3253
+ ] {
3254
+ let request = native_request(
3255
+ vec![ChatMessage {
3256
+ role: ChatRole::Tool,
3257
+ content: vec![ContentBlock::ToolResult {
3258
+ tool_use_id: "mcp-call".into(),
3259
+ content: text.into(),
3260
+ is_error,
3261
+ }],
3262
+ }],
3263
+ None,
3264
+ );
3265
+ let payload =
3266
+ anthropic_chat_payload(&MintConfig::default(), "claude-x", &request, false)
3267
+ .unwrap();
3268
+ assert_eq!(payload["messages"][0]["content"][0]["is_error"], is_error);
3269
+ assert_eq!(payload["messages"][0]["content"][0]["content"], text);
3270
+ }
3271
+ }
3272
+
2508
3273
  #[test]
2509
3274
  fn anthropic_payload_omits_the_cache_breakpoint_for_an_empty_system_prompt() {
2510
3275
  let mut request = native_request(vec![ChatMessage::text(ChatRole::User, "hi")], None);
@@ -2673,6 +3438,7 @@ mod tests {
2673
3438
  messages: None,
2674
3439
  tools: None,
2675
3440
  temperature: None,
3441
+ ..Default::default()
2676
3442
  };
2677
3443
  assert!(require_supported_attachments("openai", &request).is_ok());
2678
3444
  }
@@ -2698,6 +3464,7 @@ mod tests {
2698
3464
  messages: None,
2699
3465
  tools: None,
2700
3466
  temperature: None,
3467
+ ..Default::default()
2701
3468
  };
2702
3469
  for provider in [
2703
3470
  "local_openai",
@@ -3093,6 +3860,32 @@ mod tests {
3093
3860
  (Some(80), Some(40))
3094
3861
  );
3095
3862
  }
3863
+ #[test]
3864
+ fn anthropic_cached_prompt_tokens_count_toward_context_in_json_and_streams() {
3865
+ let usage = json!({"input_tokens":100,"cache_read_input_tokens":98_000,"cache_creation_input_tokens":1_000,"output_tokens":400,"cache_creation":{"ephemeral_5m_input_tokens":1_000}});
3866
+ let reply = parse_anthropic_reply(
3867
+ "claude-test".into(),
3868
+ &json!({"content":[{"type":"text","text":"Ready"}],"usage":usage}),
3869
+ )
3870
+ .unwrap();
3871
+ assert_eq!(reply.input_tokens, Some(99_100));
3872
+ let mut stream = StreamReplyAccumulator::default();
3873
+ stream.consume_anthropic(
3874
+ &json!({"type":"message_start","message":{"usage":usage}}),
3875
+ &mut |_| {},
3876
+ );
3877
+ stream.consume_anthropic(
3878
+ &json!({"type":"content_block_delta","delta":{"type":"text_delta","text":"Ready"}}),
3879
+ &mut |_| {},
3880
+ );
3881
+ stream.consume_anthropic(
3882
+ &json!({"type":"message_delta","usage":{"output_tokens":400}}),
3883
+ &mut |_| {},
3884
+ );
3885
+ let reply = stream.finish("claude-test".into()).unwrap();
3886
+ assert_eq!(reply.input_tokens, Some(99_100));
3887
+ assert_eq!(reply.output_tokens, Some(400));
3888
+ }
3096
3889
 
3097
3890
  #[test]
3098
3891
  fn missing_usage_data_yields_no_token_counts() {
@@ -3231,6 +4024,7 @@ mod tests {
3231
4024
  let config = MintConfig {
3232
4025
  ai_provider: "anthropic".into(),
3233
4026
  anthropic_model: "claude-sonnet-5".into(),
4027
+ thinking_enabled: false,
3234
4028
  ..MintConfig::default()
3235
4029
  };
3236
4030
  let payload = anthropic_chat_payload(&config, "claude-sonnet-5", &request, false).unwrap();