@pheem49/mint 1.14.0 → 1.15.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 (585) hide show
  1. package/CLAUDE.md +1 -1
  2. package/Cargo.lock +291 -11
  3. package/Cargo.toml +1 -1
  4. package/README.md +71 -9
  5. package/ROADMAP.md +30 -0
  6. package/Release_Note.md +579 -465
  7. package/crates/mint-cli/Cargo.toml +3 -1
  8. package/crates/mint-cli/src/agent/approval_prompts.rs +16 -3
  9. package/crates/mint-cli/src/agent/live_status.rs +367 -118
  10. package/crates/mint-cli/src/agent/markdown_render.rs +601 -61
  11. package/crates/mint-cli/src/agent.rs +452 -215
  12. package/crates/mint-cli/src/commands/agent.rs +16 -21
  13. package/crates/mint-cli/src/commands/eval.rs +9 -5
  14. package/crates/mint-cli/src/commands/git.rs +204 -0
  15. package/crates/mint-cli/src/commands/integrations.rs +28 -0
  16. package/crates/mint-cli/src/commands/knowledge.rs +1 -0
  17. package/crates/mint-cli/src/commands/mod.rs +37 -2
  18. package/crates/mint-cli/src/commands/tailnet_web.rs +235 -0
  19. package/crates/mint-cli/src/image.rs +26 -3
  20. package/crates/mint-cli/src/interactive/chat_tui.rs +4742 -0
  21. package/crates/mint-cli/src/interactive/command_ui.rs +477 -0
  22. package/crates/mint-cli/src/interactive/format.rs +8 -0
  23. package/crates/mint-cli/src/interactive/input_box.rs +38 -2
  24. package/crates/mint-cli/src/interactive/picker.rs +180 -37
  25. package/crates/mint-cli/src/interactive/resume_picker.rs +686 -0
  26. package/crates/mint-cli/src/interactive/slash_commands.rs +868 -450
  27. package/crates/mint-cli/src/interactive/thought_viewer.rs +286 -0
  28. package/crates/mint-cli/src/interactive.rs +400 -136
  29. package/crates/mint-cli/src/main.rs +84 -23
  30. package/crates/mint-cli/src/markdown.rs +12 -5
  31. package/crates/mint-cli/src/onboard.rs +32 -30
  32. package/crates/mint-cli/src/plugins_cli.rs +8 -5
  33. package/crates/mint-cli/src/setup.rs +10 -8
  34. package/crates/mint-cli/src/terminal_theme.rs +187 -0
  35. package/crates/mint-cli/src/updater.rs +56 -21
  36. package/crates/mint-core/src/agent/agent_loop.rs +1 -0
  37. package/crates/mint-core/src/agent/chat.rs +843 -177
  38. package/crates/mint-core/src/agent/code_tools.rs +83 -2
  39. package/crates/mint-core/src/agent/memory.rs +658 -20
  40. package/crates/mint-core/src/agent/skills.rs +1 -1
  41. package/crates/mint-core/src/api_server/routes/chat.rs +31 -5
  42. package/crates/mint-core/src/api_server/routes/linked_folders.rs +118 -3
  43. package/crates/mint-core/src/api_server/routes/misc.rs +10 -14
  44. package/crates/mint-core/src/api_server/routes/sessions.rs +44 -0
  45. package/crates/mint-core/src/api_server.rs +22 -3
  46. package/crates/mint-core/src/avatar_bridge.rs +2 -0
  47. package/crates/mint-core/src/browser/cdp.rs +74 -83
  48. package/crates/mint-core/src/browser/evidence.rs +260 -0
  49. package/crates/mint-core/src/browser/input.rs +1 -1
  50. package/crates/mint-core/src/browser/interact.rs +98 -124
  51. package/crates/mint-core/src/browser/lifecycle.rs +75 -4
  52. package/crates/mint-core/src/browser/mod.rs +16 -2
  53. package/crates/mint-core/src/browser/navigate.rs +27 -19
  54. package/crates/mint-core/src/browser/observe.js +40 -0
  55. package/crates/mint-core/src/browser/session.rs +1558 -0
  56. package/crates/mint-core/src/git/branches.rs +348 -0
  57. package/crates/mint-core/src/git/checkpoint.rs +40 -0
  58. package/crates/mint-core/src/git/diff.rs +206 -0
  59. package/crates/mint-core/src/git/mod.rs +10 -2
  60. package/crates/mint-core/src/integrations/channels.rs +1 -0
  61. package/crates/mint-core/src/lib.rs +22 -14
  62. package/crates/mint-core/src/live_sync.rs +28 -9
  63. package/crates/mint-core/src/media/auto_shorts.rs +1 -0
  64. package/crates/mint-core/src/media/gemini_live.rs +12 -1
  65. package/crates/mint-core/src/media/mic_transcribe.rs +1 -0
  66. package/crates/mint-core/src/media/subtitle.rs +1 -0
  67. package/crates/mint-core/src/orchestration/context_render.rs +1 -0
  68. package/crates/mint-core/src/orchestration/memory_skill.rs +6 -1
  69. package/crates/mint-core/src/orchestration/mod.rs +1024 -271
  70. package/crates/mint-core/src/orchestration/tools/browser.rs +16 -139
  71. package/crates/mint-core/src/orchestration/tools/misc.rs +2 -1
  72. package/crates/mint-core/src/orchestration/tools/web.rs +1 -1
  73. package/crates/mint-core/src/prompts/agent.rs +58 -29
  74. package/crates/mint-core/src/prompts/chat.rs +1 -1
  75. package/crates/mint-core/src/prompts/persona.rs +1 -1
  76. package/crates/mint-core/src/prompts/tool_catalog.rs +93 -25
  77. package/crates/mint-core/src/search/linked_folders/index.rs +732 -0
  78. package/crates/mint-core/src/search/linked_folders.rs +391 -130
  79. package/crates/mint-core/src/slash/mod.rs +470 -22
  80. package/crates/mint-core/src/slash/model_fetcher.rs +12 -3
  81. package/crates/mint-core/src/slash/models.rs +66 -10
  82. package/crates/mint-core/src/system/config.rs +230 -0
  83. package/crates/mint-core/src/system/workspace.rs +228 -0
  84. package/crates/mint-core/src/system.rs +1 -0
  85. package/crates/mint-core/tests/memory_persistence.rs +39 -0
  86. package/mint-presentation-demo.mp4 +0 -0
  87. package/out/web/assets/CliSessionsView-BxUVWEEL.js +1 -0
  88. package/out/web/assets/CliSessionsView-CBDTXuQv.css +1 -0
  89. package/out/web/assets/CodeReviewPage-DkMgfcos.css +1 -0
  90. package/out/web/assets/CodeReviewPage-jKlmLJwT.js +3 -0
  91. package/out/web/assets/CommandPalette-DnwPA5hy.js +1 -0
  92. package/out/web/assets/ImageStudioPanel-BR1XFwhR.js +1 -0
  93. package/out/web/assets/ImageStudioPanel-D0lnhQV5.css +1 -0
  94. package/out/web/assets/LinkedFoldersView-bi-CWQ-I.js +1 -0
  95. package/out/web/assets/McpRegistryPicker-D5tEFnyq.js +1 -0
  96. package/out/web/assets/McpServersView-BGG1dhFM.js +1 -0
  97. package/out/web/assets/MintDashboard-BIglDEWX.js +20 -0
  98. package/out/web/assets/MintDashboard-DSMywSgs.css +1 -0
  99. package/out/web/assets/ModelPanel-CbJ1PFoZ.js +1 -0
  100. package/out/web/assets/PicturesLibrary-CQP0fLP1.css +1 -0
  101. package/out/web/assets/PicturesLibrary-DoYN7T1p.js +1 -0
  102. package/out/web/assets/PluginsView-CU1HWXGk.js +1 -0
  103. package/out/web/assets/ScheduledTasksView-Bnl36y2N.js +1 -0
  104. package/out/web/assets/SettingsWindow-DhWLAn9T.css +1 -0
  105. package/out/web/assets/SettingsWindow-Dw8L01pw.js +2 -0
  106. package/out/web/assets/SkillsView-cnl8aY5p.js +2 -0
  107. package/out/web/assets/ToolSurfacePage-CgsQw6l5.css +1 -0
  108. package/out/web/assets/ToolSurfacePage-Cz2VJAPZ.js +37 -0
  109. package/out/web/assets/VeoStudioPanel-D4JnERWR.js +4 -0
  110. package/out/web/assets/VeoStudioPanel-DOj7_WR3.css +1 -0
  111. package/out/web/assets/WorkspacePanel-CbJ1PFoZ.js +1 -0
  112. package/out/web/assets/{abnfDiagram-VCTEODGH-DAOBA7ZG.js → abnfDiagram-VCTEODGH-ByMyfanq.js} +1 -1
  113. package/out/web/assets/{architecture-7GRP2DOG-CgSwQOPQ.js → architecture-7GRP2DOG-CLq2kEOB.js} +1 -1
  114. package/out/web/assets/{architectureDiagram-5GKGNRK7-CYp3yupP.js → architectureDiagram-5GKGNRK7-6PoVSmNG.js} +1 -1
  115. package/out/web/assets/{blockDiagram-NRAW4CY4-CyWybDWr.js → blockDiagram-NRAW4CY4-cKw9mAKi.js} +1 -1
  116. package/out/web/assets/{c4Diagram-UCG6FXSJ-BvY3U709.js → c4Diagram-UCG6FXSJ-Cqy8N5Ge.js} +1 -1
  117. package/out/web/assets/{chunk-75Z2AOVW-TLXCoZYv.js → chunk-75Z2AOVW-3qybijF5.js} +1 -1
  118. package/out/web/assets/chunk-CKBBP62Z-cIrXq6jN.js +2 -0
  119. package/out/web/assets/{chunk-CLGD4ZFX-xjsKtNS7.js → chunk-CLGD4ZFX-CndDQkww.js} +1 -1
  120. package/out/web/assets/chunk-DSNCTWBM-Cc4NxvgD.js +2 -0
  121. package/out/web/assets/chunk-DU6HZSFF-drzh6GsU.js +128 -0
  122. package/out/web/assets/{chunk-F27PBJKO-BFpgF55A.js → chunk-F27PBJKO-FiY35l4v.js} +1 -1
  123. package/out/web/assets/{chunk-G27WJ6UU-DfF7qW-C.js → chunk-G27WJ6UU-Bjt9Il4A.js} +1 -1
  124. package/out/web/assets/{chunk-GMAD6QVW-C-558kh_.js → chunk-GMAD6QVW-Bppi3Ctd.js} +1 -1
  125. package/out/web/assets/{chunk-JQ64N6SF-wJmeGKAw.js → chunk-JQ64N6SF-DGg5kcEV.js} +1 -1
  126. package/out/web/assets/{chunk-LCL6LL3I-C1wHD8Ie.js → chunk-LCL6LL3I-CCYcjd7p.js} +1 -1
  127. package/out/web/assets/{chunk-OBVCFTLP-BsRTPBqs.js → chunk-OBVCFTLP-DJeNil5M.js} +1 -1
  128. package/out/web/assets/{chunk-P2QGCYS3-HZIiM7Ni.js → chunk-P2QGCYS3-lRzkRjBB.js} +1 -1
  129. package/out/web/assets/{chunk-POPQ4Y6H-Dk9wvVb0.js → chunk-POPQ4Y6H-DouASMBW.js} +1 -1
  130. package/out/web/assets/{chunk-PWAF6VOD-BmjYLNbq.js → chunk-PWAF6VOD-DMmD1vVf.js} +1 -1
  131. package/out/web/assets/{chunk-R7TYR2AO-CaHIf-lK.js → chunk-R7TYR2AO-CYLA5UC5.js} +1 -1
  132. package/out/web/assets/{chunk-RHFEMEQ7-_T561hOU.js → chunk-RHFEMEQ7-BiXwEcUT.js} +1 -1
  133. package/out/web/assets/{chunk-SVP7TREG-CM-GGYoY.js → chunk-SVP7TREG-BV-zPslF.js} +1 -1
  134. package/out/web/assets/{chunk-TEH6E4GO-K2A8Vg2C.js → chunk-TEH6E4GO-B2RGEohZ.js} +1 -1
  135. package/out/web/assets/classDiagram-DTDB5LWJ-Dq0smRWk.js +1 -0
  136. package/out/web/assets/classDiagram-v2-JRS7N3AN-Dq0smRWk.js +1 -0
  137. package/out/web/assets/config-C-ZFtipU.css +1 -0
  138. package/out/web/assets/config-eEDngJXO.js +1 -0
  139. package/out/web/assets/core-hQ2VA9Ti.js +1 -0
  140. package/out/web/assets/{cynefin-OW5HDTMX-ClzaB6cc.js → cynefin-OW5HDTMX-Cas4csHM.js} +1 -1
  141. package/out/web/assets/{cynefinDiagram-5FMLGOSQ-D_JzZBzf.js → cynefinDiagram-5FMLGOSQ-Bbpj66Y3.js} +1 -1
  142. package/out/web/assets/{dagre-3AP2YEHR-BDuUeMHj.js → dagre-3AP2YEHR-B0-2RDEd.js} +1 -1
  143. package/out/web/assets/{diagram-S7CK7UJ4-DgtcfE8P.js → diagram-S7CK7UJ4-BdzEjUyH.js} +1 -1
  144. package/out/web/assets/{diagram-UQ7AKVKN-DTLoEYti.js → diagram-UQ7AKVKN-CJDwyPxF.js} +1 -1
  145. package/out/web/assets/{diagram-VSXAHHWV-ChGl9pXz.js → diagram-VSXAHHWV-BxaG1r_B.js} +1 -1
  146. package/out/web/assets/{diagram-VX7I27RA-Be8DWz0S.js → diagram-VX7I27RA-BMmUqm8f.js} +1 -1
  147. package/out/web/assets/{diagram-Z3DM3KII-BMXjRTOF.js → diagram-Z3DM3KII-CFtd03eE.js} +1 -1
  148. package/out/web/assets/dist-js-Bf2bhoku.js +1 -0
  149. package/out/web/assets/{ebnfDiagram-PWID7BFC-8Q9pl1CJ.js → ebnfDiagram-PWID7BFC-DwAen1hO.js} +1 -1
  150. package/out/web/assets/{erDiagram-SSCWMZ5O-DjH8Lnkn.js → erDiagram-SSCWMZ5O-BCZSHACj.js} +1 -1
  151. package/out/web/assets/event-ChBVpRy0.js +1 -0
  152. package/out/web/assets/{eventmodeling-NTZA5JFV-BKUH_DOp.js → eventmodeling-NTZA5JFV-XHStE4E_.js} +1 -1
  153. package/out/web/assets/fira-code-latin-600-normal-BmSSHshu.woff +0 -0
  154. package/out/web/assets/fira-code-latin-600-normal-CvYZpPHg.woff2 +0 -0
  155. package/out/web/assets/fira-code-symbols2-600-normal-Dvq6IQE5.woff +0 -0
  156. package/out/web/assets/flowDiagram-A5DVABFB-BLg7nEcd.js +1 -0
  157. package/out/web/assets/{ganttDiagram-EL5Y4UJY-D7fZKJNM.js → ganttDiagram-EL5Y4UJY-BAEu1Cql.js} +1 -1
  158. package/out/web/assets/{gitGraph-4MIJSDKK-BypWaaBU.js → gitGraph-4MIJSDKK-r32Vyper.js} +1 -1
  159. package/out/web/assets/{gitGraphDiagram-WWUBYQGX-DWiw_3bq.js → gitGraphDiagram-WWUBYQGX-z9o9qt6V.js} +1 -1
  160. package/out/web/assets/index-Btsf4L8M.css +1 -0
  161. package/out/web/assets/index-D8D9MeRA.js +11 -0
  162. package/out/web/assets/{info-A6RAGUB7-KkAUxT4j.js → info-A6RAGUB7-BvLaC2cw.js} +1 -1
  163. package/out/web/assets/{infoDiagram-RXCK75RN-BNb6E48y.js → infoDiagram-RXCK75RN-Be6WAXXw.js} +1 -1
  164. package/out/web/assets/{ishikawaDiagram-5VMMS53U-Caa-JoAK.js → ishikawaDiagram-5VMMS53U-DZjoHJHC.js} +1 -1
  165. package/out/web/assets/{journeyDiagram-EYS64GPL-DOrmFGz6.js → journeyDiagram-EYS64GPL-CtVkjkGm.js} +1 -1
  166. package/out/web/assets/{kanban-definition-3QL26DDD-BOWCYcK0.js → kanban-definition-3QL26DDD-CW5FMv1w.js} +1 -1
  167. package/out/web/assets/lucide-react-Ip3L6Jtn.js +1 -0
  168. package/out/web/assets/markdown-BSeeUbJH.js +38 -0
  169. package/out/web/assets/{mermaid-parser.core-BQqjCkS_.js → mermaid-parser.core-aUgCqsZ5.js} +3 -3
  170. package/out/web/assets/mermaid.core-D57sEiSL.js +11 -0
  171. package/out/web/assets/{mindmap-definition-FBJOCRG2-jTceWpr0.js → mindmap-definition-FBJOCRG2-DAmSpM3a.js} +1 -1
  172. package/out/web/assets/modelManager-tMVmJVAW.js +2 -0
  173. package/out/web/assets/oauthManager-9jxicDWJ.js +1 -0
  174. package/out/web/assets/{packet-AYTQ26CC-CJ5RXgol.js → packet-AYTQ26CC-DJ27iIoJ.js} +1 -1
  175. package/out/web/assets/{pegDiagram-XKGWAZYB-y8wMtd0y.js → pegDiagram-XKGWAZYB-Bj3ii-gS.js} +1 -1
  176. package/out/web/assets/{pie-WAS4IAKB-72fTBbl4.js → pie-WAS4IAKB-DG5mCUbc.js} +1 -1
  177. package/out/web/assets/{pieDiagram-E7YTZNPT-e0O02qWq.js → pieDiagram-E7YTZNPT-BrJLPnDA.js} +1 -1
  178. package/out/web/assets/prompt-latin-700-normal-BfbftDSq.woff +0 -0
  179. package/out/web/assets/prompt-latin-700-normal-I2gc831J.woff2 +0 -0
  180. package/out/web/assets/prompt-thai-700-normal-Cb5ilR-c.woff +0 -0
  181. package/out/web/assets/prompt-thai-700-normal-Cg4aQ0Nn.woff2 +0 -0
  182. package/out/web/assets/{quadrantDiagram-AXDQQJYC-C9phYLom.js → quadrantDiagram-AXDQQJYC-3x_TmF3j.js} +1 -1
  183. package/out/web/assets/{radar-RG4KPBEZ-BAU2hzUT.js → radar-RG4KPBEZ-CrXaTTuV.js} +1 -1
  184. package/out/web/assets/{railroad-74A4TZTK-B0bCAVHC.js → railroad-74A4TZTK-BpqMH9BE.js} +1 -1
  185. package/out/web/assets/{railroad-abnf-HS5TGJTU-WhxO7ndE.js → railroad-abnf-HS5TGJTU-DHI0uLlV.js} +1 -1
  186. package/out/web/assets/{railroad-ebnf-LZEXJU2U-BkA0m7-F.js → railroad-ebnf-LZEXJU2U-CQCb316y.js} +1 -1
  187. package/out/web/assets/{railroad-peg-WCYAUIDC-BsVpHIaj.js → railroad-peg-WCYAUIDC-Ph1giKkH.js} +1 -1
  188. package/out/web/assets/{railroadDiagram-O6MQD6OU-CyOLN65Q.js → railroadDiagram-O6MQD6OU-BRq6wIxx.js} +1 -1
  189. package/out/web/assets/{requirementDiagram-EFPCY7ZU-DQXJ7Dud.js → requirementDiagram-EFPCY7ZU-DIJj6G1g.js} +1 -1
  190. package/out/web/assets/{sankeyDiagram-P5KCCOFB-B2XLyIIc.js → sankeyDiagram-P5KCCOFB-DZajSryq.js} +1 -1
  191. package/out/web/assets/{sequenceDiagram-WJ2MYXX4-DGQY4wwm.js → sequenceDiagram-WJ2MYXX4-DxJCWyZS.js} +1 -1
  192. package/out/web/assets/{stateDiagram-HBIQ2CUA-DHZpDwNy.js → stateDiagram-HBIQ2CUA-k6voi8Vf.js} +1 -1
  193. package/out/web/assets/stateDiagram-v2-4QOOHH4V-DeEc0lGZ.js +1 -0
  194. package/out/web/assets/studio-B1mk40wD.js +1 -0
  195. package/out/web/assets/{swimlanes-XN3QIQJK-Bn_jcZz-.js → swimlanes-XN3QIQJK-igYhTjXB.js} +1 -1
  196. package/out/web/assets/swimlanesDiagram-VK2B7HYN-DpVik9v7.js +8 -0
  197. package/out/web/assets/{timeline-definition-24CTP7MA-CdTERuJZ.js → timeline-definition-24CTP7MA-Dke2p93p.js} +1 -1
  198. package/out/web/assets/{treeView-Q6P3EWNA-TKBKQG6R.js → treeView-Q6P3EWNA-hNELnXUs.js} +1 -1
  199. package/out/web/assets/{treemap-WGGIJYW6-BP7gQoiC.js → treemap-WGGIJYW6-D2B6wDsx.js} +1 -1
  200. package/out/web/assets/{vennDiagram-4TSXK5OY-BHUmxDuK.js → vennDiagram-4TSXK5OY-CDkW7rUP.js} +1 -1
  201. package/out/web/assets/{wardley-WFR3VGLG-9bct5AAN.js → wardley-WFR3VGLG-BECdXETi.js} +1 -1
  202. package/out/web/assets/{wardleyDiagram-VM6X3IG4-BfJwDDso.js → wardleyDiagram-VM6X3IG4-B5BycpQ4.js} +1 -1
  203. package/out/web/assets/window-5qYXKoW7.js +1 -0
  204. package/out/web/assets/{xychartDiagram-S5SC5T6Z-Dqd9Mvpo.js → xychartDiagram-S5SC5T6Z-CtJS3b--.js} +1 -1
  205. package/out/web/index-web.html +3 -4
  206. package/out/web/index.html +3 -4
  207. package/package.json +17 -6
  208. package/skills-lock.json +36 -0
  209. package/slash-commands.json +9 -0
  210. package/src/renderer/index-web.html +1 -0
  211. package/src/renderer/index.html +1 -0
  212. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  213. package/src/renderer/shared/agentProgress.ts +168 -3
  214. package/src/renderer/shared/components/AgentActivityDrawer.tsx +6 -6
  215. package/src/renderer/shared/components/AgentActivityTable.tsx +192 -132
  216. package/src/renderer/shared/components/ArtifactPreviewPanel.tsx +4 -4
  217. package/src/renderer/shared/components/AuthGate.tsx +4 -9
  218. package/src/renderer/shared/components/CalculationCard.tsx +41 -15
  219. package/src/renderer/shared/components/ChatMessageItem.tsx +20 -21
  220. package/src/renderer/shared/components/ChatPanel.tsx +432 -193
  221. package/src/renderer/shared/components/ChunkErrorBoundary.tsx +198 -49
  222. package/src/renderer/shared/components/CliSessionsView.tsx +495 -0
  223. package/src/renderer/shared/components/CodeReviewPage.tsx +565 -0
  224. package/src/renderer/shared/components/CommandPalette.tsx +8 -8
  225. package/src/renderer/shared/components/DashboardSidebar.tsx +862 -125
  226. package/src/renderer/shared/components/DesktopTitlebar.tsx +345 -0
  227. package/src/renderer/shared/components/DiffReviewModal.tsx +3 -3
  228. package/src/renderer/shared/components/GitBranchSelector.tsx +442 -0
  229. package/src/renderer/shared/components/ImageGenCard.tsx +31 -12
  230. package/src/renderer/shared/components/ImageSearchCard.tsx +59 -38
  231. package/src/renderer/shared/components/ImageStudioPanel.tsx +40 -41
  232. package/src/renderer/shared/components/InlineThoughtNote.tsx +27 -0
  233. package/src/renderer/shared/components/LinkedFoldersView.tsx +365 -46
  234. package/src/renderer/shared/components/McpRegistryPicker.tsx +1 -1
  235. package/src/renderer/shared/components/McpServersView.tsx +24 -22
  236. package/src/renderer/shared/components/MermaidCard.tsx +14 -8
  237. package/src/renderer/shared/components/MintDashboard.tsx +1141 -508
  238. package/src/renderer/shared/components/ModelSelectorPopover.tsx +535 -214
  239. package/src/renderer/shared/components/PlanChecklistWidget.tsx +1 -1
  240. package/src/renderer/shared/components/PluginsView.tsx +1 -1
  241. package/src/renderer/shared/components/RewindModal.tsx +6 -6
  242. package/src/renderer/shared/components/RunSummaryDashboard.tsx +3 -3
  243. package/src/renderer/shared/components/ScheduledTasksView.tsx +4 -4
  244. package/src/renderer/shared/components/ScreenCaptureDialog.css +221 -0
  245. package/src/renderer/shared/components/ScreenCaptureDialog.tsx +199 -0
  246. package/src/renderer/shared/components/Settings/AgentsTab.tsx +34 -39
  247. package/src/renderer/shared/components/Settings/AudioTab.tsx +8 -8
  248. package/src/renderer/shared/components/Settings/AutomationTab.tsx +8 -58
  249. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +368 -0
  250. package/src/renderer/shared/components/Settings/GeneralTab.tsx +335 -81
  251. package/src/renderer/shared/components/Settings/MemoryTab.tsx +3 -3
  252. package/src/renderer/shared/components/Settings/PluginsTab.tsx +20 -12
  253. package/src/renderer/shared/components/Settings/ProfileTab.tsx +29 -5
  254. package/src/renderer/shared/components/Settings/SearchableModelCombobox.tsx +411 -0
  255. package/src/renderer/shared/components/Settings/ThemeTab.tsx +401 -158
  256. package/src/renderer/shared/components/SkillsView.tsx +41 -16
  257. package/src/renderer/shared/components/SlashSuggestions.tsx +3 -2
  258. package/src/renderer/shared/components/SourcesBlock.tsx +44 -32
  259. package/src/renderer/shared/components/StockCard.tsx +58 -24
  260. package/src/renderer/shared/components/SubagentDagView.tsx +4 -4
  261. package/src/renderer/shared/components/TerminalDock.tsx +241 -0
  262. package/src/renderer/shared/components/ThinkingBlock.tsx +14 -8
  263. package/src/renderer/shared/components/ToolSurfacePage.tsx +520 -0
  264. package/src/renderer/shared/components/UiFeatureCard.tsx +267 -0
  265. package/src/renderer/shared/components/UiGridCard.tsx +188 -0
  266. package/src/renderer/shared/components/UiMockupWidget.tsx +149 -0
  267. package/src/renderer/shared/components/VeoStudioPanel.tsx +13 -29
  268. package/src/renderer/shared/components/WeatherCard.tsx +3 -3
  269. package/src/renderer/shared/components/WorkspaceContextPopover.tsx +133 -0
  270. package/src/renderer/shared/components/WorkspaceSelector.tsx +149 -0
  271. package/src/renderer/shared/constants/config.ts +28 -3
  272. package/src/renderer/shared/constants/models.ts +189 -2
  273. package/src/renderer/shared/conversation/coordinator.test.ts +27 -0
  274. package/src/renderer/shared/conversation/coordinator.ts +82 -0
  275. package/src/renderer/shared/conversation/syncView.test.ts +46 -0
  276. package/src/renderer/shared/conversation/syncView.ts +43 -0
  277. package/src/renderer/shared/conversation/useConversationCoordinator.ts +106 -0
  278. package/src/renderer/shared/css/agent-observability.css +35 -3
  279. package/src/renderer/shared/css/auth-gate.css +2 -2
  280. package/src/renderer/shared/css/chat-ui-cards.css +495 -0
  281. package/src/renderer/shared/css/code-review.css +823 -0
  282. package/src/renderer/shared/css/code-sessions.css +452 -0
  283. package/src/renderer/shared/css/file-changes.css +132 -0
  284. package/src/renderer/shared/css/image-studio.css +16 -16
  285. package/src/renderer/shared/css/management-views.css +277 -27
  286. package/src/renderer/shared/css/model-selector.css +442 -42
  287. package/src/renderer/shared/css/settings/agents.css +1 -1
  288. package/src/renderer/shared/css/settings/automation.css +13 -13
  289. package/src/renderer/shared/css/settings/general.css +526 -5
  290. package/src/renderer/shared/css/settings/plugins.css +5 -5
  291. package/src/renderer/shared/css/settings/profile.css +331 -1
  292. package/src/renderer/shared/css/settings/theme.css +651 -3
  293. package/src/renderer/shared/css/veo-studio.css +16 -15
  294. package/src/renderer/shared/fonts.ts +19 -37
  295. package/src/renderer/shared/platform.test.ts +34 -0
  296. package/src/renderer/shared/platform.ts +173 -6
  297. package/src/renderer/shared/theme/themeManager.ts +144 -0
  298. package/src/renderer/shared/theme/themes.ts +116 -0
  299. package/src/renderer/shared/theme/tokens.css +318 -0
  300. package/src/renderer/shared/types.ts +82 -1
  301. package/src/renderer/shared/utils/agentActivity.ts +126 -5
  302. package/src/renderer/shared/utils/cardJson.test.ts +35 -0
  303. package/src/renderer/shared/utils/cardJson.ts +30 -0
  304. package/src/renderer/shared/utils/fileIcons.ts +1 -1
  305. package/src/renderer/shared/utils/markdown.tsx +329 -80
  306. package/src/renderer/shared/utils/markdown.webImages.test.tsx +40 -0
  307. package/src/renderer/shared/utils/providers.ts +8 -3
  308. package/src/renderer/shared/utils/screenCapture.ts +43 -0
  309. package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
  310. package/src/renderer/shared/utils/ui.ts +4 -64
  311. package/src/renderer/src/App.tsx +7 -0
  312. package/src/renderer/src/components/BrowserShell.tsx +108 -0
  313. package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +91 -0
  314. package/src/renderer/src/components/Live2DStage.tsx +32 -9
  315. package/src/renderer/src/components/MintDashboard.tsx +4 -2
  316. package/src/renderer/src/components/ModelPanel.tsx +2 -1
  317. package/src/renderer/src/components/PicturesLibrary.tsx +3 -2
  318. package/src/renderer/src/components/ScreenPicker.css +190 -0
  319. package/src/renderer/src/components/ScreenPicker.tsx +185 -540
  320. package/src/renderer/src/components/SettingsWindow.tsx +40 -2
  321. package/src/renderer/src/components/SpotlightWindow.tsx +1 -0
  322. package/src/renderer/src/components/WidgetWindow.tsx +27 -3
  323. package/src/renderer/src/components/WorkspacePanel.tsx +61 -43
  324. package/src/renderer/src/components/browser-shell.css +185 -0
  325. package/src/renderer/src/components/browser-suggestions-popup.css +55 -0
  326. package/src/renderer/src/components/liveTranslateFrames.test.ts +23 -0
  327. package/src/renderer/src/components/liveTranslateFrames.ts +34 -0
  328. package/src/renderer/src/css/app-shell.css +394 -113
  329. package/src/renderer/src/css/chat.css +1926 -380
  330. package/src/renderer/src/css/dashboard.css +11 -0
  331. package/src/renderer/src/css/desktop-tools.css +730 -0
  332. package/src/renderer/src/css/misc.css +19 -22
  333. package/src/renderer/src/css/model.css +49 -48
  334. package/src/renderer/src/css/pictures.css +144 -136
  335. package/src/renderer/src/css/settings/base.css +357 -123
  336. package/src/renderer/src/css/sidebar-workspace.css +930 -291
  337. package/src/renderer/src/css/spotlight.css +15 -26
  338. package/src/renderer/src/css/tool-surfaces.css +728 -0
  339. package/src/renderer/src/css/widget.css +35 -7
  340. package/src/renderer/src/env.d.ts +3 -0
  341. package/src/renderer/src/hooks/useProviderModels.ts +2 -9
  342. package/src/renderer/src/index.css +53 -46
  343. package/src/renderer/src/main.tsx +11 -0
  344. package/src/renderer/src/tauri.ts +201 -30
  345. package/src/renderer/src-web/App.tsx +40 -0
  346. package/src/renderer/src-web/components/MintDashboard.tsx +4 -2
  347. package/src/renderer/src-web/components/PicturesLibrary.tsx +3 -2
  348. package/src/renderer/src-web/components/PullToRefresh.tsx +107 -0
  349. package/src/renderer/src-web/components/SettingsWindow.tsx +28 -2
  350. package/src/renderer/src-web/css/app-shell.css +174 -96
  351. package/src/renderer/src-web/css/chat.css +1519 -316
  352. package/src/renderer/src-web/css/dashboard.css +11 -0
  353. package/src/renderer/src-web/css/misc.css +17 -17
  354. package/src/renderer/src-web/css/mobile.css +678 -0
  355. package/src/renderer/src-web/css/model.css +46 -46
  356. package/src/renderer/src-web/css/pictures.css +81 -34
  357. package/src/renderer/src-web/css/pull-to-refresh.css +54 -0
  358. package/src/renderer/src-web/css/settings/base.css +316 -100
  359. package/src/renderer/src-web/css/settings/mobile.css +320 -0
  360. package/src/renderer/src-web/css/sidebar-workspace.css +545 -100
  361. package/src/renderer/src-web/css/spotlight.css +9 -26
  362. package/src/renderer/src-web/css/widget.css +0 -5
  363. package/src/renderer/src-web/env.d.ts +3 -0
  364. package/src/renderer/src-web/hooks/useProviderModels.ts +2 -9
  365. package/src/renderer/src-web/index.css +53 -96
  366. package/src/renderer/src-web/main.tsx +11 -0
  367. package/src/renderer/src-web/tauri.ts +200 -30
  368. package/vite.config.web.ts +1 -0
  369. package/out/web/assets/MintDashboard-DWSqucgb.js +0 -57
  370. package/out/web/assets/SettingsWindow-DFM2aj5B.js +0 -2
  371. package/out/web/assets/chunk-CKBBP62Z-nr1uKPj9.js +0 -2
  372. package/out/web/assets/chunk-DSNCTWBM-Dtj8Rqsd.js +0 -2
  373. package/out/web/assets/chunk-DU6HZSFF-2tv5K1wT.js +0 -128
  374. package/out/web/assets/classDiagram-DTDB5LWJ-BdSFArgw.js +0 -1
  375. package/out/web/assets/classDiagram-v2-JRS7N3AN-BdSFArgw.js +0 -1
  376. package/out/web/assets/core-DImejIUG.js +0 -1
  377. package/out/web/assets/dist-js-BxuKEVWM.js +0 -1
  378. package/out/web/assets/event-BNl8Zahw.js +0 -1
  379. package/out/web/assets/fira-code-cyrillic-400-normal-UC0NFL4U.woff2 +0 -0
  380. package/out/web/assets/fira-code-cyrillic-400-normal-dc1q3VD7.woff +0 -0
  381. package/out/web/assets/fira-code-cyrillic-500-normal-Bu7VCL72.woff +0 -0
  382. package/out/web/assets/fira-code-cyrillic-500-normal-CX35h3Mg.woff2 +0 -0
  383. package/out/web/assets/fira-code-cyrillic-ext-400-normal-Buh61xzB.woff +0 -0
  384. package/out/web/assets/fira-code-cyrillic-ext-400-normal-txZ9Fk_1.woff2 +0 -0
  385. package/out/web/assets/fira-code-cyrillic-ext-500-normal-CYRspHgm.woff2 +0 -0
  386. package/out/web/assets/fira-code-cyrillic-ext-500-normal-CwtLmSFD.woff +0 -0
  387. package/out/web/assets/fira-code-greek-400-normal-B2Gh_Y8s.woff2 +0 -0
  388. package/out/web/assets/fira-code-greek-400-normal-DobhaxvF.woff +0 -0
  389. package/out/web/assets/fira-code-greek-500-normal-CVO0IRNt.woff2 +0 -0
  390. package/out/web/assets/fira-code-greek-500-normal-DEXIWiey.woff +0 -0
  391. package/out/web/assets/fira-code-greek-ext-400-normal-DR7mBgIM.woff2 +0 -0
  392. package/out/web/assets/fira-code-greek-ext-400-normal-DSPUheWg.woff +0 -0
  393. package/out/web/assets/fira-code-greek-ext-500-normal-BO3iq16n.woff2 +0 -0
  394. package/out/web/assets/fira-code-greek-ext-500-normal-CMiNLXER.woff +0 -0
  395. package/out/web/assets/fira-code-latin-ext-400-normal-DG73JjOf.woff +0 -0
  396. package/out/web/assets/fira-code-latin-ext-400-normal-KSMg0QLl.woff2 +0 -0
  397. package/out/web/assets/fira-code-latin-ext-500-normal-CaANqXjI.woff +0 -0
  398. package/out/web/assets/fira-code-latin-ext-500-normal-Crd9e_oI.woff2 +0 -0
  399. package/out/web/assets/flowDiagram-A5DVABFB-BpnHUSU7.js +0 -1
  400. package/out/web/assets/index-CVRg270z.css +0 -1
  401. package/out/web/assets/index-DxZQr9yD.js +0 -10
  402. package/out/web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  403. package/out/web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  404. package/out/web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  405. package/out/web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  406. package/out/web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  407. package/out/web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  408. package/out/web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  409. package/out/web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  410. package/out/web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  411. package/out/web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  412. package/out/web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  413. package/out/web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  414. package/out/web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  415. package/out/web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  416. package/out/web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  417. package/out/web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  418. package/out/web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  419. package/out/web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  420. package/out/web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  421. package/out/web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  422. package/out/web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  423. package/out/web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  424. package/out/web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  425. package/out/web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  426. package/out/web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  427. package/out/web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  428. package/out/web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  429. package/out/web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  430. package/out/web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  431. package/out/web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  432. package/out/web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  433. package/out/web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  434. package/out/web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  435. package/out/web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  436. package/out/web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  437. package/out/web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  438. package/out/web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  439. package/out/web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  440. package/out/web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  441. package/out/web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  442. package/out/web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  443. package/out/web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  444. package/out/web/assets/kanit-latin-400-normal-BUlsQXH3.woff2 +0 -0
  445. package/out/web/assets/kanit-latin-400-normal-HVZWrfev.woff +0 -0
  446. package/out/web/assets/kanit-latin-500-normal-CX_4XcKi.woff2 +0 -0
  447. package/out/web/assets/kanit-latin-500-normal-qBha86O4.woff +0 -0
  448. package/out/web/assets/kanit-latin-600-normal-0lS78yAd.woff2 +0 -0
  449. package/out/web/assets/kanit-latin-600-normal-DlacpeTg.woff +0 -0
  450. package/out/web/assets/kanit-latin-ext-400-normal-B2xPf58u.woff2 +0 -0
  451. package/out/web/assets/kanit-latin-ext-400-normal-CHvNKHHD.woff +0 -0
  452. package/out/web/assets/kanit-latin-ext-500-normal-BFdWrG22.woff +0 -0
  453. package/out/web/assets/kanit-latin-ext-500-normal-EHs3suD6.woff2 +0 -0
  454. package/out/web/assets/kanit-latin-ext-600-normal-BBDzoLaj.woff +0 -0
  455. package/out/web/assets/kanit-latin-ext-600-normal-DAX3wtuq.woff2 +0 -0
  456. package/out/web/assets/kanit-thai-400-normal-D2nliXKa.woff +0 -0
  457. package/out/web/assets/kanit-thai-400-normal-D80Pglro.woff2 +0 -0
  458. package/out/web/assets/kanit-thai-500-normal-BB8gri04.woff2 +0 -0
  459. package/out/web/assets/kanit-thai-500-normal-Iy_w0a80.woff +0 -0
  460. package/out/web/assets/kanit-thai-600-normal-Bj6oiaDc.woff2 +0 -0
  461. package/out/web/assets/kanit-thai-600-normal-wbTlPO7I.woff +0 -0
  462. package/out/web/assets/kanit-vietnamese-400-normal-DRI8YWyb.woff2 +0 -0
  463. package/out/web/assets/kanit-vietnamese-400-normal-dow6Bnto.woff +0 -0
  464. package/out/web/assets/kanit-vietnamese-500-normal-BzTrpCM3.woff2 +0 -0
  465. package/out/web/assets/kanit-vietnamese-500-normal-T1ndT7MF.woff +0 -0
  466. package/out/web/assets/kanit-vietnamese-600-normal-Bi7fw3I4.woff2 +0 -0
  467. package/out/web/assets/kanit-vietnamese-600-normal-DXCRAa_8.woff +0 -0
  468. package/out/web/assets/mali-latin-400-normal-ByPcZ8Gx.woff +0 -0
  469. package/out/web/assets/mali-latin-400-normal-Cuc6Ykl8.woff2 +0 -0
  470. package/out/web/assets/mali-latin-500-normal-B01M-LP6.woff2 +0 -0
  471. package/out/web/assets/mali-latin-500-normal-Thgqw_9f.woff +0 -0
  472. package/out/web/assets/mali-latin-600-normal-B9TiUNpT.woff2 +0 -0
  473. package/out/web/assets/mali-latin-600-normal-CycpQCwU.woff +0 -0
  474. package/out/web/assets/mali-latin-ext-400-normal-DUVdJQO4.woff +0 -0
  475. package/out/web/assets/mali-latin-ext-400-normal-iCG1ALuJ.woff2 +0 -0
  476. package/out/web/assets/mali-latin-ext-500-normal-BfJE4UMZ.woff2 +0 -0
  477. package/out/web/assets/mali-latin-ext-500-normal-FoLEyOKS.woff +0 -0
  478. package/out/web/assets/mali-latin-ext-600-normal-BTvIfiCp.woff2 +0 -0
  479. package/out/web/assets/mali-latin-ext-600-normal-DAQcLVmD.woff +0 -0
  480. package/out/web/assets/mali-thai-400-normal-BO7A3bLZ.woff2 +0 -0
  481. package/out/web/assets/mali-thai-400-normal-CX9VrjTn.woff +0 -0
  482. package/out/web/assets/mali-thai-500-normal-Cph2qha9.woff +0 -0
  483. package/out/web/assets/mali-thai-500-normal-D89rMT77.woff2 +0 -0
  484. package/out/web/assets/mali-thai-600-normal-DPgXT6rZ.woff +0 -0
  485. package/out/web/assets/mali-thai-600-normal-DlTx3OsB.woff2 +0 -0
  486. package/out/web/assets/mali-vietnamese-400-normal-DUKxzXaz.woff2 +0 -0
  487. package/out/web/assets/mali-vietnamese-400-normal-gFDLvsOK.woff +0 -0
  488. package/out/web/assets/mali-vietnamese-500-normal--OY07q2c.woff +0 -0
  489. package/out/web/assets/mali-vietnamese-500-normal-KDrNRwfW.woff2 +0 -0
  490. package/out/web/assets/mali-vietnamese-600-normal-CtTMUU7T.woff +0 -0
  491. package/out/web/assets/mali-vietnamese-600-normal-DFlJr66u.woff2 +0 -0
  492. package/out/web/assets/mermaid.core-DYGGlqOf.js +0 -11
  493. package/out/web/assets/mitr-latin-400-normal-Cb18vPmZ.woff +0 -0
  494. package/out/web/assets/mitr-latin-400-normal-CwbkLQ4a.woff2 +0 -0
  495. package/out/web/assets/mitr-latin-500-normal-B4JOhzAi.woff +0 -0
  496. package/out/web/assets/mitr-latin-500-normal-BmClOow2.woff2 +0 -0
  497. package/out/web/assets/mitr-latin-ext-400-normal-B3CPG7Yo.woff2 +0 -0
  498. package/out/web/assets/mitr-latin-ext-400-normal-DTx65hiC.woff +0 -0
  499. package/out/web/assets/mitr-latin-ext-500-normal-B_Diajz1.woff2 +0 -0
  500. package/out/web/assets/mitr-latin-ext-500-normal-DYOsVaVT.woff +0 -0
  501. package/out/web/assets/mitr-thai-400-normal-CS4Do6aM.woff +0 -0
  502. package/out/web/assets/mitr-thai-400-normal-CXWaAdVj.woff2 +0 -0
  503. package/out/web/assets/mitr-thai-500-normal-CVdBrEVH.woff2 +0 -0
  504. package/out/web/assets/mitr-thai-500-normal-CW_B5D-i.woff +0 -0
  505. package/out/web/assets/mitr-vietnamese-400-normal-CPMGWyoC.woff2 +0 -0
  506. package/out/web/assets/mitr-vietnamese-400-normal-Z6XhIpdY.woff +0 -0
  507. package/out/web/assets/mitr-vietnamese-500-normal-CDrv0585.woff2 +0 -0
  508. package/out/web/assets/mitr-vietnamese-500-normal-CMQWyq-Z.woff +0 -0
  509. package/out/web/assets/noto-sans-thai-latin-400-normal-BaWBrqFP.woff2 +0 -0
  510. package/out/web/assets/noto-sans-thai-latin-400-normal-Dh_FJjOB.woff +0 -0
  511. package/out/web/assets/noto-sans-thai-latin-500-normal-BC04jsmm.woff +0 -0
  512. package/out/web/assets/noto-sans-thai-latin-500-normal-fyBKClF1.woff2 +0 -0
  513. package/out/web/assets/noto-sans-thai-latin-600-normal-CemcjnVR.woff2 +0 -0
  514. package/out/web/assets/noto-sans-thai-latin-600-normal-Dbc-dNO6.woff +0 -0
  515. package/out/web/assets/noto-sans-thai-latin-ext-400-normal-B-1H0YkH.woff +0 -0
  516. package/out/web/assets/noto-sans-thai-latin-ext-400-normal-nBP59pVw.woff2 +0 -0
  517. package/out/web/assets/noto-sans-thai-latin-ext-500-normal-B-ZWb8SZ.woff2 +0 -0
  518. package/out/web/assets/noto-sans-thai-latin-ext-500-normal-CrEHO7C8.woff +0 -0
  519. package/out/web/assets/noto-sans-thai-latin-ext-600-normal-PkMZo0xa.woff +0 -0
  520. package/out/web/assets/noto-sans-thai-latin-ext-600-normal-maHHBsez.woff2 +0 -0
  521. package/out/web/assets/noto-sans-thai-thai-400-normal-BEOmKAI1.woff2 +0 -0
  522. package/out/web/assets/noto-sans-thai-thai-400-normal-Dxb_unuU.woff +0 -0
  523. package/out/web/assets/noto-sans-thai-thai-500-normal-BOGXO2tw.woff +0 -0
  524. package/out/web/assets/noto-sans-thai-thai-500-normal-CU_X86GQ.woff2 +0 -0
  525. package/out/web/assets/noto-sans-thai-thai-600-normal-CmgB8_JK.woff +0 -0
  526. package/out/web/assets/noto-sans-thai-thai-600-normal-Cs73sVjl.woff2 +0 -0
  527. package/out/web/assets/oauthManager-DNiwM8Rn.js +0 -2
  528. package/out/web/assets/oauthManager-DuamS1Av.css +0 -1
  529. package/out/web/assets/outfit-latin-300-normal-AWLfuCom.woff +0 -0
  530. package/out/web/assets/outfit-latin-300-normal-CRYTwY6m.woff2 +0 -0
  531. package/out/web/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
  532. package/out/web/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
  533. package/out/web/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
  534. package/out/web/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
  535. package/out/web/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
  536. package/out/web/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
  537. package/out/web/assets/outfit-latin-ext-300-normal-BYS6Dpvq.woff2 +0 -0
  538. package/out/web/assets/outfit-latin-ext-300-normal-CMIMshEd.woff +0 -0
  539. package/out/web/assets/outfit-latin-ext-400-normal-5tcqmc2S.woff2 +0 -0
  540. package/out/web/assets/outfit-latin-ext-400-normal-DHm7mdGe.woff +0 -0
  541. package/out/web/assets/outfit-latin-ext-500-normal-DrCvqoFD.woff +0 -0
  542. package/out/web/assets/outfit-latin-ext-500-normal-zeox_O30.woff2 +0 -0
  543. package/out/web/assets/outfit-latin-ext-600-normal-B85nYjL1.woff2 +0 -0
  544. package/out/web/assets/outfit-latin-ext-600-normal-CWJcPgd7.woff +0 -0
  545. package/out/web/assets/preload-helper-D0uHx7FM.js +0 -1
  546. package/out/web/assets/prompt-latin-ext-400-normal-1pHbN9uy.woff +0 -0
  547. package/out/web/assets/prompt-latin-ext-400-normal-DdSafGZ9.woff2 +0 -0
  548. package/out/web/assets/prompt-latin-ext-500-normal--EZ1um7s.woff2 +0 -0
  549. package/out/web/assets/prompt-latin-ext-500-normal-D-lI1EVT.woff +0 -0
  550. package/out/web/assets/prompt-latin-ext-600-normal-C8b50x67.woff +0 -0
  551. package/out/web/assets/prompt-latin-ext-600-normal-Cg9L7iJU.woff2 +0 -0
  552. package/out/web/assets/prompt-vietnamese-400-normal-BCPzsgPT.woff2 +0 -0
  553. package/out/web/assets/prompt-vietnamese-400-normal-D4pLyeNK.woff +0 -0
  554. package/out/web/assets/prompt-vietnamese-500-normal-DmzxmPwa.woff2 +0 -0
  555. package/out/web/assets/prompt-vietnamese-500-normal-w_sS_j2F.woff +0 -0
  556. package/out/web/assets/prompt-vietnamese-600-normal-1dyohtNh.woff +0 -0
  557. package/out/web/assets/prompt-vietnamese-600-normal-7QWjJBsF.woff2 +0 -0
  558. package/out/web/assets/sarabun-latin-400-normal-DmguIS-7.woff +0 -0
  559. package/out/web/assets/sarabun-latin-400-normal-URPBxl-K.woff2 +0 -0
  560. package/out/web/assets/sarabun-latin-500-normal-BjUTcdxu.woff2 +0 -0
  561. package/out/web/assets/sarabun-latin-500-normal-CJexTAnl.woff +0 -0
  562. package/out/web/assets/sarabun-latin-600-normal-9FonlkKx.woff +0 -0
  563. package/out/web/assets/sarabun-latin-600-normal-DMD3TROr.woff2 +0 -0
  564. package/out/web/assets/sarabun-latin-ext-400-normal-BbP7GpLL.woff2 +0 -0
  565. package/out/web/assets/sarabun-latin-ext-400-normal-BsmTn0SS.woff +0 -0
  566. package/out/web/assets/sarabun-latin-ext-500-normal-Cot16Bd2.woff +0 -0
  567. package/out/web/assets/sarabun-latin-ext-500-normal-DE1LysqI.woff2 +0 -0
  568. package/out/web/assets/sarabun-latin-ext-600-normal-0tXu9x_i.woff +0 -0
  569. package/out/web/assets/sarabun-latin-ext-600-normal-BRxjKUQ6.woff2 +0 -0
  570. package/out/web/assets/sarabun-thai-400-normal-C2DaJlKK.woff2 +0 -0
  571. package/out/web/assets/sarabun-thai-400-normal-CH-UbWgg.woff +0 -0
  572. package/out/web/assets/sarabun-thai-500-normal-BVssjame.woff2 +0 -0
  573. package/out/web/assets/sarabun-thai-500-normal-C3iDLEzi.woff +0 -0
  574. package/out/web/assets/sarabun-thai-600-normal-3dIAQ-_s.woff2 +0 -0
  575. package/out/web/assets/sarabun-thai-600-normal-DWKeTe6a.woff +0 -0
  576. package/out/web/assets/sarabun-vietnamese-400-normal-DPfkSCMy.woff +0 -0
  577. package/out/web/assets/sarabun-vietnamese-400-normal-igdrzAGY.woff2 +0 -0
  578. package/out/web/assets/sarabun-vietnamese-500-normal-CpRVvGRU.woff +0 -0
  579. package/out/web/assets/sarabun-vietnamese-500-normal-Dpe7Br9m.woff2 +0 -0
  580. package/out/web/assets/sarabun-vietnamese-600-normal-BMbOgAW-.woff +0 -0
  581. package/out/web/assets/sarabun-vietnamese-600-normal-DkxrOlLB.woff2 +0 -0
  582. package/out/web/assets/stateDiagram-v2-4QOOHH4V-s6P9g2_b.js +0 -1
  583. package/out/web/assets/swimlanesDiagram-VK2B7HYN-CV9fOLrV.js +0 -8
  584. package/out/web/assets/tauri-CNrHQ2Gb.js +0 -3
  585. package/out/web/assets/window-DkIhgI7B.js +0 -1
