@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.
- package/Cargo.lock +6 -6
- package/Cargo.toml +2 -2
- package/README.md +8 -2
- package/Release_Note.md +222 -71
- package/crates/mint-cli/src/agent/diff_render.rs +66 -44
- package/crates/mint-cli/src/agent/live_status.rs +21 -4
- package/crates/mint-cli/src/agent/markdown_render.rs +342 -5
- package/crates/mint-cli/src/agent.rs +25 -10
- package/crates/mint-cli/src/commands/agent.rs +4 -92
- package/crates/mint-cli/src/commands/browser.rs +2 -15
- package/crates/mint-cli/src/commands/integrations.rs +3 -3
- package/crates/mint-cli/src/commands/mod.rs +0 -3
- package/crates/mint-cli/src/commands/tailnet_web.rs +1 -0
- package/crates/mint-cli/src/interactive/chat_tui/dialog.rs +572 -0
- package/crates/mint-cli/src/interactive/chat_tui/mod.rs +1427 -0
- package/crates/mint-cli/src/interactive/chat_tui/render.rs +1524 -0
- package/crates/mint-cli/src/interactive/chat_tui/selection.rs +401 -0
- package/crates/mint-cli/src/interactive/chat_tui/state.rs +1833 -0
- package/crates/mint-cli/src/interactive/chat_tui/tests.rs +3414 -0
- package/crates/mint-cli/src/interactive/chat_tui/thought.rs +202 -0
- package/crates/mint-cli/src/interactive/chat_tui/transcript.rs +728 -0
- package/crates/mint-cli/src/interactive/chat_tui/watermark.rs +148 -0
- package/crates/mint-cli/src/interactive/command_ui.rs +53 -5
- package/crates/mint-cli/src/interactive/composer.rs +186 -0
- package/crates/mint-cli/src/interactive/format.rs +78 -3
- package/crates/mint-cli/src/interactive/input_box.rs +61 -1
- package/crates/mint-cli/src/interactive/resume_picker.rs +367 -25
- package/crates/mint-cli/src/interactive/slash_commands.rs +648 -371
- package/crates/mint-cli/src/interactive/tool_activity.rs +93 -20
- package/crates/mint-cli/src/interactive.rs +37 -0
- package/crates/mint-cli/src/main.rs +6 -3
- package/crates/mint-cli/src/markdown.rs +2 -37
- package/crates/mint-cli/src/onboard.rs +700 -282
- package/crates/mint-cli/src/setup.rs +1342 -624
- package/crates/mint-cli/src/terminal_theme.rs +159 -35
- package/crates/mint-cli/tests/library_intake.rs +1 -0
- package/crates/mint-core/src/agent/chat/anthropic.rs +318 -0
- package/crates/mint-core/src/agent/chat/custom.rs +149 -0
- package/crates/mint-core/src/agent/chat/gemini.rs +518 -0
- package/crates/mint-core/src/agent/chat/mod.rs +857 -0
- package/crates/mint-core/src/agent/chat/ollama.rs +321 -0
- package/crates/mint-core/src/agent/chat/openai.rs +582 -0
- package/crates/mint-core/src/agent/chat/streaming.rs +469 -0
- package/crates/mint-core/src/agent/chat/tests.rs +1479 -0
- package/crates/mint-core/src/agent/memory.rs +56 -2
- package/crates/mint-core/src/agent/tasks.rs +2 -2
- package/crates/mint-core/src/api_server/routes/chat.rs +24 -2
- package/crates/mint-core/src/api_server/routes/gemini_live.rs +14 -1
- package/crates/mint-core/src/api_server/routes/html_preview.rs +1 -8
- package/crates/mint-core/src/api_server/routes/sessions.rs +21 -0
- package/crates/mint-core/src/avatar_bridge.rs +0 -22
- package/crates/mint-core/src/browser/cdp.rs +10 -4
- package/crates/mint-core/src/browser/headless.rs +582 -0
- package/crates/mint-core/src/browser/input.rs +7 -62
- package/crates/mint-core/src/browser/interact.rs +0 -14
- package/crates/mint-core/src/browser/mod.rs +8 -29
- package/crates/mint-core/src/browser/navigate.rs +2 -36
- package/crates/mint-core/src/browser/runtime.rs +2 -4
- package/crates/mint-core/src/browser/session.rs +50 -466
- package/crates/mint-core/src/browser/workflow.rs +142 -32
- package/crates/mint-core/src/cron/store.rs +2 -2
- package/crates/mint-core/src/integrations/mcp.rs +6 -8
- package/crates/mint-core/src/lib.rs +0 -5
- package/crates/mint-core/src/media/gemini_live.rs +636 -86
- package/crates/mint-core/src/media/library/migration.rs +26 -0
- package/crates/mint-core/src/media/library/tests.rs +1 -1
- package/crates/mint-core/src/media/library/thumbnails.rs +8 -2
- package/crates/mint-core/src/media/pictures.rs +1 -1
- package/crates/mint-core/src/media/tool_images.rs +92 -1
- package/crates/mint-core/src/mods/manager.rs +101 -35
- package/crates/mint-core/src/orchestration/context_render.rs +441 -24
- package/crates/mint-core/src/orchestration/decision_parsing.rs +531 -16
- package/crates/mint-core/src/orchestration/full_skill_result_tests.rs +12 -0
- package/crates/mint-core/src/orchestration/loop_guard.rs +145 -0
- package/crates/mint-core/src/orchestration/mcp_outcome_tests.rs +864 -0
- package/crates/mint-core/src/orchestration/memory_skill.rs +88 -28
- package/crates/mint-core/src/orchestration/mod.rs +320 -4249
- package/crates/mint-core/src/orchestration/reliability.rs +67 -27
- package/crates/mint-core/src/orchestration/review_regressions.rs +5 -0
- package/crates/mint-core/src/orchestration/tests.rs +1665 -0
- package/crates/mint-core/src/orchestration/tools/dispatch.rs +967 -0
- package/crates/mint-core/src/orchestration/tools/files.rs +1 -0
- package/crates/mint-core/src/orchestration/tools/mod.rs +5 -1
- package/crates/mint-core/src/orchestration/tools/planning.rs +102 -42
- package/crates/mint-core/src/orchestration/tools/shell.rs +129 -58
- package/crates/mint-core/src/orchestration/truncation.rs +170 -13
- package/crates/mint-core/src/orchestration/types.rs +648 -0
- package/crates/mint-core/src/orchestration/verification.rs +0 -3
- package/crates/mint-core/src/orchestration/workspace_helpers.rs +24 -6
- package/crates/mint-core/src/prompts/agent.rs +52 -76
- package/crates/mint-core/src/prompts/tool_catalog.rs +8 -151
- package/crates/mint-core/src/search/files.rs +20 -0
- package/crates/mint-core/src/slash/mod.rs +228 -265
- package/crates/mint-core/src/slash/models.rs +6 -1
- package/crates/mint-core/src/system/bg_shell.rs +1 -0
- package/crates/mint-core/src/system/config.rs +176 -34
- package/crates/mint-mod-runtime/src/lib.rs +37 -23
- package/out/web/assets/ArtifactPreviewPanel-Cx6BVOOF.js +2 -0
- package/out/web/assets/CalculationCard-D2E74u5q.js +1 -0
- package/out/web/assets/CliSessionsView-ZbdtSjvK.css +1 -0
- package/out/web/assets/{CliSessionsView-MI8kXMAb.js → CliSessionsView-z5i6qGyb.js} +1 -1
- package/out/web/assets/CodeReviewPage-BK2bOScd.css +1 -0
- package/out/web/assets/CodeReviewPage-Dp6zHuHu.js +2 -0
- package/out/web/assets/{CommandPalette-BD8SlTE5.js → CommandPalette-DaKlzbKs.js} +1 -1
- package/out/web/assets/ImageGenCard-BMCJHP9O.js +1 -0
- package/out/web/assets/ImageSearchCard-Cld_SOUg.js +1 -0
- package/out/web/assets/{ImageStudioPanel-BP7N4rLr.js → ImageStudioPanel-B5N7TjeB.js} +1 -1
- package/out/web/assets/LibraryPage-CMB89rJ0.css +1 -0
- package/out/web/assets/LibraryPage-DbZEi4W_.js +2 -0
- package/out/web/assets/LinkedFoldersView-9B9DIPoZ.js +1 -0
- package/out/web/assets/{McpRegistryPicker-CoS83vtc.js → McpRegistryPicker-DnXPlzI7.js} +1 -1
- package/out/web/assets/McpServersView-CiNBNUl4.js +1 -0
- package/out/web/assets/MermaidCard-BBDNEXbY.js +2 -0
- package/out/web/assets/MintDashboard-C_UYPoNV.js +119 -0
- package/out/web/assets/MintDashboard-DTd8C_c8.css +1 -0
- package/out/web/assets/PlanCard-C2x5pajq.js +1 -0
- package/out/web/assets/{PluginsView-jBYmcKh4.js → PluginsView-DoTg9MVa.js} +1 -1
- package/out/web/assets/RewindModal-B5oowrR0.js +1 -0
- package/out/web/assets/{ScheduledTasksView-5uw9-V9H.js → ScheduledTasksView-BD5DVPkZ.js} +1 -1
- package/out/web/assets/{SelectField-DXBQtrUK.js → SelectField-Cy6CghrU.js} +1 -1
- package/out/web/assets/SettingsWindow-BYl_oCFh.js +4 -0
- package/out/web/assets/SettingsWindow-Ckcgnjt8.css +1 -0
- package/out/web/assets/{SkillsView-C-gp9J7m.js → SkillsView-CIeWwq5z.js} +1 -1
- package/out/web/assets/StockCard-DxUciKyp.js +1 -0
- package/out/web/assets/ToolSurfacePage-qBgs_VHq.css +1 -0
- package/out/web/assets/ToolSurfacePage-saVbVrkE.js +38 -0
- package/out/web/assets/{UiFeatureCard-6BwxTOCe.js → UiFeatureCard-CdY0hGWy.js} +1 -1
- package/out/web/assets/{UiGridCard-DvZcLNxb.js → UiGridCard-BBK3I5hC.js} +1 -1
- package/out/web/assets/UiMockupWidget-CpR9EDuc.js +1 -0
- package/out/web/assets/{VeoStudioPanel-DOj7_WR3.css → VeoStudioPanel-9kU9YLEN.css} +1 -1
- package/out/web/assets/{VeoStudioPanel-DfsOJKv0.js → VeoStudioPanel-Co6-wEWM.js} +2 -2
- package/out/web/assets/WeatherCard-Buzi7Vfu.js +1 -0
- package/out/web/assets/WorkspacePdfPreview-Bi8Zzyir.js +2 -0
- package/out/web/assets/{abnfDiagram-VCTEODGH-BOo3Ckng.js → abnfDiagram-VCTEODGH-CMfR95N2.js} +1 -1
- package/out/web/assets/{architecture-7GRP2DOG-ZOwheW3n.js → architecture-7GRP2DOG-DhAM4HyO.js} +1 -1
- package/out/web/assets/{architectureDiagram-5GKGNRK7-DC2FDzVd.js → architectureDiagram-5GKGNRK7-Yk3TUJ_F.js} +1 -1
- package/out/web/assets/{blockDiagram-I7D4REHJ-BrKFWdAH.js → blockDiagram-I7D4REHJ-X9fBgJik.js} +1 -1
- package/out/web/assets/{c4Diagram-7LVT6UL2-BHTHK1Yd.js → c4Diagram-7LVT6UL2-Bk9x7N-8.js} +1 -1
- package/out/web/assets/{chunk-4HAMMTFA-BYFkZwNj.js → chunk-4HAMMTFA-Dx5fk1_0.js} +1 -1
- package/out/web/assets/{chunk-75Z2AOVW-BSKfSKSA.js → chunk-75Z2AOVW-BszTQoM4.js} +1 -1
- package/out/web/assets/{chunk-DU6HZSFF-vIhsiW6n.js → chunk-DU6HZSFF-DK-As3rk.js} +1 -1
- package/out/web/assets/{chunk-F27PBJKO-BlYpW9xq.js → chunk-F27PBJKO-LmncZZIS.js} +1 -1
- package/out/web/assets/{chunk-GMAD6QVW-BIBz_Ug5.js → chunk-GMAD6QVW-DSQ_sDRJ.js} +1 -1
- package/out/web/assets/{chunk-GVQU2GXP-NPSHxeqC.js → chunk-GVQU2GXP-Cno3ulje.js} +1 -1
- package/out/web/assets/{chunk-IMKFNOWR-YlJK8_Fp.js → chunk-IMKFNOWR-bIzfY9Jc.js} +1 -1
- package/out/web/assets/{chunk-L3NEJ4N5-DMYi4-af.js → chunk-L3NEJ4N5-Dl6eOa0D.js} +1 -1
- package/out/web/assets/{chunk-OSK3NFVY-Begd3-jA.js → chunk-OSK3NFVY-BaUGJ-cM.js} +1 -1
- package/out/web/assets/{chunk-P2QGCYS3-D1I4xFHN.js → chunk-P2QGCYS3-_066ndhH.js} +1 -1
- package/out/web/assets/{chunk-POPQ4Y6H-CafZZzNE.js → chunk-POPQ4Y6H-YT1AKhTv.js} +1 -1
- package/out/web/assets/{chunk-PWAF6VOD-Dwh6_Lnt.js → chunk-PWAF6VOD-CrEgDsxh.js} +1 -1
- package/out/web/assets/{chunk-SHT3W25Y-B0XrCSEb.js → chunk-SHT3W25Y-_KwaRLOl.js} +1 -1
- package/out/web/assets/{chunk-SVP7TREG-CxLsddxC.js → chunk-SVP7TREG-BQ1qb32L.js} +1 -1
- package/out/web/assets/{chunk-TICWLB2K-y156QInb.js → chunk-TICWLB2K-BDyehCtg.js} +1 -1
- package/out/web/assets/classDiagram-ZZMXUADV-BoSpzRCJ.js +1 -0
- package/out/web/assets/classDiagram-v2-VYDZK3BY-BoSpzRCJ.js +1 -0
- package/out/web/assets/{config-DqXJXeZM.css → config-2suVFPZg.css} +1 -1
- package/out/web/assets/config-CkRCdkxn.js +2 -0
- package/out/web/assets/{cynefin-OW5HDTMX-SMVvrLf-.js → cynefin-OW5HDTMX-Dl_31aaN.js} +1 -1
- package/out/web/assets/{cynefinDiagram-5FMLGOSQ-rZznN24T.js → cynefinDiagram-5FMLGOSQ-Bpir8jha.js} +1 -1
- package/out/web/assets/{dagre-GXQ25YYZ-BOToydFu.js → dagre-GXQ25YYZ-D9xu3nsq.js} +1 -1
- package/out/web/assets/{diagram-S7CK7UJ4-TMVaGQEJ.js → diagram-S7CK7UJ4-DeveoEeB.js} +1 -1
- package/out/web/assets/{diagram-UQ7AKVKN-C2JK-GP2.js → diagram-UQ7AKVKN-p4xWRESV.js} +1 -1
- package/out/web/assets/{diagram-VSXAHHWV-Cw8VSi2K.js → diagram-VSXAHHWV-C7qz9NDF.js} +1 -1
- package/out/web/assets/{diagram-VX7I27RA-DXp4fKk9.js → diagram-VX7I27RA-DRiCa3Vd.js} +1 -1
- package/out/web/assets/{diagram-Z3DM3KII-CzSicYiW.js → diagram-Z3DM3KII-Bt7zduQX.js} +1 -1
- package/out/web/assets/{ebnfDiagram-PWID7BFC-Dqj-zqPu.js → ebnfDiagram-PWID7BFC-DV9WpwdR.js} +1 -1
- package/out/web/assets/{erDiagram-RLTQ6QDP-DZwFZAuD.js → erDiagram-RLTQ6QDP-BJ_nwG2m.js} +1 -1
- package/out/web/assets/{eventmodeling-NTZA5JFV-C0O-Lvgp.js → eventmodeling-NTZA5JFV-BTBMrfVa.js} +1 -1
- package/out/web/assets/flowDiagram-HODETNUW-Cdlq3cJL.js +1 -0
- package/out/web/assets/{ganttDiagram-EL5Y4UJY-Da4ULXTl.js → ganttDiagram-EL5Y4UJY-DQLdsXrC.js} +1 -1
- package/out/web/assets/{gitGraph-4MIJSDKK-B9wNFZIP.js → gitGraph-4MIJSDKK-CL1OEOQ_.js} +1 -1
- package/out/web/assets/{gitGraphDiagram-WWUBYQGX-RjrhB0MP.js → gitGraphDiagram-WWUBYQGX-8cSi42H7.js} +1 -1
- package/out/web/assets/index-BocxnrDG.js +168 -0
- package/out/web/assets/index-yeh_r55a.css +1 -0
- package/out/web/assets/{info-A6RAGUB7-B3dfCpeY.js → info-A6RAGUB7-C1zsX5sc.js} +1 -1
- package/out/web/assets/{infoDiagram-27XIBGKW-QVWkLVx7.js → infoDiagram-27XIBGKW-DsRYtIyI.js} +1 -1
- package/out/web/assets/{ishikawaDiagram-5VMMS53U-CLFNIRI9.js → ishikawaDiagram-5VMMS53U-B2crwIXj.js} +1 -1
- package/out/web/assets/{journeyDiagram-3NMN7TZE-fgOQadYF.js → journeyDiagram-3NMN7TZE-BTH-GyVR.js} +1 -1
- package/out/web/assets/{kanban-definition-UXKFOSKX-G3Q_9g9O.js → kanban-definition-UXKFOSKX-BeRfCnuA.js} +1 -1
- package/out/web/assets/{mermaid-parser.core-v3Jm7nco.js → mermaid-parser.core-BJwgCBBc.js} +3 -3
- package/out/web/assets/{mermaid.core-DTEjLUA1.js → mermaid.core-CsDDodN8.js} +4 -4
- package/out/web/assets/{mindmap-definition-YA3MSWOX-C_D42_fF.js → mindmap-definition-YA3MSWOX-B42efOQ1.js} +1 -1
- package/out/web/assets/{modelManager-BPLycQrN.js → modelManager-B3ZFMh8v.js} +2 -2
- package/out/web/assets/{oauthManager-BUK7ic7q.js → oauthManager-Ch5Du2fF.js} +1 -1
- package/out/web/assets/{packet-AYTQ26CC-B-R-SHlF.js → packet-AYTQ26CC-B6xUyE6r.js} +1 -1
- package/out/web/assets/{pegDiagram-XKGWAZYB-Da2q_giz.js → pegDiagram-XKGWAZYB-1LabVycL.js} +1 -1
- package/out/web/assets/{pie-WAS4IAKB-BC8F0N7-.js → pie-WAS4IAKB-DJwo-nf_.js} +1 -1
- package/out/web/assets/{pieDiagram-E7YTZNPT-Iy9REJNC.js → pieDiagram-E7YTZNPT-icmAqzy4.js} +1 -1
- package/out/web/assets/{quadrantDiagram-AXDQQJYC-ztsiYw7h.js → quadrantDiagram-AXDQQJYC-CZ-wEx41.js} +1 -1
- package/out/web/assets/{radar-RG4KPBEZ-B_1P8Z_z.js → radar-RG4KPBEZ-D7d-Xriw.js} +1 -1
- package/out/web/assets/{railroad-74A4TZTK-DuTqczSo.js → railroad-74A4TZTK-viuhdYbs.js} +1 -1
- package/out/web/assets/{railroad-abnf-HS5TGJTU-Df29WLmp.js → railroad-abnf-HS5TGJTU-DNXiCNea.js} +1 -1
- package/out/web/assets/{railroad-ebnf-LZEXJU2U-ePFMo-qp.js → railroad-ebnf-LZEXJU2U-nkSu1-0C.js} +1 -1
- package/out/web/assets/{railroad-peg-WCYAUIDC-DYXLW4_E.js → railroad-peg-WCYAUIDC-BhbMeQzz.js} +1 -1
- package/out/web/assets/{railroadDiagram-O6MQD6OU-LStnZf2S.js → railroadDiagram-O6MQD6OU-CLpG1jUX.js} +1 -1
- package/out/web/assets/{requirementDiagram-BXWQKSXE-DmzonMH-.js → requirementDiagram-BXWQKSXE-kRDrvWrc.js} +1 -1
- package/out/web/assets/{sankeyDiagram-P5KCCOFB-B8vjmUU1.js → sankeyDiagram-P5KCCOFB-DxAwmq4x.js} +1 -1
- package/out/web/assets/{sequenceDiagram-WJ2MYXX4-CTnplOAX.js → sequenceDiagram-WJ2MYXX4-aLgY-6Wz.js} +1 -1
- package/out/web/assets/{stateDiagram-D77RDMKH-BFrIaLdI.js → stateDiagram-D77RDMKH-D5QWfOhp.js} +1 -1
- package/out/web/assets/stateDiagram-v2-MP3YSRHH-D6OiZoAP.js +1 -0
- package/out/web/assets/{swimlanes-42K2YHIH-BXGngUNU.js → swimlanes-42K2YHIH-CRs5UyA-.js} +1 -1
- package/out/web/assets/swimlanesDiagram-VR7AAH4N-UgEXIsV0.js +8 -0
- package/out/web/assets/syntaxHighlight-C-wAGT84.js +3 -0
- package/out/web/assets/{timeline-definition-24CTP7MA-CegH7n43.js → timeline-definition-24CTP7MA-Denqd9qb.js} +1 -1
- package/out/web/assets/{treeView-Q6P3EWNA-DTpJv2Tw.js → treeView-Q6P3EWNA-0my3Z3s4.js} +1 -1
- package/out/web/assets/{treemap-WGGIJYW6-i8M9iJU6.js → treemap-WGGIJYW6-B_EjkLYq.js} +1 -1
- package/out/web/assets/{vennDiagram-4TSXK5OY-Dpzo1S8U.js → vennDiagram-4TSXK5OY-DxVg3Ewr.js} +1 -1
- package/out/web/assets/{wardley-WFR3VGLG-BrNsgtfw.js → wardley-WFR3VGLG-CGD-Tj4p.js} +1 -1
- package/out/web/assets/{wardleyDiagram-VM6X3IG4-lQ92LFE1.js → wardleyDiagram-VM6X3IG4-CmfS39Xm.js} +1 -1
- package/out/web/assets/{xychartDiagram-S5SC5T6Z-BaOaEoBd.js → xychartDiagram-S5SC5T6Z-BVM9bKis.js} +1 -1
- package/out/web/index-web.html +2 -2
- package/out/web/index.html +2 -2
- package/package.json +2 -2
- package/shared-modules/CODEOWNERS +3 -3
- package/shared-modules/SDL2/SDL2_mixer.json +27 -0
- package/shared-modules/appstream/README.md +13 -0
- package/shared-modules/appstream/appstream-compose.json +111 -0
- package/shared-modules/dbus-glib/dbus-glib.json +2 -2
- package/shared-modules/libportal/libportal.json +23 -0
- package/shared-modules/linux-audio/fluidsynth2-static.json +2 -2
- package/shared-modules/linux-audio/fluidsynth2.json +2 -2
- package/shared-modules/linux-audio/libinstpatch.json +2 -2
- package/shared-modules/linux-audio/liblo-static.json +3 -0
- package/shared-modules/luajit/luajit.json +2 -2
- package/shared-modules/squashfs-tools/squashfs-tools.json +0 -3
- package/slash-commands.json +7 -2
- package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/renderer/shared/agentProgress.ts +51 -3
- package/src/renderer/shared/components/Settings/AutomationTab.tsx +2 -56
- package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +15 -1
- package/src/renderer/shared/components/Settings/GeneralTab.tsx +25 -12
- package/src/renderer/shared/components/Settings/ThemeTab.test.tsx +45 -0
- package/src/renderer/shared/components/Settings/ThemeTab.tsx +106 -3
- package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +67 -17
- package/src/renderer/shared/components/activity/AgentActivityTable.tsx +376 -134
- package/src/renderer/shared/components/activity/AskUserActivityCard.test.tsx +122 -0
- package/src/renderer/shared/components/activity/AskUserActivityCard.tsx +70 -0
- package/src/renderer/shared/components/activity/McpToolResults.tsx +39 -2
- package/src/renderer/shared/components/activity/SubagentDagView.tsx +7 -7
- package/src/renderer/shared/components/activity/ViewedImages.tsx +41 -2
- package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +3 -3
- package/src/renderer/shared/components/app/CommandPalette.tsx +7 -7
- package/src/renderer/shared/components/app/DesktopTitlebar.tsx +2 -2
- package/src/renderer/shared/components/app/MintDashboard.tsx +27 -17
- package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +3 -3
- package/src/renderer/shared/components/cards/CalculationCard.tsx +3 -3
- package/src/renderer/shared/components/cards/ImageGenCard.tsx +2 -2
- package/src/renderer/shared/components/cards/ImageSearchCard.tsx +2 -2
- package/src/renderer/shared/components/cards/MermaidCard.tsx +1 -1
- package/src/renderer/shared/components/cards/PlanCard.test.tsx +41 -0
- package/src/renderer/shared/components/cards/PlanCard.tsx +32 -0
- package/src/renderer/shared/components/cards/ReleaseNotesCard.test.tsx +29 -0
- package/src/renderer/shared/components/cards/ReleaseNotesCard.tsx +137 -0
- package/src/renderer/shared/components/cards/StockCard.tsx +7 -7
- package/src/renderer/shared/components/cards/ToolResultCards.tsx +11 -3
- package/src/renderer/shared/components/cards/UiMockupWidget.tsx +4 -4
- package/src/renderer/shared/components/cards/WeatherCard.tsx +5 -5
- package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +11 -0
- package/src/renderer/shared/components/chat/ApprovalCard.tsx +21 -2
- package/src/renderer/shared/components/chat/ArtifactCards.test.tsx +69 -0
- package/src/renderer/shared/components/chat/ArtifactCards.tsx +238 -0
- package/src/renderer/shared/components/chat/AttachedDocumentCard.test.tsx +39 -0
- package/src/renderer/shared/components/chat/AttachedDocumentCard.tsx +220 -0
- package/src/renderer/shared/components/chat/ChatCodeBlock.test.tsx +33 -0
- package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +15 -1
- package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +40 -2
- package/src/renderer/shared/components/chat/ChatMessageItem.tsx +180 -34
- package/src/renderer/shared/components/chat/ChatModeSelector.css +227 -0
- package/src/renderer/shared/components/chat/ChatModeSelector.test.tsx +45 -0
- package/src/renderer/shared/components/chat/ChatModeSelector.tsx +198 -0
- package/src/renderer/shared/components/chat/ChatPanel.test.tsx +22 -0
- package/src/renderer/shared/components/chat/ChatPanel.tsx +227 -53
- package/src/renderer/shared/components/chat/DiffReviewModal.tsx +6 -6
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +142 -1
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +45 -3
- package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +42 -14
- package/src/renderer/shared/components/chat/RewindModal.tsx +10 -10
- package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +14 -0
- package/src/renderer/shared/components/chat/SourcesBlock.test.tsx +53 -0
- package/src/renderer/shared/components/chat/SourcesBlock.tsx +236 -90
- package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +45 -1
- package/src/renderer/shared/components/chat/TurnFileChanges.tsx +202 -169
- package/src/renderer/shared/components/extensions/CliSessionsView.tsx +5 -4
- package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +3 -3
- package/src/renderer/shared/components/studio/LibraryPage.tsx +48 -5
- package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +125 -13
- package/src/renderer/shared/components/workspace/CodeReviewPage.test.tsx +50 -0
- package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +77 -16
- package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +78 -22
- package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +13 -0
- package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +48 -28
- package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +266 -47
- package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +18 -6
- package/src/renderer/shared/components/workspace/background-terminals.css +11 -11
- package/src/renderer/shared/constants/config.ts +1 -5
- package/src/renderer/shared/constants/models.test.ts +51 -0
- package/src/renderer/shared/constants/models.ts +139 -32
- package/src/renderer/shared/conversation/lifecycle.test.ts +91 -3
- package/src/renderer/shared/conversation/lifecycle.ts +104 -5
- package/src/renderer/shared/conversation/progressiveStream.test.ts +83 -0
- package/src/renderer/shared/conversation/progressiveStream.ts +219 -0
- package/src/renderer/shared/css/agent-observability.css +133 -0
- package/src/renderer/shared/css/chat-ui-cards.css +13 -13
- package/src/renderer/shared/css/code-review.css +63 -34
- package/src/renderer/shared/css/code-sessions.css +47 -18
- package/src/renderer/shared/css/collapsed-paste.css +1 -1
- package/src/renderer/shared/css/error-notice.css +1 -1
- package/src/renderer/shared/css/file-changes.css +165 -80
- package/src/renderer/shared/css/library.css +105 -10
- package/src/renderer/shared/css/management-views.css +1 -1
- package/src/renderer/shared/css/mcp-results.css +5 -5
- package/src/renderer/shared/css/model-selector.css +18 -18
- package/src/renderer/shared/css/mods.css +7 -7
- package/src/renderer/shared/css/notification-notice.css +2 -2
- package/src/renderer/shared/css/queued-messages.css +5 -5
- package/src/renderer/shared/css/settings/general.css +18 -13
- package/src/renderer/shared/css/settings/profile.css +5 -5
- package/src/renderer/shared/css/settings/theme.css +5 -5
- package/src/renderer/shared/css/slash-choice.css +2 -2
- package/src/renderer/shared/css/slash-suggestions.css +3 -3
- package/src/renderer/shared/css/sources-block.css +41 -2
- package/src/renderer/shared/css/tools-panel.css +2 -2
- package/src/renderer/shared/css/ui-tailwind.css +8 -0
- package/src/renderer/shared/css/veo-studio.css +2 -2
- package/src/renderer/shared/css/viewed-images.css +7 -7
- package/src/renderer/shared/platform.ts +2 -1
- package/src/renderer/shared/theme/themeManager.test.ts +17 -0
- package/src/renderer/shared/theme/themeManager.ts +21 -4
- package/src/renderer/shared/theme/themes.ts +54 -0
- package/src/renderer/shared/theme/tokens.css +6 -4
- package/src/renderer/shared/types.ts +1 -1
- package/src/renderer/shared/utils/agentActivity.test.ts +468 -2
- package/src/renderer/shared/utils/agentActivity.ts +602 -35
- package/src/renderer/shared/utils/artifactBrowser.test.ts +37 -32
- package/src/renderer/shared/utils/artifactBrowser.ts +17 -19
- package/src/renderer/shared/utils/attachedDocument.test.ts +73 -0
- package/src/renderer/shared/utils/attachedDocument.ts +49 -0
- package/src/renderer/shared/utils/fileIcons.test.ts +59 -0
- package/src/renderer/shared/utils/fileIcons.ts +149 -16
- package/src/renderer/shared/utils/markdown.tsx +72 -19
- package/src/renderer/shared/utils/markdown.webImages.test.tsx +16 -0
- package/src/renderer/shared/utils/mcpResults.test.tsx +29 -0
- package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
- package/src/renderer/shared/utils/syntaxHighlight.test.tsx +11 -1
- package/src/renderer/shared/utils/syntaxHighlight.tsx +51 -3
- package/src/renderer/shared/utils/useGeminiLiveVoice.ts +324 -51
- package/src/renderer/shared/utils/workspaceDrag.test.ts +153 -0
- package/src/renderer/shared/utils/workspaceDrag.ts +142 -0
- package/src/renderer/shared/utils/workspaceMedia.ts +10 -2
- package/src/renderer/src/App.tsx +14 -8
- package/src/renderer/src/agentProgress.ts +1 -0
- package/src/renderer/src/components/ScreenPicker.css +1 -1
- package/src/renderer/src/components/WorkspacePanel.tsx +6 -5
- package/src/renderer/src/css/app-shell.css +4 -4
- package/src/renderer/src/css/chat.css +1261 -241
- package/src/renderer/src/css/desktop-tools.css +11 -11
- package/src/renderer/src/css/sidebar-workspace.css +2 -2
- package/src/renderer/src/css/tool-surfaces.css +12 -12
- package/src/renderer/src/index.css +38 -20
- package/src/renderer/src/tauri.ts +22 -2
- package/src/renderer/src-web/App.tsx +14 -0
- package/src/renderer/src-web/agentProgress.ts +1 -0
- package/src/renderer/src-web/css/app-shell.css +4 -4
- package/src/renderer/src-web/css/chat.css +1278 -190
- package/src/renderer/src-web/css/mobile.css +29 -13
- package/src/renderer/src-web/css/settings/mobile.css +2 -2
- package/src/renderer/src-web/css/sidebar-workspace.css +1 -1
- package/src/renderer/src-web/index.css +38 -20
- package/src/renderer/src-web/tauri.ts +22 -2
- package/src/renderer/tests/browserArtifacts.browser.tsx +1 -1
- package/src/renderer/tests/library.browser.tsx +3 -0
- package/src/renderer/tests/liveVoice.browser.html +1 -0
- package/src/renderer/tests/workspaceDrop.browser.tsx +1 -1
- package/src-tauri/src/app/lifecycle.rs +1 -14
- package/src-tauri/src/background/headless.rs +14 -8
- package/src-tauri/src/commands/conversation.rs +38 -0
- package/src-tauri/src/native/mod.rs +0 -1
- package/src-tauri/src/native/system.rs +4 -47
- package/crates/mint-cli/src/interactive/chat_tui.rs +0 -8855
- package/crates/mint-core/src/agent/chat.rs +0 -4394
- package/crates/mint-core/src/browser/lifecycle.rs +0 -291
- package/crates/mint-core/src/browser/logging.rs +0 -20
- package/crates/mint-core/src/orchestration/tools/browser.rs +0 -31
- package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +0 -2
- package/out/web/assets/CalculationCard-CRjP3E1j.js +0 -1
- package/out/web/assets/CliSessionsView-C3Br2v5Y.css +0 -1
- package/out/web/assets/CodeReviewPage-CUJceQme.css +0 -1
- package/out/web/assets/CodeReviewPage-VCUgPla_.js +0 -2
- package/out/web/assets/ImageGenCard-BCXxkVJX.js +0 -1
- package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +0 -1
- package/out/web/assets/LibraryPage-DAxUTAg7.css +0 -1
- package/out/web/assets/LibraryPage-WlviRdR8.js +0 -2
- package/out/web/assets/LinkedFoldersView-CKRgroWK.js +0 -1
- package/out/web/assets/McpServersView-D3wHnuHg.js +0 -1
- package/out/web/assets/MermaidCard-NqrHdyJW.js +0 -2
- package/out/web/assets/MintDashboard-B2DJsDDO.css +0 -1
- package/out/web/assets/MintDashboard-COw_xol7.js +0 -63
- package/out/web/assets/RewindModal-DempuDod.js +0 -1
- package/out/web/assets/SettingsWindow-2YP3yj7s.js +0 -4
- package/out/web/assets/SettingsWindow-Dz8LQVgD.css +0 -1
- package/out/web/assets/StockCard-BDMCe7mx.js +0 -1
- package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +0 -38
- package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +0 -1
- package/out/web/assets/UiMockupWidget-DD73QWg1.js +0 -1
- package/out/web/assets/WeatherCard-CPucIfq8.js +0 -1
- package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +0 -2
- package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +0 -1
- package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +0 -1
- package/out/web/assets/config-CvIO5hkn.js +0 -2
- package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +0 -1
- package/out/web/assets/index-BmpulIkg.js +0 -168
- package/out/web/assets/index-CkvBwhX_.css +0 -1
- package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +0 -1
- package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +0 -8
- package/shared-modules/libsecret/libsecret.json +0 -23
- package/shared-modules/libusb/libusb.json +0 -27
- package/shared-modules/lzo/lzo.json +0 -27
- package/src/renderer/src/components/BrowserShell.tsx +0 -108
- package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +0 -91
- package/src/renderer/src/components/browser-shell.css +0 -185
- 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
|
-
//
|
|
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
|
-
|
|
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({
|
|
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<{
|
|
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 —
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
137
|
-
//
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
+
userTurnFreshRef.current = true
|
|
384
|
+
assistantTurnFreshRef.current = true
|
|
157
385
|
setErrorMessage('Live connection closed')
|
|
158
386
|
break
|
|
159
387
|
}
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
484
|
+
const forwardFloatChunk = (samples: Float32Array) => {
|
|
250
485
|
if (!sessionIdRef.current || isPausedRef.current) return
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
})
|