@pheem49/mint 1.17.0 → 1.18.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 (421) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +2 -2
  3. package/README.md +8 -2
  4. package/Release_Note.md +222 -71
  5. package/crates/mint-cli/src/agent/diff_render.rs +66 -44
  6. package/crates/mint-cli/src/agent/live_status.rs +21 -4
  7. package/crates/mint-cli/src/agent/markdown_render.rs +342 -5
  8. package/crates/mint-cli/src/agent.rs +25 -10
  9. package/crates/mint-cli/src/commands/agent.rs +4 -92
  10. package/crates/mint-cli/src/commands/browser.rs +2 -15
  11. package/crates/mint-cli/src/commands/integrations.rs +3 -3
  12. package/crates/mint-cli/src/commands/mod.rs +0 -3
  13. package/crates/mint-cli/src/commands/tailnet_web.rs +1 -0
  14. package/crates/mint-cli/src/interactive/chat_tui/dialog.rs +572 -0
  15. package/crates/mint-cli/src/interactive/chat_tui/mod.rs +1427 -0
  16. package/crates/mint-cli/src/interactive/chat_tui/render.rs +1524 -0
  17. package/crates/mint-cli/src/interactive/chat_tui/selection.rs +401 -0
  18. package/crates/mint-cli/src/interactive/chat_tui/state.rs +1833 -0
  19. package/crates/mint-cli/src/interactive/chat_tui/tests.rs +3414 -0
  20. package/crates/mint-cli/src/interactive/chat_tui/thought.rs +202 -0
  21. package/crates/mint-cli/src/interactive/chat_tui/transcript.rs +728 -0
  22. package/crates/mint-cli/src/interactive/chat_tui/watermark.rs +148 -0
  23. package/crates/mint-cli/src/interactive/command_ui.rs +53 -5
  24. package/crates/mint-cli/src/interactive/composer.rs +186 -0
  25. package/crates/mint-cli/src/interactive/format.rs +78 -3
  26. package/crates/mint-cli/src/interactive/input_box.rs +61 -1
  27. package/crates/mint-cli/src/interactive/resume_picker.rs +367 -25
  28. package/crates/mint-cli/src/interactive/slash_commands.rs +648 -371
  29. package/crates/mint-cli/src/interactive/tool_activity.rs +93 -20
  30. package/crates/mint-cli/src/interactive.rs +37 -0
  31. package/crates/mint-cli/src/main.rs +6 -3
  32. package/crates/mint-cli/src/markdown.rs +2 -37
  33. package/crates/mint-cli/src/onboard.rs +700 -282
  34. package/crates/mint-cli/src/setup.rs +1342 -624
  35. package/crates/mint-cli/src/terminal_theme.rs +159 -35
  36. package/crates/mint-cli/tests/library_intake.rs +1 -0
  37. package/crates/mint-core/src/agent/chat/anthropic.rs +318 -0
  38. package/crates/mint-core/src/agent/chat/custom.rs +149 -0
  39. package/crates/mint-core/src/agent/chat/gemini.rs +518 -0
  40. package/crates/mint-core/src/agent/chat/mod.rs +857 -0
  41. package/crates/mint-core/src/agent/chat/ollama.rs +321 -0
  42. package/crates/mint-core/src/agent/chat/openai.rs +582 -0
  43. package/crates/mint-core/src/agent/chat/streaming.rs +469 -0
  44. package/crates/mint-core/src/agent/chat/tests.rs +1479 -0
  45. package/crates/mint-core/src/agent/memory.rs +56 -2
  46. package/crates/mint-core/src/agent/tasks.rs +2 -2
  47. package/crates/mint-core/src/api_server/routes/chat.rs +24 -2
  48. package/crates/mint-core/src/api_server/routes/gemini_live.rs +14 -1
  49. package/crates/mint-core/src/api_server/routes/html_preview.rs +1 -8
  50. package/crates/mint-core/src/api_server/routes/sessions.rs +21 -0
  51. package/crates/mint-core/src/avatar_bridge.rs +0 -22
  52. package/crates/mint-core/src/browser/cdp.rs +10 -4
  53. package/crates/mint-core/src/browser/headless.rs +582 -0
  54. package/crates/mint-core/src/browser/input.rs +7 -62
  55. package/crates/mint-core/src/browser/interact.rs +0 -14
  56. package/crates/mint-core/src/browser/mod.rs +8 -29
  57. package/crates/mint-core/src/browser/navigate.rs +2 -36
  58. package/crates/mint-core/src/browser/runtime.rs +2 -4
  59. package/crates/mint-core/src/browser/session.rs +50 -466
  60. package/crates/mint-core/src/browser/workflow.rs +142 -32
  61. package/crates/mint-core/src/cron/store.rs +2 -2
  62. package/crates/mint-core/src/integrations/mcp.rs +6 -8
  63. package/crates/mint-core/src/lib.rs +0 -5
  64. package/crates/mint-core/src/media/gemini_live.rs +636 -86
  65. package/crates/mint-core/src/media/library/migration.rs +26 -0
  66. package/crates/mint-core/src/media/library/tests.rs +1 -1
  67. package/crates/mint-core/src/media/library/thumbnails.rs +8 -2
  68. package/crates/mint-core/src/media/pictures.rs +1 -1
  69. package/crates/mint-core/src/media/tool_images.rs +92 -1
  70. package/crates/mint-core/src/mods/manager.rs +101 -35
  71. package/crates/mint-core/src/orchestration/context_render.rs +441 -24
  72. package/crates/mint-core/src/orchestration/decision_parsing.rs +531 -16
  73. package/crates/mint-core/src/orchestration/full_skill_result_tests.rs +12 -0
  74. package/crates/mint-core/src/orchestration/loop_guard.rs +145 -0
  75. package/crates/mint-core/src/orchestration/mcp_outcome_tests.rs +864 -0
  76. package/crates/mint-core/src/orchestration/memory_skill.rs +88 -28
  77. package/crates/mint-core/src/orchestration/mod.rs +320 -4249
  78. package/crates/mint-core/src/orchestration/reliability.rs +67 -27
  79. package/crates/mint-core/src/orchestration/review_regressions.rs +5 -0
  80. package/crates/mint-core/src/orchestration/tests.rs +1665 -0
  81. package/crates/mint-core/src/orchestration/tools/dispatch.rs +967 -0
  82. package/crates/mint-core/src/orchestration/tools/files.rs +1 -0
  83. package/crates/mint-core/src/orchestration/tools/mod.rs +5 -1
  84. package/crates/mint-core/src/orchestration/tools/planning.rs +102 -42
  85. package/crates/mint-core/src/orchestration/tools/shell.rs +129 -58
  86. package/crates/mint-core/src/orchestration/truncation.rs +170 -13
  87. package/crates/mint-core/src/orchestration/types.rs +648 -0
  88. package/crates/mint-core/src/orchestration/verification.rs +0 -3
  89. package/crates/mint-core/src/orchestration/workspace_helpers.rs +24 -6
  90. package/crates/mint-core/src/prompts/agent.rs +52 -76
  91. package/crates/mint-core/src/prompts/tool_catalog.rs +8 -151
  92. package/crates/mint-core/src/search/files.rs +20 -0
  93. package/crates/mint-core/src/slash/mod.rs +228 -265
  94. package/crates/mint-core/src/slash/models.rs +6 -1
  95. package/crates/mint-core/src/system/bg_shell.rs +1 -0
  96. package/crates/mint-core/src/system/config.rs +176 -34
  97. package/crates/mint-mod-runtime/src/lib.rs +37 -23
  98. package/out/web/assets/ArtifactPreviewPanel-Cx6BVOOF.js +2 -0
  99. package/out/web/assets/CalculationCard-D2E74u5q.js +1 -0
  100. package/out/web/assets/CliSessionsView-ZbdtSjvK.css +1 -0
  101. package/out/web/assets/{CliSessionsView-MI8kXMAb.js → CliSessionsView-z5i6qGyb.js} +1 -1
  102. package/out/web/assets/CodeReviewPage-BK2bOScd.css +1 -0
  103. package/out/web/assets/CodeReviewPage-Dp6zHuHu.js +2 -0
  104. package/out/web/assets/{CommandPalette-BD8SlTE5.js → CommandPalette-DaKlzbKs.js} +1 -1
  105. package/out/web/assets/ImageGenCard-BMCJHP9O.js +1 -0
  106. package/out/web/assets/ImageSearchCard-Cld_SOUg.js +1 -0
  107. package/out/web/assets/{ImageStudioPanel-BP7N4rLr.js → ImageStudioPanel-B5N7TjeB.js} +1 -1
  108. package/out/web/assets/LibraryPage-CMB89rJ0.css +1 -0
  109. package/out/web/assets/LibraryPage-DbZEi4W_.js +2 -0
  110. package/out/web/assets/LinkedFoldersView-9B9DIPoZ.js +1 -0
  111. package/out/web/assets/{McpRegistryPicker-CoS83vtc.js → McpRegistryPicker-DnXPlzI7.js} +1 -1
  112. package/out/web/assets/McpServersView-CiNBNUl4.js +1 -0
  113. package/out/web/assets/MermaidCard-BBDNEXbY.js +2 -0
  114. package/out/web/assets/MintDashboard-C_UYPoNV.js +119 -0
  115. package/out/web/assets/MintDashboard-DTd8C_c8.css +1 -0
  116. package/out/web/assets/PlanCard-C2x5pajq.js +1 -0
  117. package/out/web/assets/{PluginsView-jBYmcKh4.js → PluginsView-DoTg9MVa.js} +1 -1
  118. package/out/web/assets/RewindModal-B5oowrR0.js +1 -0
  119. package/out/web/assets/{ScheduledTasksView-5uw9-V9H.js → ScheduledTasksView-BD5DVPkZ.js} +1 -1
  120. package/out/web/assets/{SelectField-DXBQtrUK.js → SelectField-Cy6CghrU.js} +1 -1
  121. package/out/web/assets/SettingsWindow-BYl_oCFh.js +4 -0
  122. package/out/web/assets/SettingsWindow-Ckcgnjt8.css +1 -0
  123. package/out/web/assets/{SkillsView-C-gp9J7m.js → SkillsView-CIeWwq5z.js} +1 -1
  124. package/out/web/assets/StockCard-DxUciKyp.js +1 -0
  125. package/out/web/assets/ToolSurfacePage-qBgs_VHq.css +1 -0
  126. package/out/web/assets/ToolSurfacePage-saVbVrkE.js +38 -0
  127. package/out/web/assets/{UiFeatureCard-6BwxTOCe.js → UiFeatureCard-CdY0hGWy.js} +1 -1
  128. package/out/web/assets/{UiGridCard-DvZcLNxb.js → UiGridCard-BBK3I5hC.js} +1 -1
  129. package/out/web/assets/UiMockupWidget-CpR9EDuc.js +1 -0
  130. package/out/web/assets/{VeoStudioPanel-DOj7_WR3.css → VeoStudioPanel-9kU9YLEN.css} +1 -1
  131. package/out/web/assets/{VeoStudioPanel-DfsOJKv0.js → VeoStudioPanel-Co6-wEWM.js} +2 -2
  132. package/out/web/assets/WeatherCard-Buzi7Vfu.js +1 -0
  133. package/out/web/assets/WorkspacePdfPreview-Bi8Zzyir.js +2 -0
  134. package/out/web/assets/{abnfDiagram-VCTEODGH-BOo3Ckng.js → abnfDiagram-VCTEODGH-CMfR95N2.js} +1 -1
  135. package/out/web/assets/{architecture-7GRP2DOG-ZOwheW3n.js → architecture-7GRP2DOG-DhAM4HyO.js} +1 -1
  136. package/out/web/assets/{architectureDiagram-5GKGNRK7-DC2FDzVd.js → architectureDiagram-5GKGNRK7-Yk3TUJ_F.js} +1 -1
  137. package/out/web/assets/{blockDiagram-I7D4REHJ-BrKFWdAH.js → blockDiagram-I7D4REHJ-X9fBgJik.js} +1 -1
  138. package/out/web/assets/{c4Diagram-7LVT6UL2-BHTHK1Yd.js → c4Diagram-7LVT6UL2-Bk9x7N-8.js} +1 -1
  139. package/out/web/assets/{chunk-4HAMMTFA-BYFkZwNj.js → chunk-4HAMMTFA-Dx5fk1_0.js} +1 -1
  140. package/out/web/assets/{chunk-75Z2AOVW-BSKfSKSA.js → chunk-75Z2AOVW-BszTQoM4.js} +1 -1
  141. package/out/web/assets/{chunk-DU6HZSFF-vIhsiW6n.js → chunk-DU6HZSFF-DK-As3rk.js} +1 -1
  142. package/out/web/assets/{chunk-F27PBJKO-BlYpW9xq.js → chunk-F27PBJKO-LmncZZIS.js} +1 -1
  143. package/out/web/assets/{chunk-GMAD6QVW-BIBz_Ug5.js → chunk-GMAD6QVW-DSQ_sDRJ.js} +1 -1
  144. package/out/web/assets/{chunk-GVQU2GXP-NPSHxeqC.js → chunk-GVQU2GXP-Cno3ulje.js} +1 -1
  145. package/out/web/assets/{chunk-IMKFNOWR-YlJK8_Fp.js → chunk-IMKFNOWR-bIzfY9Jc.js} +1 -1
  146. package/out/web/assets/{chunk-L3NEJ4N5-DMYi4-af.js → chunk-L3NEJ4N5-Dl6eOa0D.js} +1 -1
  147. package/out/web/assets/{chunk-OSK3NFVY-Begd3-jA.js → chunk-OSK3NFVY-BaUGJ-cM.js} +1 -1
  148. package/out/web/assets/{chunk-P2QGCYS3-D1I4xFHN.js → chunk-P2QGCYS3-_066ndhH.js} +1 -1
  149. package/out/web/assets/{chunk-POPQ4Y6H-CafZZzNE.js → chunk-POPQ4Y6H-YT1AKhTv.js} +1 -1
  150. package/out/web/assets/{chunk-PWAF6VOD-Dwh6_Lnt.js → chunk-PWAF6VOD-CrEgDsxh.js} +1 -1
  151. package/out/web/assets/{chunk-SHT3W25Y-B0XrCSEb.js → chunk-SHT3W25Y-_KwaRLOl.js} +1 -1
  152. package/out/web/assets/{chunk-SVP7TREG-CxLsddxC.js → chunk-SVP7TREG-BQ1qb32L.js} +1 -1
  153. package/out/web/assets/{chunk-TICWLB2K-y156QInb.js → chunk-TICWLB2K-BDyehCtg.js} +1 -1
  154. package/out/web/assets/classDiagram-ZZMXUADV-BoSpzRCJ.js +1 -0
  155. package/out/web/assets/classDiagram-v2-VYDZK3BY-BoSpzRCJ.js +1 -0
  156. package/out/web/assets/{config-DqXJXeZM.css → config-2suVFPZg.css} +1 -1
  157. package/out/web/assets/config-CkRCdkxn.js +2 -0
  158. package/out/web/assets/{cynefin-OW5HDTMX-SMVvrLf-.js → cynefin-OW5HDTMX-Dl_31aaN.js} +1 -1
  159. package/out/web/assets/{cynefinDiagram-5FMLGOSQ-rZznN24T.js → cynefinDiagram-5FMLGOSQ-Bpir8jha.js} +1 -1
  160. package/out/web/assets/{dagre-GXQ25YYZ-BOToydFu.js → dagre-GXQ25YYZ-D9xu3nsq.js} +1 -1
  161. package/out/web/assets/{diagram-S7CK7UJ4-TMVaGQEJ.js → diagram-S7CK7UJ4-DeveoEeB.js} +1 -1
  162. package/out/web/assets/{diagram-UQ7AKVKN-C2JK-GP2.js → diagram-UQ7AKVKN-p4xWRESV.js} +1 -1
  163. package/out/web/assets/{diagram-VSXAHHWV-Cw8VSi2K.js → diagram-VSXAHHWV-C7qz9NDF.js} +1 -1
  164. package/out/web/assets/{diagram-VX7I27RA-DXp4fKk9.js → diagram-VX7I27RA-DRiCa3Vd.js} +1 -1
  165. package/out/web/assets/{diagram-Z3DM3KII-CzSicYiW.js → diagram-Z3DM3KII-Bt7zduQX.js} +1 -1
  166. package/out/web/assets/{ebnfDiagram-PWID7BFC-Dqj-zqPu.js → ebnfDiagram-PWID7BFC-DV9WpwdR.js} +1 -1
  167. package/out/web/assets/{erDiagram-RLTQ6QDP-DZwFZAuD.js → erDiagram-RLTQ6QDP-BJ_nwG2m.js} +1 -1
  168. package/out/web/assets/{eventmodeling-NTZA5JFV-C0O-Lvgp.js → eventmodeling-NTZA5JFV-BTBMrfVa.js} +1 -1
  169. package/out/web/assets/flowDiagram-HODETNUW-Cdlq3cJL.js +1 -0
  170. package/out/web/assets/{ganttDiagram-EL5Y4UJY-Da4ULXTl.js → ganttDiagram-EL5Y4UJY-DQLdsXrC.js} +1 -1
  171. package/out/web/assets/{gitGraph-4MIJSDKK-B9wNFZIP.js → gitGraph-4MIJSDKK-CL1OEOQ_.js} +1 -1
  172. package/out/web/assets/{gitGraphDiagram-WWUBYQGX-RjrhB0MP.js → gitGraphDiagram-WWUBYQGX-8cSi42H7.js} +1 -1
  173. package/out/web/assets/index-BocxnrDG.js +168 -0
  174. package/out/web/assets/index-yeh_r55a.css +1 -0
  175. package/out/web/assets/{info-A6RAGUB7-B3dfCpeY.js → info-A6RAGUB7-C1zsX5sc.js} +1 -1
  176. package/out/web/assets/{infoDiagram-27XIBGKW-QVWkLVx7.js → infoDiagram-27XIBGKW-DsRYtIyI.js} +1 -1
  177. package/out/web/assets/{ishikawaDiagram-5VMMS53U-CLFNIRI9.js → ishikawaDiagram-5VMMS53U-B2crwIXj.js} +1 -1
  178. package/out/web/assets/{journeyDiagram-3NMN7TZE-fgOQadYF.js → journeyDiagram-3NMN7TZE-BTH-GyVR.js} +1 -1
  179. package/out/web/assets/{kanban-definition-UXKFOSKX-G3Q_9g9O.js → kanban-definition-UXKFOSKX-BeRfCnuA.js} +1 -1
  180. package/out/web/assets/{mermaid-parser.core-v3Jm7nco.js → mermaid-parser.core-BJwgCBBc.js} +3 -3
  181. package/out/web/assets/{mermaid.core-DTEjLUA1.js → mermaid.core-CsDDodN8.js} +4 -4
  182. package/out/web/assets/{mindmap-definition-YA3MSWOX-C_D42_fF.js → mindmap-definition-YA3MSWOX-B42efOQ1.js} +1 -1
  183. package/out/web/assets/{modelManager-BPLycQrN.js → modelManager-B3ZFMh8v.js} +2 -2
  184. package/out/web/assets/{oauthManager-BUK7ic7q.js → oauthManager-Ch5Du2fF.js} +1 -1
  185. package/out/web/assets/{packet-AYTQ26CC-B-R-SHlF.js → packet-AYTQ26CC-B6xUyE6r.js} +1 -1
  186. package/out/web/assets/{pegDiagram-XKGWAZYB-Da2q_giz.js → pegDiagram-XKGWAZYB-1LabVycL.js} +1 -1
  187. package/out/web/assets/{pie-WAS4IAKB-BC8F0N7-.js → pie-WAS4IAKB-DJwo-nf_.js} +1 -1
  188. package/out/web/assets/{pieDiagram-E7YTZNPT-Iy9REJNC.js → pieDiagram-E7YTZNPT-icmAqzy4.js} +1 -1
  189. package/out/web/assets/{quadrantDiagram-AXDQQJYC-ztsiYw7h.js → quadrantDiagram-AXDQQJYC-CZ-wEx41.js} +1 -1
  190. package/out/web/assets/{radar-RG4KPBEZ-B_1P8Z_z.js → radar-RG4KPBEZ-D7d-Xriw.js} +1 -1
  191. package/out/web/assets/{railroad-74A4TZTK-DuTqczSo.js → railroad-74A4TZTK-viuhdYbs.js} +1 -1
  192. package/out/web/assets/{railroad-abnf-HS5TGJTU-Df29WLmp.js → railroad-abnf-HS5TGJTU-DNXiCNea.js} +1 -1
  193. package/out/web/assets/{railroad-ebnf-LZEXJU2U-ePFMo-qp.js → railroad-ebnf-LZEXJU2U-nkSu1-0C.js} +1 -1
  194. package/out/web/assets/{railroad-peg-WCYAUIDC-DYXLW4_E.js → railroad-peg-WCYAUIDC-BhbMeQzz.js} +1 -1
  195. package/out/web/assets/{railroadDiagram-O6MQD6OU-LStnZf2S.js → railroadDiagram-O6MQD6OU-CLpG1jUX.js} +1 -1
  196. package/out/web/assets/{requirementDiagram-BXWQKSXE-DmzonMH-.js → requirementDiagram-BXWQKSXE-kRDrvWrc.js} +1 -1
  197. package/out/web/assets/{sankeyDiagram-P5KCCOFB-B8vjmUU1.js → sankeyDiagram-P5KCCOFB-DxAwmq4x.js} +1 -1
  198. package/out/web/assets/{sequenceDiagram-WJ2MYXX4-CTnplOAX.js → sequenceDiagram-WJ2MYXX4-aLgY-6Wz.js} +1 -1
  199. package/out/web/assets/{stateDiagram-D77RDMKH-BFrIaLdI.js → stateDiagram-D77RDMKH-D5QWfOhp.js} +1 -1
  200. package/out/web/assets/stateDiagram-v2-MP3YSRHH-D6OiZoAP.js +1 -0
  201. package/out/web/assets/{swimlanes-42K2YHIH-BXGngUNU.js → swimlanes-42K2YHIH-CRs5UyA-.js} +1 -1
  202. package/out/web/assets/swimlanesDiagram-VR7AAH4N-UgEXIsV0.js +8 -0
  203. package/out/web/assets/syntaxHighlight-C-wAGT84.js +3 -0
  204. package/out/web/assets/{timeline-definition-24CTP7MA-CegH7n43.js → timeline-definition-24CTP7MA-Denqd9qb.js} +1 -1
  205. package/out/web/assets/{treeView-Q6P3EWNA-DTpJv2Tw.js → treeView-Q6P3EWNA-0my3Z3s4.js} +1 -1
  206. package/out/web/assets/{treemap-WGGIJYW6-i8M9iJU6.js → treemap-WGGIJYW6-B_EjkLYq.js} +1 -1
  207. package/out/web/assets/{vennDiagram-4TSXK5OY-Dpzo1S8U.js → vennDiagram-4TSXK5OY-DxVg3Ewr.js} +1 -1
  208. package/out/web/assets/{wardley-WFR3VGLG-BrNsgtfw.js → wardley-WFR3VGLG-CGD-Tj4p.js} +1 -1
  209. package/out/web/assets/{wardleyDiagram-VM6X3IG4-lQ92LFE1.js → wardleyDiagram-VM6X3IG4-CmfS39Xm.js} +1 -1
  210. package/out/web/assets/{xychartDiagram-S5SC5T6Z-BaOaEoBd.js → xychartDiagram-S5SC5T6Z-BVM9bKis.js} +1 -1
  211. package/out/web/index-web.html +2 -2
  212. package/out/web/index.html +2 -2
  213. package/package.json +2 -2
  214. package/shared-modules/CODEOWNERS +3 -3
  215. package/shared-modules/SDL2/SDL2_mixer.json +27 -0
  216. package/shared-modules/appstream/README.md +13 -0
  217. package/shared-modules/appstream/appstream-compose.json +111 -0
  218. package/shared-modules/dbus-glib/dbus-glib.json +2 -2
  219. package/shared-modules/libportal/libportal.json +23 -0
  220. package/shared-modules/linux-audio/fluidsynth2-static.json +2 -2
  221. package/shared-modules/linux-audio/fluidsynth2.json +2 -2
  222. package/shared-modules/linux-audio/libinstpatch.json +2 -2
  223. package/shared-modules/linux-audio/liblo-static.json +3 -0
  224. package/shared-modules/luajit/luajit.json +2 -2
  225. package/shared-modules/squashfs-tools/squashfs-tools.json +0 -3
  226. package/slash-commands.json +7 -2
  227. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  228. package/src/renderer/shared/agentProgress.ts +51 -3
  229. package/src/renderer/shared/components/Settings/AutomationTab.tsx +2 -56
  230. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +15 -1
  231. package/src/renderer/shared/components/Settings/GeneralTab.tsx +25 -12
  232. package/src/renderer/shared/components/Settings/ThemeTab.test.tsx +45 -0
  233. package/src/renderer/shared/components/Settings/ThemeTab.tsx +106 -3
  234. package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +67 -17
  235. package/src/renderer/shared/components/activity/AgentActivityTable.tsx +376 -134
  236. package/src/renderer/shared/components/activity/AskUserActivityCard.test.tsx +122 -0
  237. package/src/renderer/shared/components/activity/AskUserActivityCard.tsx +70 -0
  238. package/src/renderer/shared/components/activity/McpToolResults.tsx +39 -2
  239. package/src/renderer/shared/components/activity/SubagentDagView.tsx +7 -7
  240. package/src/renderer/shared/components/activity/ViewedImages.tsx +41 -2
  241. package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +3 -3
  242. package/src/renderer/shared/components/app/CommandPalette.tsx +7 -7
  243. package/src/renderer/shared/components/app/DesktopTitlebar.tsx +2 -2
  244. package/src/renderer/shared/components/app/MintDashboard.tsx +27 -17
  245. package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +3 -3
  246. package/src/renderer/shared/components/cards/CalculationCard.tsx +3 -3
  247. package/src/renderer/shared/components/cards/ImageGenCard.tsx +2 -2
  248. package/src/renderer/shared/components/cards/ImageSearchCard.tsx +2 -2
  249. package/src/renderer/shared/components/cards/MermaidCard.tsx +1 -1
  250. package/src/renderer/shared/components/cards/PlanCard.test.tsx +41 -0
  251. package/src/renderer/shared/components/cards/PlanCard.tsx +32 -0
  252. package/src/renderer/shared/components/cards/ReleaseNotesCard.test.tsx +29 -0
  253. package/src/renderer/shared/components/cards/ReleaseNotesCard.tsx +137 -0
  254. package/src/renderer/shared/components/cards/StockCard.tsx +7 -7
  255. package/src/renderer/shared/components/cards/ToolResultCards.tsx +11 -3
  256. package/src/renderer/shared/components/cards/UiMockupWidget.tsx +4 -4
  257. package/src/renderer/shared/components/cards/WeatherCard.tsx +5 -5
  258. package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +11 -0
  259. package/src/renderer/shared/components/chat/ApprovalCard.tsx +21 -2
  260. package/src/renderer/shared/components/chat/ArtifactCards.test.tsx +69 -0
  261. package/src/renderer/shared/components/chat/ArtifactCards.tsx +238 -0
  262. package/src/renderer/shared/components/chat/AttachedDocumentCard.test.tsx +39 -0
  263. package/src/renderer/shared/components/chat/AttachedDocumentCard.tsx +220 -0
  264. package/src/renderer/shared/components/chat/ChatCodeBlock.test.tsx +33 -0
  265. package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +15 -1
  266. package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +40 -2
  267. package/src/renderer/shared/components/chat/ChatMessageItem.tsx +180 -34
  268. package/src/renderer/shared/components/chat/ChatModeSelector.css +227 -0
  269. package/src/renderer/shared/components/chat/ChatModeSelector.test.tsx +45 -0
  270. package/src/renderer/shared/components/chat/ChatModeSelector.tsx +198 -0
  271. package/src/renderer/shared/components/chat/ChatPanel.test.tsx +22 -0
  272. package/src/renderer/shared/components/chat/ChatPanel.tsx +227 -53
  273. package/src/renderer/shared/components/chat/DiffReviewModal.tsx +6 -6
  274. package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +142 -1
  275. package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +45 -3
  276. package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +42 -14
  277. package/src/renderer/shared/components/chat/RewindModal.tsx +10 -10
  278. package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +14 -0
  279. package/src/renderer/shared/components/chat/SourcesBlock.test.tsx +53 -0
  280. package/src/renderer/shared/components/chat/SourcesBlock.tsx +236 -90
  281. package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +45 -1
  282. package/src/renderer/shared/components/chat/TurnFileChanges.tsx +202 -169
  283. package/src/renderer/shared/components/extensions/CliSessionsView.tsx +5 -4
  284. package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +3 -3
  285. package/src/renderer/shared/components/studio/LibraryPage.tsx +48 -5
  286. package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +125 -13
  287. package/src/renderer/shared/components/workspace/CodeReviewPage.test.tsx +50 -0
  288. package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +77 -16
  289. package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +78 -22
  290. package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +13 -0
  291. package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +48 -28
  292. package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +266 -47
  293. package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +18 -6
  294. package/src/renderer/shared/components/workspace/background-terminals.css +11 -11
  295. package/src/renderer/shared/constants/config.ts +1 -5
  296. package/src/renderer/shared/constants/models.test.ts +51 -0
  297. package/src/renderer/shared/constants/models.ts +139 -32
  298. package/src/renderer/shared/conversation/lifecycle.test.ts +91 -3
  299. package/src/renderer/shared/conversation/lifecycle.ts +104 -5
  300. package/src/renderer/shared/conversation/progressiveStream.test.ts +83 -0
  301. package/src/renderer/shared/conversation/progressiveStream.ts +219 -0
  302. package/src/renderer/shared/css/agent-observability.css +133 -0
  303. package/src/renderer/shared/css/chat-ui-cards.css +13 -13
  304. package/src/renderer/shared/css/code-review.css +63 -34
  305. package/src/renderer/shared/css/code-sessions.css +47 -18
  306. package/src/renderer/shared/css/collapsed-paste.css +1 -1
  307. package/src/renderer/shared/css/error-notice.css +1 -1
  308. package/src/renderer/shared/css/file-changes.css +165 -80
  309. package/src/renderer/shared/css/library.css +105 -10
  310. package/src/renderer/shared/css/management-views.css +1 -1
  311. package/src/renderer/shared/css/mcp-results.css +5 -5
  312. package/src/renderer/shared/css/model-selector.css +18 -18
  313. package/src/renderer/shared/css/mods.css +7 -7
  314. package/src/renderer/shared/css/notification-notice.css +2 -2
  315. package/src/renderer/shared/css/queued-messages.css +5 -5
  316. package/src/renderer/shared/css/settings/general.css +18 -13
  317. package/src/renderer/shared/css/settings/profile.css +5 -5
  318. package/src/renderer/shared/css/settings/theme.css +5 -5
  319. package/src/renderer/shared/css/slash-choice.css +2 -2
  320. package/src/renderer/shared/css/slash-suggestions.css +3 -3
  321. package/src/renderer/shared/css/sources-block.css +41 -2
  322. package/src/renderer/shared/css/tools-panel.css +2 -2
  323. package/src/renderer/shared/css/ui-tailwind.css +8 -0
  324. package/src/renderer/shared/css/veo-studio.css +2 -2
  325. package/src/renderer/shared/css/viewed-images.css +7 -7
  326. package/src/renderer/shared/platform.ts +2 -1
  327. package/src/renderer/shared/theme/themeManager.test.ts +17 -0
  328. package/src/renderer/shared/theme/themeManager.ts +21 -4
  329. package/src/renderer/shared/theme/themes.ts +54 -0
  330. package/src/renderer/shared/theme/tokens.css +6 -4
  331. package/src/renderer/shared/types.ts +1 -1
  332. package/src/renderer/shared/utils/agentActivity.test.ts +468 -2
  333. package/src/renderer/shared/utils/agentActivity.ts +602 -35
  334. package/src/renderer/shared/utils/artifactBrowser.test.ts +37 -32
  335. package/src/renderer/shared/utils/artifactBrowser.ts +17 -19
  336. package/src/renderer/shared/utils/attachedDocument.test.ts +73 -0
  337. package/src/renderer/shared/utils/attachedDocument.ts +49 -0
  338. package/src/renderer/shared/utils/fileIcons.test.ts +59 -0
  339. package/src/renderer/shared/utils/fileIcons.ts +149 -16
  340. package/src/renderer/shared/utils/markdown.tsx +72 -19
  341. package/src/renderer/shared/utils/markdown.webImages.test.tsx +16 -0
  342. package/src/renderer/shared/utils/mcpResults.test.tsx +29 -0
  343. package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
  344. package/src/renderer/shared/utils/syntaxHighlight.test.tsx +11 -1
  345. package/src/renderer/shared/utils/syntaxHighlight.tsx +51 -3
  346. package/src/renderer/shared/utils/useGeminiLiveVoice.ts +324 -51
  347. package/src/renderer/shared/utils/workspaceDrag.test.ts +153 -0
  348. package/src/renderer/shared/utils/workspaceDrag.ts +142 -0
  349. package/src/renderer/shared/utils/workspaceMedia.ts +10 -2
  350. package/src/renderer/src/App.tsx +14 -8
  351. package/src/renderer/src/agentProgress.ts +1 -0
  352. package/src/renderer/src/components/ScreenPicker.css +1 -1
  353. package/src/renderer/src/components/WorkspacePanel.tsx +6 -5
  354. package/src/renderer/src/css/app-shell.css +4 -4
  355. package/src/renderer/src/css/chat.css +1261 -241
  356. package/src/renderer/src/css/desktop-tools.css +11 -11
  357. package/src/renderer/src/css/sidebar-workspace.css +2 -2
  358. package/src/renderer/src/css/tool-surfaces.css +12 -12
  359. package/src/renderer/src/index.css +38 -20
  360. package/src/renderer/src/tauri.ts +22 -2
  361. package/src/renderer/src-web/App.tsx +14 -0
  362. package/src/renderer/src-web/agentProgress.ts +1 -0
  363. package/src/renderer/src-web/css/app-shell.css +4 -4
  364. package/src/renderer/src-web/css/chat.css +1278 -190
  365. package/src/renderer/src-web/css/mobile.css +29 -13
  366. package/src/renderer/src-web/css/settings/mobile.css +2 -2
  367. package/src/renderer/src-web/css/sidebar-workspace.css +1 -1
  368. package/src/renderer/src-web/index.css +38 -20
  369. package/src/renderer/src-web/tauri.ts +22 -2
  370. package/src/renderer/tests/browserArtifacts.browser.tsx +1 -1
  371. package/src/renderer/tests/library.browser.tsx +3 -0
  372. package/src/renderer/tests/liveVoice.browser.html +1 -0
  373. package/src/renderer/tests/workspaceDrop.browser.tsx +1 -1
  374. package/src-tauri/src/app/lifecycle.rs +1 -14
  375. package/src-tauri/src/background/headless.rs +14 -8
  376. package/src-tauri/src/commands/conversation.rs +38 -0
  377. package/src-tauri/src/native/mod.rs +0 -1
  378. package/src-tauri/src/native/system.rs +4 -47
  379. package/crates/mint-cli/src/interactive/chat_tui.rs +0 -8855
  380. package/crates/mint-core/src/agent/chat.rs +0 -4394
  381. package/crates/mint-core/src/browser/lifecycle.rs +0 -291
  382. package/crates/mint-core/src/browser/logging.rs +0 -20
  383. package/crates/mint-core/src/orchestration/tools/browser.rs +0 -31
  384. package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +0 -2
  385. package/out/web/assets/CalculationCard-CRjP3E1j.js +0 -1
  386. package/out/web/assets/CliSessionsView-C3Br2v5Y.css +0 -1
  387. package/out/web/assets/CodeReviewPage-CUJceQme.css +0 -1
  388. package/out/web/assets/CodeReviewPage-VCUgPla_.js +0 -2
  389. package/out/web/assets/ImageGenCard-BCXxkVJX.js +0 -1
  390. package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +0 -1
  391. package/out/web/assets/LibraryPage-DAxUTAg7.css +0 -1
  392. package/out/web/assets/LibraryPage-WlviRdR8.js +0 -2
  393. package/out/web/assets/LinkedFoldersView-CKRgroWK.js +0 -1
  394. package/out/web/assets/McpServersView-D3wHnuHg.js +0 -1
  395. package/out/web/assets/MermaidCard-NqrHdyJW.js +0 -2
  396. package/out/web/assets/MintDashboard-B2DJsDDO.css +0 -1
  397. package/out/web/assets/MintDashboard-COw_xol7.js +0 -63
  398. package/out/web/assets/RewindModal-DempuDod.js +0 -1
  399. package/out/web/assets/SettingsWindow-2YP3yj7s.js +0 -4
  400. package/out/web/assets/SettingsWindow-Dz8LQVgD.css +0 -1
  401. package/out/web/assets/StockCard-BDMCe7mx.js +0 -1
  402. package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +0 -38
  403. package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +0 -1
  404. package/out/web/assets/UiMockupWidget-DD73QWg1.js +0 -1
  405. package/out/web/assets/WeatherCard-CPucIfq8.js +0 -1
  406. package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +0 -2
  407. package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +0 -1
  408. package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +0 -1
  409. package/out/web/assets/config-CvIO5hkn.js +0 -2
  410. package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +0 -1
  411. package/out/web/assets/index-BmpulIkg.js +0 -168
  412. package/out/web/assets/index-CkvBwhX_.css +0 -1
  413. package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +0 -1
  414. package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +0 -8
  415. package/shared-modules/libsecret/libsecret.json +0 -23
  416. package/shared-modules/libusb/libusb.json +0 -27
  417. package/shared-modules/lzo/lzo.json +0 -27
  418. package/src/renderer/src/components/BrowserShell.tsx +0 -108
  419. package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +0 -91
  420. package/src/renderer/src/components/browser-shell.css +0 -185
  421. package/src-tauri/src/native/browser.rs +0 -252