@@ -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)]
@@ -483,35 +561,54 @@ pub async fn stream_chat<F>(
483
561
  ) -> Result<ChatResponse, ChatError>
484
562
  where
485
563
  F: FnMut(String),
564
+ {
565
+ stream_chat_events(config, request, |event| {
566
+ if let ChatStreamEvent::TextDelta { delta } = event {
567
+ on_chunk(delta);
568
+ }
569
+ })
570
+ .await
571
+ }
572
+
573
+ pub async fn stream_chat_events<F>(
574
+ config: &MintConfig,
575
+ request: &ChatRequest,
576
+ mut on_event: F,
577
+ ) -> Result<ChatResponse, ChatError>
578
+ where
579
+ F: FnMut(ChatStreamEvent),
486
580
  {
487
581
  let client = crate::HTTP_CLIENT.clone();
488
582
  let provider = config.ai_provider.as_str();
489
583
  require_supported_attachments(provider, request)?;
490
- let (model, text) = match provider {
491
- "gemini" => stream_gemini(&client, config, request, &mut on_chunk).await?,
584
+ let reply = match provider {
585
+ "gemini" => stream_gemini_reply(&client, config, request, &mut on_event).await?,
492
586
  "openai" | "local_openai" | "openrouter" | "deepseek" => {
493
- stream_openai(&client, config, request, &mut on_chunk).await?
587
+ stream_openai_reply(&client, config, request, &mut on_event).await?
494
588
  }
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?,
589
+ "ollama" => stream_ollama_reply(&client, config, request, &mut on_event).await?,
590
+ "anthropic" => stream_anthropic_reply(&client, config, request, &mut on_event).await?,
591
+ "huggingface" => stream_huggingface_reply(&client, config, request, &mut on_event).await?,
498
592
  p if p.starts_with("custom:") => {
499
- stream_custom_provider(&client, config, request, &mut on_chunk).await?
593
+ stream_custom_provider_reply(&client, config, request, &mut on_event).await?
500
594
  }
501
595
  other => return Err(ChatError::UnsupportedProvider(other.into())),
502
596
  };
503
597
  Ok(ChatResponse {
504
598
  provider: provider.into(),
505
- model,
506
- text,
507
- thought: None,
599
+ model: reply.model,
600
+ text: reply.text,
601
+ thought: reply.thought,
508
602
  fallback_provider: None,
509
603
  fallback_reason: None,
510
- tool_calls: None,
511
- stop_reason: None,
512
- total_tokens: None,
513
- input_tokens: None,
514
- output_tokens: None,
604
+ tool_calls: reply.tool_calls,
605
+ stop_reason: reply.stop_reason,
606
+ total_tokens: match (reply.input_tokens, reply.output_tokens) {
607
+ (Some(input), Some(output)) => Some(input + output),
608
+ (input, output) => input.or(output),
609
+ },
610
+ input_tokens: reply.input_tokens,
611
+ output_tokens: reply.output_tokens,
515
612
  })
516
613
  }
517
614
 
@@ -529,7 +626,9 @@ async fn call_gemini(
529
626
  let payload = if request.messages.is_some() {
530
627
  let mut p = gemini_native_payload(request)?;
531
628
  let temp = effective_temperature(config, request);
532
- p["generationConfig"] = json!({ "temperature": temp });
629
+ let mut gen_cfg = json!({ "temperature": temp });
630
+ gemini_apply_thinking_config(&mut gen_cfg, config, request, &model);
631
+ p["generationConfig"] = gen_cfg;
533
632
  p
534
633
  } else {
535
634
  gemini_chat_payload(config, request)?
@@ -1009,6 +1108,10 @@ async fn call_ollama(
1009
1108
  "num_ctx": config.ollama_num_ctx,
1010
1109
  "temperature": temp,
1011
1110
  });
1111
+ let (thinking_enabled, _) = effective_thinking(config, request, &model);
1112
+ if MintConfig::is_thinking_supported_for_model("ollama", &model) {
1113
+ body["think"] = json!(thinking_enabled);
1114
+ }
1012
1115
  let response: Value = client
1013
1116
  .post(format!("{host}/api/chat"))
1014
1117
  .json(&body)
@@ -1203,11 +1306,27 @@ fn anthropic_chat_payload(
1203
1306
  stream: bool,
1204
1307
  ) -> Result<Value, ChatError> {
1205
1308
  let system = anthropic_system_blocks(&request.system_instruction);
1206
- let temp = effective_temperature(config, request);
1309
+ let (thinking_enabled, thinking_effort) = effective_thinking(config, request, model);
1310
+ let is_thinking_supported = MintConfig::is_thinking_supported_for_model("anthropic", model);
1311
+ let use_thinking = thinking_enabled && is_thinking_supported;
1312
+
1313
+ let (budget, max_tokens, temp) = if use_thinking {
1314
+ let b = match thinking_effort.as_str() {
1315
+ "low" => 1024,
1316
+ "medium" => 4096,
1317
+ "high" => 16384,
1318
+ "extra_high" => 32768,
1319
+ _ => 4096,
1320
+ };
1321
+ (Some(b), b + 8192, 1.0)
1322
+ } else {
1323
+ (None, 8192, effective_temperature(config, request))
1324
+ };
1325
+
1207
1326
  let mut payload = if let Some(messages) = &request.messages {
1208
1327
  json!({
1209
1328
  "model": model,
1210
- "max_tokens": 8192,
1329
+ "max_tokens": max_tokens,
1211
1330
  "system": system,
1212
1331
  "messages": anthropic_messages(messages)?,
1213
1332
  "temperature": temp,
@@ -1215,7 +1334,7 @@ fn anthropic_chat_payload(
1215
1334
  } else {
1216
1335
  json!({
1217
1336
  "model": model,
1218
- "max_tokens": 8192,
1337
+ "max_tokens": max_tokens,
1219
1338
  "system": system,
1220
1339
  "messages": [{
1221
1340
  "role": "user",
@@ -1224,6 +1343,12 @@ fn anthropic_chat_payload(
1224
1343
  "temperature": temp,
1225
1344
  })
1226
1345
  };
1346
+ if let Some(b) = budget {
1347
+ payload["thinking"] = json!({
1348
+ "type": "enabled",
1349
+ "budget_tokens": b,
1350
+ });
1351
+ }
1227
1352
  if stream {
1228
1353
  payload["stream"] = json!(true);
1229
1354
  }
@@ -1469,41 +1594,43 @@ async fn call_huggingface(
1469
1594
  parse_openai_style_reply(model, &response)
1470
1595
  }
1471
1596
 
1472
- async fn stream_gemini<F>(
1597
+ async fn stream_gemini_reply<F>(
1473
1598
  client: &Client,
1474
1599
  config: &MintConfig,
1475
1600
  request: &ChatRequest,
1476
- on_chunk: &mut F,
1477
- ) -> Result<(String, String), ChatError>
1601
+ on_event: &mut F,
1602
+ ) -> Result<ProviderReply, ChatError>
1478
1603
  where
1479
- F: FnMut(String),
1604
+ F: FnMut(ChatStreamEvent),
1480
1605
  {
1481
1606
  let api_key = provider_key(&config.api_key, "GEMINI_API_KEY");
1482
1607
  required_key("gemini", &api_key)?;
1483
1608
  let model = config.gemini_model.clone();
1484
-
1609
+ let payload = if request.messages.is_some() {
1610
+ gemini_native_payload(request)?
1611
+ } else {
1612
+ gemini_chat_payload(config, request)?
1613
+ };
1485
1614
  let response = client
1486
1615
  .post(format!(
1487
1616
  "https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent?alt=sse"
1488
1617
  ))
1489
1618
  .header("x-goog-api-key", api_key)
1490
- .json(&gemini_chat_payload(config, request)?)
1619
+ .json(&payload)
1491
1620
  .send()
1492
1621
  .await?
1493
1622
  .error_for_status()?;
1494
- collect_stream(response, StreamFormat::Gemini, on_chunk)
1495
- .await
1496
- .map(|text| (model, text))
1623
+ collect_stream_reply(response, StreamFormat::Gemini, model, on_event).await
1497
1624
  }
1498
1625
 
1499
- async fn stream_openai<F>(
1626
+ async fn stream_openai_reply<F>(
1500
1627
  client: &Client,
1501
1628
  config: &MintConfig,
1502
1629
  request: &ChatRequest,
1503
- on_chunk: &mut F,
1504
- ) -> Result<(String, String), ChatError>
1630
+ on_event: &mut F,
1631
+ ) -> Result<ProviderReply, ChatError>
1505
1632
  where
1506
- F: FnMut(String),
1633
+ F: FnMut(ChatStreamEvent),
1507
1634
  {
1508
1635
  let local = config.ai_provider == "local_openai";
1509
1636
  let openrouter = config.ai_provider == "openrouter";
@@ -1551,19 +1678,71 @@ where
1551
1678
  let body = response.text().await.unwrap_or_default();
1552
1679
  return Err(ChatError::Api(format!("HTTP {status}: {body}")));
1553
1680
  }
1554
- collect_stream(response, StreamFormat::OpenAi, on_chunk)
1555
- .await
1556
- .map(|text| (model, text))
1681
+ collect_stream_reply(response, StreamFormat::OpenAi, model, on_event).await
1682
+ }
1683
+
1684
+ fn ollama_stream_payload(
1685
+ config: &MintConfig,
1686
+ request: &ChatRequest,
1687
+ model: &str,
1688
+ ) -> Result<Value, ChatError> {
1689
+ let native = request.messages.is_some()
1690
+ && config.tool_calling_mode() == crate::config::ToolCallingMode::Native;
1691
+ let mut payload = if native {
1692
+ let mut value = json!({
1693
+ "model": model,
1694
+ "stream": true,
1695
+ "messages": ollama_native_messages(request.messages.as_deref().unwrap_or(&[])),
1696
+ });
1697
+ if let Some(tools) = &request.tools {
1698
+ value["tools"] = json!(
1699
+ tools
1700
+ .iter()
1701
+ .map(|tool| json!({
1702
+ "type": "function",
1703
+ "function": {
1704
+ "name": tool.name,
1705
+ "description": tool.description,
1706
+ "parameters": tool.parameters,
1707
+ }
1708
+ }))
1709
+ .collect::<Vec<_>>()
1710
+ );
1711
+ }
1712
+ value
1713
+ } else {
1714
+ let mut user = json!({ "role": "user", "content": request.message });
1715
+ if let Some(images) = ollama_images(request) {
1716
+ user["images"] = json!(images);
1717
+ }
1718
+ json!({
1719
+ "model": model,
1720
+ "stream": true,
1721
+ "messages": [
1722
+ { "role": "system", "content": request.system_instruction },
1723
+ user
1724
+ ]
1725
+ })
1726
+ };
1727
+ payload["options"] = json!({
1728
+ "num_ctx": config.ollama_num_ctx,
1729
+ "temperature": effective_temperature(config, request),
1730
+ });
1731
+ let (thinking_enabled, _) = effective_thinking(config, request, model);
1732
+ if MintConfig::is_thinking_supported_for_model("ollama", model) {
1733
+ payload["think"] = json!(thinking_enabled);
1734
+ }
1735
+ Ok(payload)
1557
1736
  }
1558
1737
 
1559
- async fn stream_ollama<F>(
1738
+ async fn stream_ollama_reply<F>(
1560
1739
  client: &Client,
1561
1740
  config: &MintConfig,
1562
1741
  request: &ChatRequest,
1563
- on_chunk: &mut F,
1564
- ) -> Result<(String, String), ChatError>
1742
+ on_event: &mut F,
1743
+ ) -> Result<ProviderReply, ChatError>
1565
1744
  where
1566
- F: FnMut(String),
1745
+ F: FnMut(ChatStreamEvent),
1567
1746
  {
1568
1747
  let host = if config.ollama_host.trim().is_empty() {
1569
1748
  "http://localhost:11434"
@@ -1571,41 +1750,23 @@ where
1571
1750
  config.ollama_host.trim_end_matches('/')
1572
1751
  };
1573
1752
  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
1753
  let response = client
1579
1754
  .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
- }))
1755
+ .json(&ollama_stream_payload(config, request, &model)?)
1593
1756
  .send()
1594
1757
  .await?
1595
1758
  .error_for_status()?;
1596
- collect_stream(response, StreamFormat::Ollama, on_chunk)
1597
- .await
1598
- .map(|text| (model, text))
1759
+ collect_stream_reply(response, StreamFormat::Ollama, model, on_event).await
1599
1760
  }
1600
1761
 
1601
- async fn stream_anthropic<F>(
1762
+ async fn stream_anthropic_reply<F>(
1602
1763
  client: &Client,
1603
1764
  config: &MintConfig,
1604
1765
  request: &ChatRequest,
1605
- on_chunk: &mut F,
1606
- ) -> Result<(String, String), ChatError>
1766
+ on_event: &mut F,
1767
+ ) -> Result<ProviderReply, ChatError>
1607
1768
  where
1608
- F: FnMut(String),
1769
+ F: FnMut(ChatStreamEvent),
1609
1770
  {
1610
1771
  let api_key = provider_key(&config.anthropic_api_key, "ANTHROPIC_API_KEY");
1611
1772
  required_key("anthropic", &api_key)?;
@@ -1615,37 +1776,25 @@ where
1615
1776
  .header("x-api-key", api_key)
1616
1777
  .header("anthropic-version", "2023-06-01")
1617
1778
  .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
- }))
1779
+ .json(&anthropic_chat_payload(config, &model, request, true)?)
1626
1780
  .send()
1627
1781
  .await?
1628
1782
  .error_for_status()?;
1629
- collect_stream(response, StreamFormat::Anthropic, on_chunk)
1630
- .await
1631
- .map(|text| (model, text))
1783
+ collect_stream_reply(response, StreamFormat::Anthropic, model, on_event).await
1632
1784
  }
1633
1785
 
1634
- async fn stream_huggingface<F>(
1786
+ async fn stream_huggingface_reply<F>(
1635
1787
  client: &Client,
1636
1788
  config: &MintConfig,
1637
1789
  request: &ChatRequest,
1638
- on_chunk: &mut F,
1639
- ) -> Result<(String, String), ChatError>
1790
+ on_event: &mut F,
1791
+ ) -> Result<ProviderReply, ChatError>
1640
1792
  where
1641
- F: FnMut(String),
1793
+ F: FnMut(ChatStreamEvent),
1642
1794
  {
1643
1795
  let api_key = provider_key(&config.hf_api_key, "HF_TOKEN");
1644
1796
  required_key("huggingface", &api_key)?;
1645
1797
  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
1798
  let response = client
1650
1799
  .post("https://router.huggingface.co/v1/chat/completions")
1651
1800
  .bearer_auth(api_key)
@@ -1653,12 +1802,41 @@ where
1653
1802
  .send()
1654
1803
  .await?
1655
1804
  .error_for_status()?;
1656
- collect_stream(response, StreamFormat::OpenAi, on_chunk)
1657
- .await
1658
- .map(|text| (model, text))
1805
+ collect_stream_reply(response, StreamFormat::OpenAi, model, on_event).await
1659
1806
  }
1660
1807
 
1661
- #[derive(Clone, Copy)]
1808
+ async fn stream_custom_provider_reply<F>(
1809
+ client: &Client,
1810
+ config: &MintConfig,
1811
+ request: &ChatRequest,
1812
+ on_event: &mut F,
1813
+ ) -> Result<ProviderReply, ChatError>
1814
+ where
1815
+ F: FnMut(ChatStreamEvent),
1816
+ {
1817
+ let provider = config
1818
+ .resolve_custom_provider(&config.ai_provider)
1819
+ .ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
1820
+ let model = resolve_custom_model(config);
1821
+ let mut builder = client
1822
+ .post(format!(
1823
+ "{}/chat/completions",
1824
+ provider.base_url.trim_end_matches('/')
1825
+ ))
1826
+ .json(&openai_chat_payload(config, &model, request, true)?);
1827
+ if !provider.api_key.is_empty() {
1828
+ builder = builder.bearer_auth(&provider.api_key);
1829
+ }
1830
+ for header in &provider.headers {
1831
+ if !header.name.is_empty() {
1832
+ builder = builder.header(header.name.as_str(), header.value.as_str());
1833
+ }
1834
+ }
1835
+ let response = builder.send().await?.error_for_status()?;
1836
+ collect_stream_reply(response, StreamFormat::OpenAi, model, on_event).await
1837
+ }
1838
+
1839
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1662
1840
  enum StreamFormat {
1663
1841
  Gemini,
1664
1842
  OpenAi,
@@ -1666,55 +1844,444 @@ enum StreamFormat {
1666
1844
  Anthropic,
1667
1845
  }
1668
1846
 
1669
- async fn collect_stream<F>(
1847
+ #[derive(Default)]
1848
+ struct StreamToolCall {
1849
+ id: String,
1850
+ name: String,
1851
+ arguments: String,
1852
+ input: Option<Value>,
1853
+ thought_signature: Option<String>,
1854
+ }
1855
+
1856
+ #[derive(Default)]
1857
+ struct StreamReplyAccumulator {
1858
+ text: String,
1859
+ thought: String,
1860
+ tag_buffer: String,
1861
+ inside_think_tag: bool,
1862
+ tool_calls: Vec<StreamToolCall>,
1863
+ stop_reason: Option<String>,
1864
+ input_tokens: Option<u32>,
1865
+ output_tokens: Option<u32>,
1866
+ anthropic_blocks: std::collections::HashMap<usize, usize>,
1867
+ }
1868
+
1869
+ impl StreamReplyAccumulator {
1870
+ fn tool_at(&mut self, index: usize) -> &mut StreamToolCall {
1871
+ while self.tool_calls.len() <= index {
1872
+ self.tool_calls.push(StreamToolCall::default());
1873
+ }
1874
+ &mut self.tool_calls[index]
1875
+ }
1876
+
1877
+ fn emit_text<F>(&mut self, delta: &str, on_event: &mut F)
1878
+ where
1879
+ F: FnMut(ChatStreamEvent),
1880
+ {
1881
+ if !delta.is_empty() {
1882
+ on_event(ChatStreamEvent::TextDelta {
1883
+ delta: delta.to_owned(),
1884
+ });
1885
+ }
1886
+ }
1887
+
1888
+ /// Route inline `<think>` content without assuming tag boundaries line up
1889
+ /// with provider chunks. A possible partial tag is retained until the next
1890
+ /// chunk; ordinary text is emitted immediately.
1891
+ fn push_text<F>(&mut self, delta: &str, on_event: &mut F)
1892
+ where
1893
+ F: FnMut(ChatStreamEvent),
1894
+ {
1895
+ if delta.is_empty() {
1896
+ return;
1897
+ }
1898
+ self.text.push_str(delta);
1899
+ self.tag_buffer.push_str(delta);
1900
+ loop {
1901
+ let tag = if self.inside_think_tag {
1902
+ "</think>"
1903
+ } else {
1904
+ "<think>"
1905
+ };
1906
+ if let Some(index) = self.tag_buffer.find(tag) {
1907
+ let before = self.tag_buffer[..index].to_owned();
1908
+ self.tag_buffer.drain(..index + tag.len());
1909
+ if self.inside_think_tag {
1910
+ self.push_reasoning(&before, on_event);
1911
+ } else {
1912
+ self.emit_text(&before, on_event);
1913
+ }
1914
+ self.inside_think_tag = !self.inside_think_tag;
1915
+ continue;
1916
+ }
1917
+
1918
+ let retain_from = self
1919
+ .tag_buffer
1920
+ .rfind('<')
1921
+ .and_then(|index| tag.starts_with(&self.tag_buffer[index..]).then_some(index));
1922
+ let emit_end = retain_from.unwrap_or(self.tag_buffer.len());
1923
+ if emit_end > 0 {
1924
+ let ready = self.tag_buffer[..emit_end].to_owned();
1925
+ self.tag_buffer.drain(..emit_end);
1926
+ if self.inside_think_tag {
1927
+ self.push_reasoning(&ready, on_event);
1928
+ } else {
1929
+ self.emit_text(&ready, on_event);
1930
+ }
1931
+ }
1932
+ break;
1933
+ }
1934
+ }
1935
+
1936
+ fn flush_tag_buffer<F>(&mut self, on_event: &mut F)
1937
+ where
1938
+ F: FnMut(ChatStreamEvent),
1939
+ {
1940
+ if self.tag_buffer.is_empty() {
1941
+ return;
1942
+ }
1943
+ let remaining = std::mem::take(&mut self.tag_buffer);
1944
+ if self.inside_think_tag {
1945
+ self.push_reasoning(&remaining, on_event);
1946
+ } else {
1947
+ self.emit_text(&remaining, on_event);
1948
+ }
1949
+ }
1950
+
1951
+ fn push_reasoning<F>(&mut self, delta: &str, on_event: &mut F)
1952
+ where
1953
+ F: FnMut(ChatStreamEvent),
1954
+ {
1955
+ if !delta.is_empty() {
1956
+ self.thought.push_str(delta);
1957
+ on_event(ChatStreamEvent::ReasoningDelta {
1958
+ delta: delta.to_owned(),
1959
+ });
1960
+ }
1961
+ }
1962
+
1963
+ fn consume<F>(&mut self, format: StreamFormat, value: &Value, on_event: &mut F)
1964
+ where
1965
+ F: FnMut(ChatStreamEvent),
1966
+ {
1967
+ match format {
1968
+ StreamFormat::OpenAi => self.consume_openai(value, on_event),
1969
+ StreamFormat::Gemini => self.consume_gemini(value, on_event),
1970
+ StreamFormat::Ollama => self.consume_ollama(value, on_event),
1971
+ StreamFormat::Anthropic => self.consume_anthropic(value, on_event),
1972
+ }
1973
+ }
1974
+
1975
+ fn consume_openai<F>(&mut self, value: &Value, on_event: &mut F)
1976
+ where
1977
+ F: FnMut(ChatStreamEvent),
1978
+ {
1979
+ let choice = &value["choices"][0];
1980
+ let delta = &choice["delta"];
1981
+ if let Some(text) = delta["content"].as_str() {
1982
+ self.push_text(text, on_event);
1983
+ }
1984
+ if let Some(reasoning) = delta["reasoning_content"]
1985
+ .as_str()
1986
+ .or_else(|| delta["reasoning"].as_str())
1987
+ .or_else(|| delta["thinking"].as_str())
1988
+ {
1989
+ self.push_reasoning(reasoning, on_event);
1990
+ }
1991
+ if let Some(details) = delta["reasoning_details"].as_array() {
1992
+ for detail in details {
1993
+ if let Some(reasoning) = detail["text"].as_str() {
1994
+ self.push_reasoning(reasoning, on_event);
1995
+ }
1996
+ }
1997
+ }
1998
+ if let Some(calls) = delta["tool_calls"].as_array() {
1999
+ for (fallback_index, call) in calls.iter().enumerate() {
2000
+ let index = call["index"]
2001
+ .as_u64()
2002
+ .map(|n| n as usize)
2003
+ .unwrap_or(fallback_index);
2004
+ let name_delta = call["function"]["name"].as_str().map(str::to_owned);
2005
+ let arguments = call["function"]["arguments"].as_str().map(str::to_owned);
2006
+ let current_name = {
2007
+ let tool = self.tool_at(index);
2008
+ if let Some(id) = call["id"].as_str() {
2009
+ tool.id.push_str(id);
2010
+ }
2011
+ if let Some(name) = &name_delta {
2012
+ tool.name.push_str(name);
2013
+ }
2014
+ if let Some(arguments) = &arguments {
2015
+ tool.arguments.push_str(arguments);
2016
+ }
2017
+ tool.name.clone()
2018
+ };
2019
+ if name_delta.is_some() || arguments.is_some() {
2020
+ on_event(ChatStreamEvent::ToolCallDelta {
2021
+ index,
2022
+ name: name_delta.map(|_| current_name),
2023
+ arguments,
2024
+ input: None,
2025
+ });
2026
+ }
2027
+ }
2028
+ }
2029
+ if let Some(reason) = choice["finish_reason"].as_str() {
2030
+ self.stop_reason = Some(reason.to_owned());
2031
+ }
2032
+ self.input_tokens = value["usage"]["prompt_tokens"]
2033
+ .as_u64()
2034
+ .map(|n| n as u32)
2035
+ .or(self.input_tokens);
2036
+ self.output_tokens = value["usage"]["completion_tokens"]
2037
+ .as_u64()
2038
+ .map(|n| n as u32)
2039
+ .or(self.output_tokens);
2040
+ }
2041
+
2042
+ fn consume_gemini<F>(&mut self, value: &Value, on_event: &mut F)
2043
+ where
2044
+ F: FnMut(ChatStreamEvent),
2045
+ {
2046
+ if let Some(parts) = value["candidates"][0]["content"]["parts"].as_array() {
2047
+ for (index, part) in parts.iter().enumerate() {
2048
+ if let Some(text) = part["text"].as_str() {
2049
+ if part["thought"].as_bool().unwrap_or(false) {
2050
+ self.push_reasoning(text, on_event);
2051
+ } else {
2052
+ self.push_text(text, on_event);
2053
+ }
2054
+ }
2055
+ if let Some(call) = part.get("functionCall") {
2056
+ let tool = self.tool_at(index);
2057
+ tool.id = format!("call_{index}");
2058
+ tool.name = call["name"].as_str().unwrap_or_default().to_owned();
2059
+ tool.input = Some(call["args"].clone());
2060
+ tool.thought_signature = part["thoughtSignature"].as_str().map(str::to_owned);
2061
+ on_event(ChatStreamEvent::ToolCallDelta {
2062
+ index,
2063
+ name: call["name"].as_str().map(str::to_owned),
2064
+ arguments: None,
2065
+ input: Some(call["args"].clone()),
2066
+ });
2067
+ }
2068
+ }
2069
+ }
2070
+ self.stop_reason = value["candidates"][0]["finishReason"]
2071
+ .as_str()
2072
+ .map(str::to_owned)
2073
+ .or_else(|| self.stop_reason.take());
2074
+ self.input_tokens = value["usageMetadata"]["promptTokenCount"]
2075
+ .as_u64()
2076
+ .map(|n| n as u32)
2077
+ .or(self.input_tokens);
2078
+ self.output_tokens = value["usageMetadata"]["candidatesTokenCount"]
2079
+ .as_u64()
2080
+ .map(|n| n as u32)
2081
+ .or(self.output_tokens);
2082
+ }
2083
+
2084
+ fn consume_ollama<F>(&mut self, value: &Value, on_event: &mut F)
2085
+ where
2086
+ F: FnMut(ChatStreamEvent),
2087
+ {
2088
+ let message = &value["message"];
2089
+ if let Some(text) = message["content"].as_str() {
2090
+ self.push_text(text, on_event);
2091
+ }
2092
+ if let Some(reasoning) = message["thinking"]
2093
+ .as_str()
2094
+ .or_else(|| message["reasoning_content"].as_str())
2095
+ {
2096
+ self.push_reasoning(reasoning, on_event);
2097
+ }
2098
+ if let Some(calls) = message["tool_calls"].as_array() {
2099
+ for (index, call) in calls.iter().enumerate() {
2100
+ let tool = self.tool_at(index);
2101
+ tool.id = call["id"]
2102
+ .as_str()
2103
+ .map(str::to_owned)
2104
+ .unwrap_or_else(|| format!("call_{index}"));
2105
+ tool.name = call["function"]["name"]
2106
+ .as_str()
2107
+ .unwrap_or_default()
2108
+ .to_owned();
2109
+ tool.input = Some(call["function"]["arguments"].clone());
2110
+ on_event(ChatStreamEvent::ToolCallDelta {
2111
+ index,
2112
+ name: call["function"]["name"].as_str().map(str::to_owned),
2113
+ arguments: None,
2114
+ input: Some(call["function"]["arguments"].clone()),
2115
+ });
2116
+ }
2117
+ }
2118
+ self.input_tokens = value["prompt_eval_count"]
2119
+ .as_u64()
2120
+ .map(|n| n as u32)
2121
+ .or(self.input_tokens);
2122
+ self.output_tokens = value["eval_count"]
2123
+ .as_u64()
2124
+ .map(|n| n as u32)
2125
+ .or(self.output_tokens);
2126
+ if value["done"].as_bool().unwrap_or(false) {
2127
+ self.stop_reason = value["done_reason"]
2128
+ .as_str()
2129
+ .map(str::to_owned)
2130
+ .or_else(|| Some("stop".to_owned()));
2131
+ }
2132
+ }
2133
+
2134
+ fn consume_anthropic<F>(&mut self, value: &Value, on_event: &mut F)
2135
+ where
2136
+ F: FnMut(ChatStreamEvent),
2137
+ {
2138
+ match value["type"].as_str() {
2139
+ Some("message_start") => {
2140
+ self.input_tokens = value["message"]["usage"]["input_tokens"]
2141
+ .as_u64()
2142
+ .map(|n| n as u32);
2143
+ }
2144
+ Some("content_block_start") => {
2145
+ let block_index = value["index"].as_u64().unwrap_or(0) as usize;
2146
+ let block = &value["content_block"];
2147
+ if block["type"].as_str() == Some("tool_use") {
2148
+ let tool_index = self.tool_calls.len();
2149
+ let tool = self.tool_at(tool_index);
2150
+ tool.id = block["id"].as_str().unwrap_or_default().to_owned();
2151
+ tool.name = block["name"].as_str().unwrap_or_default().to_owned();
2152
+ self.anthropic_blocks.insert(block_index, tool_index);
2153
+ on_event(ChatStreamEvent::ToolCallDelta {
2154
+ index: tool_index,
2155
+ name: block["name"].as_str().map(str::to_owned),
2156
+ arguments: None,
2157
+ input: None,
2158
+ });
2159
+ }
2160
+ }
2161
+ Some("content_block_delta") => {
2162
+ let delta = &value["delta"];
2163
+ match delta["type"].as_str() {
2164
+ Some("text_delta") => {
2165
+ if let Some(text) = delta["text"].as_str() {
2166
+ self.push_text(text, on_event);
2167
+ }
2168
+ }
2169
+ Some("thinking_delta") => {
2170
+ if let Some(thinking) = delta["thinking"].as_str() {
2171
+ self.push_reasoning(thinking, on_event);
2172
+ }
2173
+ }
2174
+ Some("input_json_delta") => {
2175
+ let block_index = value["index"].as_u64().unwrap_or(0) as usize;
2176
+ if let Some(tool_index) = self.anthropic_blocks.get(&block_index).copied()
2177
+ && let Some(json) = delta["partial_json"].as_str()
2178
+ {
2179
+ self.tool_at(tool_index).arguments.push_str(json);
2180
+ on_event(ChatStreamEvent::ToolCallDelta {
2181
+ index: tool_index,
2182
+ name: None,
2183
+ arguments: Some(json.to_owned()),
2184
+ input: None,
2185
+ });
2186
+ }
2187
+ }
2188
+ _ => {}
2189
+ }
2190
+ }
2191
+ Some("message_delta") => {
2192
+ self.stop_reason = value["delta"]["stop_reason"]
2193
+ .as_str()
2194
+ .map(str::to_owned)
2195
+ .or_else(|| self.stop_reason.take());
2196
+ self.output_tokens = value["usage"]["output_tokens"]
2197
+ .as_u64()
2198
+ .map(|n| n as u32)
2199
+ .or(self.output_tokens);
2200
+ }
2201
+ _ => {}
2202
+ }
2203
+ }
2204
+
2205
+ fn finish(self, model: String) -> Result<ProviderReply, ChatError> {
2206
+ let (tag_thought, clean_text) = extract_think_tag(&self.text);
2207
+ let thought = if self.thought.trim().is_empty() {
2208
+ tag_thought
2209
+ } else {
2210
+ Some(self.thought.trim().to_owned())
2211
+ };
2212
+ let calls: Vec<ToolCall> = self
2213
+ .tool_calls
2214
+ .into_iter()
2215
+ .filter(|tool| !tool.name.is_empty())
2216
+ .map(|tool| {
2217
+ let input = tool.input.unwrap_or_else(|| {
2218
+ serde_json::from_str(&tool.arguments).unwrap_or_else(|_| json!({}))
2219
+ });
2220
+ ToolCall {
2221
+ id: tool.id,
2222
+ name: tool.name,
2223
+ input,
2224
+ thought_signature: tool.thought_signature,
2225
+ }
2226
+ })
2227
+ .collect();
2228
+ if clean_text.is_empty() && calls.is_empty() && thought.is_none() {
2229
+ return Err(ChatError::MissingResponseText);
2230
+ }
2231
+ Ok(ProviderReply {
2232
+ model,
2233
+ text: if clean_text.is_empty() && calls.is_empty() {
2234
+ thought.clone().unwrap_or_default()
2235
+ } else {
2236
+ clean_text
2237
+ },
2238
+ thought,
2239
+ tool_calls: (!calls.is_empty()).then_some(calls),
2240
+ stop_reason: self.stop_reason,
2241
+ input_tokens: self.input_tokens,
2242
+ output_tokens: self.output_tokens,
2243
+ })
2244
+ }
2245
+ }
2246
+
2247
+ async fn collect_stream_reply<F>(
1670
2248
  response: reqwest::Response,
1671
2249
  format: StreamFormat,
1672
- on_chunk: &mut F,
1673
- ) -> Result<String, ChatError>
2250
+ model: String,
2251
+ on_event: &mut F,
2252
+ ) -> Result<ProviderReply, ChatError>
1674
2253
  where
1675
- F: FnMut(String),
2254
+ F: FnMut(ChatStreamEvent),
1676
2255
  {
1677
2256
  let mut bytes = response.bytes_stream();
1678
2257
  let mut buffer = String::new();
1679
- let mut text = String::new();
2258
+ let mut accumulator = StreamReplyAccumulator::default();
1680
2259
  while let Some(chunk) = bytes.next().await {
1681
2260
  buffer.push_str(&String::from_utf8_lossy(&chunk?));
1682
2261
  while let Some(index) = buffer.find('\n') {
1683
2262
  let line = buffer[..index].trim().to_owned();
1684
2263
  buffer.drain(..=index);
1685
- if let Some(chunk) = parse_stream_line(format, &line) {
1686
- on_chunk(chunk.clone());
1687
- text.push_str(&chunk);
2264
+ if let Some(value) = parse_stream_value(format, &line) {
2265
+ accumulator.consume(format, &value, on_event);
1688
2266
  }
1689
2267
  }
1690
2268
  }
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)
2269
+ if let Some(value) = parse_stream_value(format, buffer.trim()) {
2270
+ accumulator.consume(format, &value, on_event);
1699
2271
  }
2272
+ accumulator.flush_tag_buffer(on_event);
2273
+ accumulator.finish(model)
1700
2274
  }
1701
2275
 
1702
- fn parse_stream_line(format: StreamFormat, line: &str) -> Option<String> {
2276
+ fn parse_stream_value(format: StreamFormat, line: &str) -> Option<Value> {
1703
2277
  let payload = match format {
1704
2278
  StreamFormat::Ollama => line,
1705
- _ => line.strip_prefix("data: ")?.trim(),
2279
+ _ => line.strip_prefix("data:")?.trim(),
1706
2280
  };
1707
2281
  if payload.is_empty() || payload == "[DONE]" {
1708
2282
  return None;
1709
2283
  }
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())
2284
+ serde_json::from_str(payload).ok()
1718
2285
  }
1719
2286
 
1720
2287
  fn required_key(provider: &str, key: &str) -> Result<(), ChatError> {
@@ -1786,42 +2353,6 @@ async fn call_custom_provider(
1786
2353
  parse_openai_style_reply(model, &response)
1787
2354
  }
1788
2355
 
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
2356
  fn provider_key(configured: &str, environment_variable: &str) -> String {
1826
2357
  if configured.trim().is_empty() {
1827
2358
  std::env::var(environment_variable).unwrap_or_default()
@@ -1913,13 +2444,15 @@ fn gemini_chat_payload(config: &MintConfig, request: &ChatRequest) -> Result<Val
1913
2444
  "systemInstruction": { "parts": [{ "text": request.system_instruction }] },
1914
2445
  "contents": [{ "role": "user", "parts": gemini_parts(request)? }]
1915
2446
  });
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;
2447
+ let mut gen_cfg = if wants_agent_json(request) {
2448
+ let mut cfg = gemini_agent_generation_config(config);
2449
+ cfg["temperature"] = json!(temp);
2450
+ cfg
1920
2451
  } else {
1921
- payload["generationConfig"] = json!({ "temperature": temp });
1922
- }
2452
+ json!({ "temperature": temp })
2453
+ };
2454
+ gemini_apply_thinking_config(&mut gen_cfg, config, request, &config.gemini_model);
2455
+ payload["generationConfig"] = gen_cfg;
1923
2456
  Ok(payload)
1924
2457
  }
1925
2458
 
@@ -2042,6 +2575,47 @@ pub(crate) fn effective_temperature(config: &MintConfig, request: &ChatRequest)
2042
2575
  .unwrap_or_else(|| config.resolved_temperature())
2043
2576
  }
2044
2577
 
2578
+ /// Resolves (enabled, effort) for this request and model, prioritizing request overrides.
2579
+ pub(crate) fn effective_thinking(
2580
+ config: &MintConfig,
2581
+ request: &ChatRequest,
2582
+ model: &str,
2583
+ ) -> (bool, String) {
2584
+ let enabled = request
2585
+ .thinking_enabled
2586
+ .unwrap_or_else(|| config.resolved_thinking_enabled_for_model(&config.ai_provider, model));
2587
+ let effort = request
2588
+ .thinking_effort
2589
+ .as_deref()
2590
+ .map(str::to_ascii_lowercase)
2591
+ .unwrap_or_else(|| config.resolved_thinking_effort_for_model(model).to_string());
2592
+ (enabled, effort)
2593
+ }
2594
+
2595
+ pub(crate) fn gemini_apply_thinking_config(
2596
+ gen_cfg: &mut Value,
2597
+ config: &MintConfig,
2598
+ request: &ChatRequest,
2599
+ model: &str,
2600
+ ) {
2601
+ if !MintConfig::is_thinking_supported_for_model("gemini", model) {
2602
+ return;
2603
+ }
2604
+ let (enabled, effort) = effective_thinking(config, request, model);
2605
+ if !enabled {
2606
+ gen_cfg["thinkingConfig"] = json!({ "thinkingBudget": 0 });
2607
+ } else {
2608
+ let budget = match effort.as_str() {
2609
+ "low" => 1024,
2610
+ "medium" => 4096,
2611
+ "high" => 16384,
2612
+ "extra_high" => 32768,
2613
+ _ => 4096,
2614
+ };
2615
+ gen_cfg["thinkingConfig"] = json!({ "thinkingBudget": budget });
2616
+ }
2617
+ }
2618
+
2045
2619
  fn openai_chat_payload(
2046
2620
  config: &MintConfig,
2047
2621
  model: &str,
@@ -2070,21 +2644,50 @@ fn openai_chat_payload(
2070
2644
  }),
2071
2645
  ]
2072
2646
  };
2647
+ // Left unset, several OpenAI-compatible providers (DeepSeek in
2648
+ // particular) default to a much lower output cap than the model can
2649
+ // actually produce, which silently truncates long structured answers
2650
+ // (a code block followed by a couple of markdown tables is enough to
2651
+ // hit it) — matches the explicit cap already used for Anthropic in
2652
+ // `anthropic_chat_payload`/`stream_anthropic`.
2653
+ // For Groq endpoints, free tiers enforce a tight 6,000–8,000 TPM limit
2654
+ // where a static 8192 max_tokens triggers immediate HTTP 413 / rate limit errors.
2655
+ let max_tokens = {
2656
+ let is_groq = config
2657
+ .resolve_custom_provider(&config.ai_provider)
2658
+ .map(|cp| cp.base_url.contains("groq.com"))
2659
+ .unwrap_or(false);
2660
+ if is_groq {
2661
+ if request.tools.is_some() { 2048 } else { 4096 }
2662
+ } else {
2663
+ 8192
2664
+ }
2665
+ };
2073
2666
  let mut payload = json!({
2074
2667
  "model": model,
2075
2668
  "stream": stream,
2076
2669
  "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,
2670
+ "max_tokens": max_tokens,
2084
2671
  });
2672
+ if stream {
2673
+ payload["stream_options"] = json!({ "include_usage": true });
2674
+ }
2085
2675
  // OpenAI reasoning models (o1, o3, etc.) reject explicit temperature parameter.