@@ -4,17 +4,198 @@ export type GeminiLiveEvent =
4
4
  | { type: 'audioChunk'; data: string }
5
5
  | { type: 'transcript'; role: string; text: string }
6
6
  | { type: 'turnComplete' }
7
+ | { type: 'interrupted' }
7
8
  | { type: 'toolStatus'; action: string; input: unknown; result?: string | null; error?: string | null }
9
+ | { type: 'approvalRequested'; token: string; approval: Record<string, unknown> }
8
10
  | { type: 'error'; message: string }
9
11
  | { type: 'closed' }
10
12
 
13
+ export interface GeminiLiveSideEffectCallbacks {
14
+ onTurnSaved?: () => void
15
+ onToolActivity?: () => void
16
+ onApprovalRequested?: (approval: { token: string; approval: Record<string, unknown> }) => void
17
+ }
18
+
19
+ export function routeGeminiLiveSideEffects(
20
+ event: GeminiLiveEvent,
21
+ callbacks: GeminiLiveSideEffectCallbacks,
22
+ ) {
23
+ switch (event.type) {
24
+ case 'turnComplete':
25
+ case 'interrupted':
26
+ case 'closed':
27
+ callbacks.onTurnSaved?.()
28
+ break
29
+ case 'toolStatus':
30
+ callbacks.onToolActivity?.()
31
+ break
32
+ case 'approvalRequested':
33
+ callbacks.onApprovalRequested?.({ token: event.token, approval: event.approval })
34
+ break
35
+ }
36
+ }
37
+
38
+ export interface StoppableAudioSource {
39
+ stop: () => void
40
+ disconnect: () => void
41
+ }
42
+
43
+ export interface LiveTranscriptFrameState {
44
+ userTranscript: string
45
+ assistantTranscript: string
46
+ userFresh: boolean
47
+ assistantFresh: boolean
48
+ }
49
+
50
+ export function applyLiveTranscriptFragment(
51
+ state: LiveTranscriptFrameState,
52
+ role: string,
53
+ text: string,
54
+ ) {
55
+ if (role === 'user') {
56
+ state.userTranscript = state.userFresh ? text : state.userTranscript + text
57
+ state.userFresh = false
58
+ } else {
59
+ state.assistantTranscript = state.assistantFresh ? text : state.assistantTranscript + text
60
+ state.assistantFresh = false
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Stops and disconnects all scheduled/playing audio buffer sources immediately,
66
+ * resetting the playback timeline cursor and speaking indicator. Used both on
67
+ * user barge-in (`interrupted`) and when ending a Live session.
68
+ */
69
+ export function stopQueuedAudioPlayback(
70
+ sources: Set<StoppableAudioSource>,
71
+ nextPlaybackTimeRef: { current: number },
72
+ speakingTimeoutRef: { current: number | null },
73
+ setIsSpeaking: (speaking: boolean) => void,
74
+ ) {
75
+ for (const source of sources) {
76
+ try {
77
+ source.stop()
78
+ source.disconnect()
79
+ } catch {
80
+ /* Already ended. */
81
+ }
82
+ }
83
+ sources.clear()
84
+ nextPlaybackTimeRef.current = 0
85
+ setIsSpeaking(false)
86
+ if (speakingTimeoutRef.current !== null && typeof window !== 'undefined') {
87
+ window.clearTimeout(speakingTimeoutRef.current)
88
+ speakingTimeoutRef.current = null
89
+ }
90
+ }
91
+
11
92
  const MIC_SAMPLE_RATE = 16000
12
93
  const PLAYBACK_SAMPLE_RATE = 24000
13
- // ScriptProcessorNode buffer size: small enough for low latency, large enough to avoid
14
- // excessive send-chunk traffic to the Rust side.
94
+ // Capture chunk size (~128ms at 16kHz, ~42ms at 48kHz): small enough for low latency,
95
+ // large enough to avoid excessive send-chunk traffic to the Rust side.
15
96
  const CAPTURE_BUFFER_SIZE = 2048
16
97
 
17
- export interface GeminiLiveVoiceOptions {
98
+ function floatToPcm16Sample(sample: number): number {
99
+ const clamped = Math.max(-1, Math.min(1, sample))
100
+ return clamped < 0
101
+ ? Math.max(-32768, Math.round(clamped * 32768))
102
+ : Math.min(32767, Math.round(clamped * 32768))
103
+ }
104
+
105
+ /**
106
+ * Downsamples Float32 mono audio to 16kHz PCM16 using a continuous time-weighted
107
+ * boxcar anti-aliasing filter (low-pass average over each output sample window)
108
+ * instead of nearest-neighbor point sampling, preventing high-frequency aliasing.
109
+ */
110
+ export function downsampleToPcm16(
111
+ input: Float32Array,
112
+ inputSampleRate: number,
113
+ targetSampleRate: number = MIC_SAMPLE_RATE,
114
+ ): Int16Array {
115
+ if (input.length === 0 || inputSampleRate <= 0 || targetSampleRate <= 0) {
116
+ return new Int16Array(0)
117
+ }
118
+ if (inputSampleRate === targetSampleRate) {
119
+ const out = new Int16Array(input.length)
120
+ for (let i = 0; i < input.length; i += 1) {
121
+ out[i] = floatToPcm16Sample(input[i])
122
+ }
123
+ return out
124
+ }
125
+ const ratio = inputSampleRate / targetSampleRate
126
+ const outLength = Math.floor(input.length / ratio)
127
+ const out = new Int16Array(outLength)
128
+ for (let i = 0; i < outLength; i += 1) {
129
+ const start = i * ratio
130
+ const end = (i + 1) * ratio
131
+ const firstIdx = Math.floor(start)
132
+ const lastIdx = Math.floor(end)
133
+ let avg = 0
134
+ if (firstIdx === lastIdx) {
135
+ avg = input[firstIdx] ?? 0
136
+ } else {
137
+ let sum = 0
138
+ let weight = 0
139
+ const wLeft = firstIdx + 1 - start
140
+ sum += (input[firstIdx] ?? 0) * wLeft
141
+ weight += wLeft
142
+ const interiorEnd = Math.min(lastIdx, input.length)
143
+ for (let k = firstIdx + 1; k < interiorEnd; k += 1) {
144
+ sum += input[k]
145
+ weight += 1
146
+ }
147
+ if (lastIdx < input.length && end > lastIdx) {
148
+ const wRight = end - lastIdx
149
+ sum += input[lastIdx] * wRight
150
+ weight += wRight
151
+ }
152
+ avg = weight > 0 ? sum / weight : 0
153
+ }
154
+ out[i] = floatToPcm16Sample(avg)
155
+ }
156
+ return out
157
+ }
158
+
159
+ export function pcm16ToBase64(pcm: Int16Array): string {
160
+ const bytes = new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength)
161
+ let binary = ''
162
+ for (let i = 0; i < bytes.length; i += 1) {
163
+ binary += String.fromCharCode(bytes[i])
164
+ }
165
+ return btoa(binary)
166
+ }
167
+
168
+ const WORKLET_PROCESSOR_NAME = 'mint-gemini-live-capture'
169
+ const WORKLET_SOURCE = `
170
+ class MintGeminiLiveCaptureProcessor extends AudioWorkletProcessor {
171
+ constructor() {
172
+ super();
173
+ this._buffer = new Float32Array(${CAPTURE_BUFFER_SIZE});
174
+ this._offset = 0;
175
+ }
176
+ process(inputs) {
177
+ const channel = inputs[0] && inputs[0][0];
178
+ if (!channel || channel.length === 0) return true;
179
+ let read = 0;
180
+ while (read < channel.length) {
181
+ const space = this._buffer.length - this._offset;
182
+ const toCopy = Math.min(space, channel.length - read);
183
+ this._buffer.set(channel.subarray(read, read + toCopy), this._offset);
184
+ this._offset += toCopy;
185
+ read += toCopy;
186
+ if (this._offset >= this._buffer.length) {
187
+ const chunk = this._buffer.slice(0);
188
+ this._offset = 0;
189
+ this.port.postMessage(chunk, [chunk.buffer]);
190
+ }
191
+ }
192
+ return true;
193
+ }
194
+ }
195
+ registerProcessor('${WORKLET_PROCESSOR_NAME}', MintGeminiLiveCaptureProcessor);
196
+ `
197
+
198
+ export interface GeminiLiveVoiceOptions extends GeminiLiveSideEffectCallbacks {
18
199
  workspacePath?: string | null
19
200
  chatId?: string | null
20
201
  /** Starts a session and returns its id; `onEvent` fires for the session's lifetime. */
@@ -40,7 +221,16 @@ export interface GeminiLiveVoiceOptions {
40
221
  * duplex stream instead of discrete listen-then-send cycles — starting/stopping is
41
222
  * driven entirely by `setVoiceMode`.
42
223
  */
43
- export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAudioChunk, stopSession }: GeminiLiveVoiceOptions) {
224
+ export function useGeminiLiveVoice({
225
+ workspacePath,
226
+ chatId,
227
+ startSession,
228
+ sendAudioChunk,
229
+ stopSession,
230
+ onTurnSaved,
231
+ onToolActivity,
232
+ onApprovalRequested,
233
+ }: GeminiLiveVoiceOptions) {
44
234
  const [isRecording, setIsRecording] = useState(false)
45
235
  const [voiceMode, setVoiceMode] = useState(false)
46
236
  // Kept separate (rather than one shared line) so the user's own speech stays
@@ -62,14 +252,26 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
62
252
  const generationRef = useRef(0)
63
253
  const playbackSourcesRef = useRef(new Set<AudioBufferSourceNode>())
64
254
  const stopRef = useRef<() => void>(() => {})
255
+ const sideEffectsRef = useRef<GeminiLiveSideEffectCallbacks>({
256
+ onTurnSaved,
257
+ onToolActivity,
258
+ onApprovalRequested,
259
+ })
260
+ sideEffectsRef.current = { onTurnSaved, onToolActivity, onApprovalRequested }
65
261
  const micStreamRef = useRef<MediaStream | null>(null)
66
- const captureNodesRef = useRef<{ source: MediaStreamAudioSourceNode; processor: ScriptProcessorNode } | null>(null)
262
+ const captureNodesRef = useRef<{
263
+ source: MediaStreamAudioSourceNode
264
+ processor: AudioNode
265
+ workletPort?: MessagePort
266
+ } | null>(null)
67
267
  const nextPlaybackTimeRef = useRef(0)
68
268
  const speakingTimeoutRef = useRef<number | null>(null)
69
- // Gemini streams transcripts as small fragments, not whole lines — this tracks
70
- // whose fragment is currently being built so same-role fragments accumulate
71
- // into one line and a role change (or `turnComplete`) starts a fresh one.
72
- const transcriptRoleRef = useRef<string | null>(null)
269
+ // Gemini streams transcripts as small fragments, not whole lines — track
270
+ // freshness per role independently so interleaved user/assistant fragments
271
+ // within the same turn accumulate into their own lines without wiping each
272
+ // other out, and start fresh after `turnComplete` / `interrupted`.
273
+ const userTurnFreshRef = useRef(true)
274
+ const assistantTurnFreshRef = useRef(true)
73
275
 
74
276
  useEffect(() => {
75
277
  voiceModeRef.current = voiceMode
@@ -120,6 +322,15 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
120
322
  speakingTimeoutRef.current = window.setTimeout(() => setIsSpeaking(false), quietDelayMs)
121
323
  }, [ensureAudioContext])
122
324
 
325
+ const stopPlayback = useCallback(() => {
326
+ stopQueuedAudioPlayback(
327
+ playbackSourcesRef.current,
328
+ nextPlaybackTimeRef,
329
+ speakingTimeoutRef,
330
+ setIsSpeaking,
331
+ )
332
+ }, [])
333
+
123
334
  const handleEvent = useCallback((event: GeminiLiveEvent) => {
124
335
  switch (event.type) {
125
336
  case 'audioChunk':
@@ -127,23 +338,39 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
127
338
  playAudioChunk(event.data)
128
339
  break
129
340
  case 'transcript': {
130
- const setTranscript = event.role === 'user' ? setUserTranscript : setAssistantTranscript
131
- setTranscript((prev) => (transcriptRoleRef.current === event.role ? prev + event.text : event.text))
132
- transcriptRoleRef.current = event.role
341
+ if (event.role === 'user') {
342
+ const fresh = userTurnFreshRef.current
343
+ userTurnFreshRef.current = false
344
+ setUserTranscript((prev) => (fresh ? event.text : prev + event.text))
345
+ } else {
346
+ const fresh = assistantTurnFreshRef.current
347
+ assistantTurnFreshRef.current = false
348
+ setAssistantTranscript((prev) => (fresh ? event.text : prev + event.text))
349
+ }
133
350
  break
134
351
  }
135
352
  case 'turnComplete':
136
- // Leaves the finished line on screen — only resets which role the next
137
- // fragment continues, so the display doesn't flash blank between turns.
138
- transcriptRoleRef.current = null
353
+ // Leaves the finished line on screen — only resets per-role freshness
354
+ // so the display doesn't flash blank between turns.
355
+ userTurnFreshRef.current = true
356
+ assistantTurnFreshRef.current = true
357
+ break
358
+ case 'interrupted':
359
+ // User barged in while Mint was speaking — flush queued audio immediately.
360
+ stopPlayback()
361
+ userTurnFreshRef.current = true
362
+ assistantTurnFreshRef.current = true
139
363
  break
140
364
  case 'toolStatus':
141
- transcriptRoleRef.current = null
365
+ assistantTurnFreshRef.current = true
142
366
  setAssistantTranscript(event.error ? `${event.action} failed: ${event.error}` : `Running ${event.action}...`)
143
367
  break
368
+ case 'approvalRequested':
369
+ break
144
370
  case 'error':
145
371
  console.error('Gemini Live session error', event.message)
146
- transcriptRoleRef.current = null
372
+ userTurnFreshRef.current = true
373
+ assistantTurnFreshRef.current = true
147
374
  setAssistantTranscript(event.message)
148
375
  setErrorMessage(event.message)
149
376
  break
@@ -153,13 +380,19 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
153
380
  setVoiceAwaitingResponse(false)
154
381
  setIsSpeaking(false)
155
382
  sessionIdRef.current = null
156
- transcriptRoleRef.current = null
383
+ userTurnFreshRef.current = true
384
+ assistantTurnFreshRef.current = true
157
385
  setErrorMessage('Live connection closed')
158
386
  break
159
387
  }
160
- }, [playAudioChunk])
388
+ routeGeminiLiveSideEffects(event, sideEffectsRef.current)
389
+ }, [playAudioChunk, stopPlayback])
161
390
 
162
391
  const stopCapture = useCallback(() => {
392
+ if (captureNodesRef.current?.workletPort) {
393
+ captureNodesRef.current.workletPort.onmessage = null
394
+ captureNodesRef.current.workletPort.close?.()
395
+ }
163
396
  captureNodesRef.current?.processor.disconnect()
164
397
  captureNodesRef.current?.source.disconnect()
165
398
  captureNodesRef.current = null
@@ -170,10 +403,7 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
170
403
  const stop = useCallback(() => {
171
404
  ++generationRef.current
172
405
  stopCapture()
173
- for (const source of playbackSourcesRef.current) {
174
- try { source.stop(); source.disconnect() } catch { /* Already ended. */ }
175
- }
176
- playbackSourcesRef.current.clear()
406
+ stopPlayback()
177
407
  analyserRef.current?.disconnect()
178
408
  analyserRef.current = null
179
409
  const context = audioContextRef.current
@@ -185,14 +415,14 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
185
415
  setVoiceAwaitingResponse(false)
186
416
  setIsPaused(false)
187
417
  isPausedRef.current = false
188
- setIsSpeaking(false)
189
- if (speakingTimeoutRef.current !== null) {
190
- window.clearTimeout(speakingTimeoutRef.current)
191
- speakingTimeoutRef.current = null
418
+ if (sessionId) {
419
+ stopSession(sessionId)
420
+ .then(() => {
421
+ sideEffectsRef.current.onTurnSaved?.()
422
+ })
423
+ .catch((error) => console.error('Failed to stop Gemini Live session', error))
192
424
  }
193
- nextPlaybackTimeRef.current = 0
194
- if (sessionId) stopSession(sessionId).catch((error) => console.error('Failed to stop Gemini Live session', error))
195
- }, [stopCapture, stopSession])
425
+ }, [stopCapture, stopPlayback, stopSession])
196
426
  stopRef.current = stop
197
427
 
198
428
  const togglePause = useCallback(() => {
@@ -207,7 +437,8 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
207
437
  const generation = ++generationRef.current
208
438
  try {
209
439
  setErrorMessage('')
210
- transcriptRoleRef.current = null
440
+ userTurnFreshRef.current = true
441
+ assistantTurnFreshRef.current = true
211
442
  setUserTranscript('')
212
443
  setAssistantTranscript('')
213
444
  if (!navigator.mediaDevices?.getUserMedia) {
@@ -220,7 +451,13 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
220
451
  // Gemini as if the user said it — causing duplicated transcripts and Gemini
221
452
  // interrupting/looping on its own voice.
222
453
  const stream = await navigator.mediaDevices.getUserMedia({
223
- audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, autoGainControl: true }
454
+ audio: {
455
+ sampleRate: MIC_SAMPLE_RATE,
456
+ channelCount: 1,
457
+ echoCancellation: true,
458
+ noiseSuppression: true,
459
+ autoGainControl: true,
460
+ },
224
461
  })
225
462
  if (generation !== generationRef.current) {
226
463
  stream.getTracks().forEach(track => track.stop())
@@ -243,30 +480,66 @@ export function useGeminiLiveVoice({ workspacePath, chatId, startSession, sendAu
243
480
  const source = ctx.createMediaStreamSource(stream)
244
481
  if (ctx.state === 'suspended') await ctx.resume()
245
482
  if (generation !== generationRef.current) { source.disconnect(); return }
246
- const processor = ctx.createScriptProcessor(CAPTURE_BUFFER_SIZE, 1, 1)
247
- const ratio = ctx.sampleRate / MIC_SAMPLE_RATE
248
483
 
249
- processor.onaudioprocess = (audioEvent) => {
484
+ const forwardFloatChunk = (samples: Float32Array) => {
250
485
  if (!sessionIdRef.current || isPausedRef.current) return
251
- const input = audioEvent.inputBuffer.getChannelData(0)
252
- const outLength = Math.floor(input.length / ratio)
253
- const pcm = new Int16Array(outLength)
254
- for (let i = 0; i < outLength; i += 1) {
255
- const sample = input[Math.floor(i * ratio)]
256
- pcm[i] = Math.max(-32768, Math.min(32767, Math.round(sample * 32768)))
257
- }
258
- let binary = ''
259
- const bytes = new Uint8Array(pcm.buffer)
260
- for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i])
261
- sendAudioChunk(sessionIdRef.current, btoa(binary)).catch((error) =>
262
- console.error('Failed to send Gemini Live audio chunk', error)
486
+ const pcm = downsampleToPcm16(samples, ctx.sampleRate, MIC_SAMPLE_RATE)
487
+ if (pcm.length === 0) return
488
+ sendAudioChunk(sessionIdRef.current, pcm16ToBase64(pcm)).catch((error) =>
489
+ console.error('Failed to send Gemini Live audio chunk', error),
263
490
  )
264
491
  }
265
492
 
266
- source.connect(processor)
267
- if (analyserRef.current) source.connect(analyserRef.current)
268
- processor.connect(ctx.destination)
269
- captureNodesRef.current = { source, processor }
493
+ let attachedWorklet = false
494
+ if (
495
+ typeof AudioWorkletNode !== 'undefined' &&
496
+ ctx.audioWorklet &&
497
+ typeof ctx.audioWorklet.addModule === 'function' &&
498
+ typeof Blob !== 'undefined' &&
499
+ typeof URL !== 'undefined' &&
500
+ typeof URL.createObjectURL === 'function'
501
+ ) {
502
+ try {
503
+ const blob = new Blob([WORKLET_SOURCE], { type: 'application/javascript' })
504
+ const moduleUrl = URL.createObjectURL(blob)
505
+ try {
506
+ await ctx.audioWorklet.addModule(moduleUrl)
507
+ } finally {
508
+ URL.revokeObjectURL(moduleUrl)
509
+ }
510
+ if (generation !== generationRef.current) {
511
+ source.disconnect()
512
+ return
513
+ }
514
+ const workletNode = new AudioWorkletNode(ctx, WORKLET_PROCESSOR_NAME, {
515
+ numberOfInputs: 1,
516
+ numberOfOutputs: 0,
517
+ channelCount: 1,
518
+ })
519
+ workletNode.port.onmessage = (msgEvent: MessageEvent<Float32Array>) => {
520
+ if (msgEvent.data instanceof Float32Array) {
521
+ forwardFloatChunk(msgEvent.data)
522
+ }
523
+ }
524
+ source.connect(workletNode)
525
+ if (analyserRef.current) source.connect(analyserRef.current)
526
+ captureNodesRef.current = { source, processor: workletNode, workletPort: workletNode.port }
527
+ attachedWorklet = true
528
+ } catch {
529
+ // Fall back to ScriptProcessorNode below if AudioWorklet registration fails.
530
+ }
531
+ }
532
+
533
+ if (!attachedWorklet) {
534
+ const processor = ctx.createScriptProcessor(CAPTURE_BUFFER_SIZE, 1, 1)
535
+ processor.onaudioprocess = (audioEvent) => {
536
+ forwardFloatChunk(audioEvent.inputBuffer.getChannelData(0))
537
+ }
538
+ source.connect(processor)
539
+ if (analyserRef.current) source.connect(analyserRef.current)
540
+ processor.connect(ctx.destination)
541
+ captureNodesRef.current = { source, processor }
542
+ }
270
543
  setIsRecording(true)
271
544
  setVoiceAwaitingResponse(true)
272
545
  } catch (error) {
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ WORKSPACE_FILE_MIME,
4
+ WORKSPACE_PATH_MIME,
5
+ WORKSPACE_IMAGE_MIME,
6
+ isImageFileName,
7
+ formatWorkspaceReference,
8
+ setWorkspaceDragData,
9
+ resolveImageToDataUri,
10
+ } from './workspaceDrag'
11
+
12
+ describe('workspaceDrag utility', () => {
13
+ it('defines the standard workspace path MIME constant', () => {
14
+ expect(WORKSPACE_PATH_MIME).toBe('application/x-mint-workspace-path')
15
+ expect(WORKSPACE_FILE_MIME).toBe('application/x-mint-workspace-file')
16
+ expect(WORKSPACE_IMAGE_MIME).toBe('application/x-mint-workspace-image')
17
+ })
18
+
19
+ describe('isImageFileName', () => {
20
+ it('identifies common image extensions', () => {
21
+ expect(isImageFileName('photo.png')).toBe(true)
22
+ expect(isImageFileName('mockup.JPG')).toBe(true)
23
+ expect(isImageFileName('diagram.svg')).toBe(true)
24
+ expect(isImageFileName('asset.webp')).toBe(true)
25
+ expect(isImageFileName('anim.gif')).toBe(true)
26
+ })
27
+
28
+ it('rejects non-image files', () => {
29
+ expect(isImageFileName('presentation.pdf')).toBe(false)
30
+ expect(isImageFileName('script.py')).toBe(false)
31
+ expect(isImageFileName('README.md')).toBe(false)
32
+ })
33
+ })
34
+
35
+ describe('formatWorkspaceReference', () => {
36
+ it('preserves existing @ prefix', () => {
37
+ expect(formatWorkspaceReference('@src/main.rs')).toBe('@src/main.rs')
38
+ })
39
+
40
+ it('adds @ prefix to relative paths', () => {
41
+ expect(formatWorkspaceReference('Harness_Presentation.pdf')).toBe('@Harness_Presentation.pdf')
42
+ expect(formatWorkspaceReference('docs/api.md')).toBe('@docs/api.md')
43
+ })
44
+
45
+ it('resolves absolute path to relative reference when inside workspace root', () => {
46
+ expect(
47
+ formatWorkspaceReference(
48
+ '/home/user/project/docs/guide.pdf',
49
+ '/home/user/project'
50
+ )
51
+ ).toBe('@docs/guide.pdf')
52
+ })
53
+
54
+ it('handles Windows paths and trailing slashes in workspace root', () => {
55
+ expect(
56
+ formatWorkspaceReference(
57
+ 'C:\\Users\\dev\\project\\assets\\logo.png',
58
+ 'C:/Users/dev/project/'
59
+ )
60
+ ).toBe('@assets/logo.png')
61
+ })
62
+
63
+ it('returns empty string for empty input', () => {
64
+ expect(formatWorkspaceReference('')).toBe('')
65
+ })
66
+ })
67
+
68
+ describe('setWorkspaceDragData', () => {
69
+ it('sets path MIME, text/plain, and workspace file MIME on DataTransfer', () => {
70
+ const store: Record<string, string> = {}
71
+ const mockDataTransfer = {
72
+ setData: (format: string, data: string) => {
73
+ store[format] = data
74
+ },
75
+ effectAllowed: 'none',
76
+ } as unknown as DataTransfer
77
+
78
+ setWorkspaceDragData(mockDataTransfer, {
79
+ path: 'Harness_Presentation.pdf',
80
+ root: '/home/user/project',
81
+ kind: 'file',
82
+ })
83
+
84
+ expect(store[WORKSPACE_PATH_MIME]).toBe('@Harness_Presentation.pdf')
85
+ expect(store['text/plain']).toBe('@Harness_Presentation.pdf')
86
+ expect(store[WORKSPACE_FILE_MIME]).toContain('Harness_Presentation.pdf')
87
+ expect(mockDataTransfer.effectAllowed).toBe('copy')
88
+ })
89
+
90
+ it('omits workspace file MIME when kind is directory', () => {
91
+ const store: Record<string, string> = {}
92
+ const mockDataTransfer = {
93
+ setData: (format: string, data: string) => {
94
+ store[format] = data
95
+ },
96
+ effectAllowed: 'none',
97
+ } as unknown as DataTransfer
98
+
99
+ setWorkspaceDragData(mockDataTransfer, {
100
+ path: 'src',
101
+ root: '/home/user/project',
102
+ kind: 'directory',
103
+ })
104
+
105
+ expect(store[WORKSPACE_PATH_MIME]).toBe('@src')
106
+ expect(store['text/plain']).toBe('@src')
107
+ expect(store[WORKSPACE_FILE_MIME]).toBeUndefined()
108
+ expect(store[WORKSPACE_IMAGE_MIME]).toBeUndefined()
109
+ })
110
+
111
+ it('sets WORKSPACE_IMAGE_MIME when dragged item is an image file', () => {
112
+ const store: Record<string, string> = {}
113
+ const mockDataTransfer = {
114
+ setData: (format: string, data: string) => {
115
+ store[format] = data
116
+ },
117
+ effectAllowed: 'none',
118
+ } as unknown as DataTransfer
119
+
120
+ setWorkspaceDragData(mockDataTransfer, {
121
+ path: 'assets/hero.png',
122
+ root: '/home/user/project',
123
+ kind: 'file',
124
+ url: 'data:image/png;base64,abc1234',
125
+ })
126
+
127
+ expect(store[WORKSPACE_IMAGE_MIME]).toBeDefined()
128
+ const parsed = JSON.parse(store[WORKSPACE_IMAGE_MIME])
129
+ expect(parsed.path).toBe('assets/hero.png')
130
+ expect(parsed.url).toBe('data:image/png;base64,abc1234')
131
+ expect(parsed.name).toBe('hero.png')
132
+ })
133
+ })
134
+
135
+ describe('resolveImageToDataUri', () => {
136
+ it('returns dataUri directly if target already starts with data:image/', async () => {
137
+ const dataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=='
138
+ const result = await resolveImageToDataUri(dataUri)
139
+ expect(result).toBe(dataUri)
140
+ })
141
+
142
+ it('falls back to platformReader for workspace image paths', async () => {
143
+ const mockReader = async (path: string, _root: string) => `data:image/jpeg;base64,mock_${path}`
144
+ const result = await resolveImageToDataUri('images/sample.jpg', '/workspace', mockReader)
145
+ expect(result).toBe('data:image/jpeg;base64,mock_images/sample.jpg')
146
+ })
147
+
148
+ it('returns empty string if resolution fails', async () => {
149
+ const result = await resolveImageToDataUri('nonexistent.jpg')
150
+ expect(result).toBe('')
151
+ })
152
+ })
153
+ })