2086
2676
  let is_reasoning_model = model.starts_with("o1") || model.starts_with("o3");
2087
- if !is_reasoning_model {
2677
+ let (thinking_enabled, thinking_effort) = effective_thinking(config, request, model);
2678
+ if is_reasoning_model {
2679
+ if thinking_enabled {
2680
+ let effort_str = match thinking_effort.as_str() {
2681
+ "low" => "low",
2682
+ "medium" => "medium",
2683
+ "high" | "extra_high" => "high",
2684
+ _ => "medium",
2685
+ };
2686
+ payload["reasoning_effort"] = json!(effort_str);
2687
+ } else {
2688
+ payload["reasoning_effort"] = json!("low");
2689
+ }
2690
+ } else {
2088
2691
  payload["temperature"] = json!(effective_temperature(config, request));
2089
2692
  }
2090
2693
  if wants_agent_json(request) {
@@ -2369,6 +2972,7 @@ mod tests {
2369
2972
  messages: None,
2370
2973
  tools: None,
2371
2974
  temperature: None,
2975
+ ..Default::default()
2372
2976
  };
2373
2977
 
2374
2978
  let error = require_supported_attachments("openai", &request).unwrap_err();
@@ -2379,28 +2983,85 @@ mod tests {
2379
2983
 
2380
2984
  #[test]
2381
2985
  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")
2986
+ let openai = parse_stream_value(
2987
+ StreamFormat::OpenAi,
2988
+ r#"data: {"choices":[{"delta":{"content":"hello"}}]}"#,
2989
+ )
2990
+ .unwrap();
2991
+ assert_eq!(openai["choices"][0]["delta"]["content"], "hello");
2992
+ let ollama =
2993
+ parse_stream_value(StreamFormat::Ollama, r#"{"message":{"content":"hi"}}"#).unwrap();
2994
+ assert_eq!(ollama["message"]["content"], "hi");
2995
+ assert!(parse_stream_value(StreamFormat::Anthropic, "data: [DONE]").is_none());
2996
+ }
2997
+
2998
+ #[test]
2999
+ fn accumulates_openai_reasoning_tools_and_usage() {
3000
+ let mut accumulator = StreamReplyAccumulator::default();
3001
+ let mut events = Vec::new();
3002
+ accumulator.consume_openai(
3003
+ &json!({
3004
+ "choices": [{"delta": {"reasoning_content": "inspect "}}]
3005
+ }),
3006
+ &mut |event| events.push(event),
3007
+ );
3008
+ accumulator.consume_openai(&json!({
3009
+ "choices": [{"delta": {"reasoning_content": "files", "tool_calls": [{
3010
+ "index": 0, "id": "call_1", "function": {"name": "read_file", "arguments": "{\"path\":"}
3011
+ }]}}]
3012
+ }), &mut |event| events.push(event));
3013
+ accumulator.consume_openai(
3014
+ &json!({
3015
+ "choices": [{"delta": {"tool_calls": [{
3016
+ "index": 0, "function": {"arguments": "\"README.md\"}"}
3017
+ }]}, "finish_reason": "tool_calls"}],
3018
+ "usage": {"prompt_tokens": 10, "completion_tokens": 5}
3019
+ }),
3020
+ &mut |event| events.push(event),
2389
3021
  );
3022
+ accumulator.flush_tag_buffer(&mut |event| events.push(event));
3023
+ let reply = accumulator.finish("test".into()).unwrap();
3024
+ assert_eq!(reply.thought.as_deref(), Some("inspect files"));
3025
+ assert_eq!(reply.stop_reason.as_deref(), Some("tool_calls"));
3026
+ assert_eq!(reply.input_tokens, Some(10));
3027
+ assert_eq!(reply.tool_calls.unwrap()[0].input["path"], "README.md");
2390
3028
  assert_eq!(
2391
- parse_stream_line(StreamFormat::Ollama, r#"{"message":{"content":"hi"}}"#).as_deref(),
2392
- Some("hi")
3029
+ events
3030
+ .iter()
3031
+ .filter(|event| matches!(event, ChatStreamEvent::ReasoningDelta { .. }))
3032
+ .count(),
3033
+ 2
2393
3034
  );
2394
3035
  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")
3036
+ events
3037
+ .iter()
3038
+ .filter(|event| matches!(event, ChatStreamEvent::ToolCallDelta { .. }))
3039
+ .count(),
3040
+ 2
2401
3041
  );
2402
3042
  }
2403
3043
 
3044
+ #[test]
3045
+ fn streams_think_tags_split_across_chunks() {
3046
+ let mut accumulator = StreamReplyAccumulator::default();
3047
+ let mut events = Vec::new();
3048
+ for chunk in ["ก่อน<thi", "nk>คิด", "อยู่</th", "ink>หลัง"] {
3049
+ accumulator.push_text(chunk, &mut |event| events.push(event));
3050
+ }
3051
+ accumulator.flush_tag_buffer(&mut |event| events.push(event));
3052
+ let reply = accumulator.finish("test".into()).unwrap();
3053
+ assert_eq!(reply.text, "ก่อนหลัง");
3054
+ assert_eq!(reply.thought.as_deref(), Some("คิดอยู่"));
3055
+ let streamed_reasoning: String = events
3056
+ .iter()
3057
+ .filter_map(|event| match event {
3058
+ ChatStreamEvent::ReasoningDelta { delta } => Some(delta.as_str()),
3059
+ _ => None,
3060
+ })
3061
+ .collect();
3062
+ assert_eq!(streamed_reasoning, "คิดอยู่");
3063
+ }
3064
+
2404
3065
  #[test]
2405
3066
  fn builds_gemini_multimodal_parts() {
2406
3067
  let parts = gemini_parts(&ChatRequest {
@@ -2418,6 +3079,7 @@ mod tests {
2418
3079
  messages: None,
2419
3080
  tools: None,
2420
3081
  temperature: None,
3082
+ ..Default::default()
2421
3083
  })
2422
3084
  .unwrap();
2423
3085
  assert_eq!(parts.len(), 3);
@@ -2453,6 +3115,7 @@ mod tests {
2453
3115
  messages: Some(messages),
2454
3116
  tools,
2455
3117
  temperature: None,
3118
+ ..Default::default()
2456
3119
  }
2457
3120
  }
2458
3121
 
@@ -2673,6 +3336,7 @@ mod tests {
2673
3336
  messages: None,
2674
3337
  tools: None,
2675
3338
  temperature: None,
3339
+ ..Default::default()
2676
3340
  };
2677
3341
  assert!(require_supported_attachments("openai", &request).is_ok());
2678
3342
  }
@@ -2698,6 +3362,7 @@ mod tests {
2698
3362
  messages: None,
2699
3363
  tools: None,
2700
3364
  temperature: None,
3365
+ ..Default::default()
2701
3366
  };
2702
3367
  for provider in [
2703
3368
  "local_openai",
@@ -3231,6 +3896,7 @@ mod tests {
3231
3896
  let config = MintConfig {
3232
3897
  ai_provider: "anthropic".into(),
3233
3898
  anthropic_model: "claude-sonnet-5".into(),
3899
+ thinking_enabled: false,
3234
3900
  ..MintConfig::default()
3235
3901
  };
3236
3902
  let payload = anthropic_chat_payload(&config, "claude-sonnet-5", &request, false).unwrap();