@hienlh/ppm 0.19.1 → 0.19.2
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/CHANGELOG.md +7 -1
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{adaptive-context-menu-JfuUSqbe.js → adaptive-context-menu-BbuRVTLn.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-MfdJKVM5.js → ai-resource-editor-BCCOUzox.js} +1 -1
- package/dist/web/assets/{ai-settings-section-CJE7jFsj.js → ai-settings-section-BEM1z7kp.js} +1 -1
- package/dist/web/assets/{angular-html-VzxSzTCy.js → angular-html-BlcrLjUx.js} +1 -1
- package/dist/web/assets/{angular-ts-CmJ4bmzM.js → angular-ts-BUjtceAJ.js} +1 -1
- package/dist/web/assets/{api-client-qRWu6_s5.js → api-client-BzWM7XPR.js} +1 -1
- package/dist/web/assets/api-client-D1CePgQ4.js +1 -0
- package/dist/web/assets/api-settings-CNtKkHqW.js +1 -0
- package/dist/web/assets/{api-settings-CPg4yGvS.js → api-settings-Cubuqjsu.js} +1 -1
- package/dist/web/assets/{api-themes-DsAp8m7m.js → api-themes-YsZgf6Xj.js} +1 -1
- package/dist/web/assets/{apl-do17bnVx.js → apl-B3g0v1pk.js} +1 -1
- package/dist/web/assets/{astro-ClY0e0OA.js → astro-FRnyn7cH.js} +1 -1
- package/dist/web/assets/{audio-preview-Fw935Mok.js → audio-preview-DWTRm4HC.js} +1 -1
- package/dist/web/assets/{blade-BXSdXDGq.js → blade-NLdOqUKw.js} +1 -1
- package/dist/web/assets/{bot-CWZAuloY.js → bot-D6sx11y0.js} +1 -1
- package/dist/web/assets/{bsl-MxsMKDUZ.js → bsl-CenHZfaU.js} +1 -1
- package/dist/web/assets/{button-CrwgGd1y.js → button-Cq8yixjf.js} +1 -1
- package/dist/web/assets/{cairo-4CIrzX_p.js → cairo-B_WR0aiQ.js} +1 -1
- package/dist/web/assets/chat-tab-CmXGsJGc.js +15 -0
- package/dist/web/assets/{chevron-down-D7CbB5f6.js → chevron-down-Dp0rpHb8.js} +1 -1
- package/dist/web/assets/chevron-right-De-Wz5Lz.js +1 -0
- package/dist/web/assets/circle-BCy0AUCo.js +1 -0
- package/dist/web/assets/{circle-question-mark-BBzFZrNh.js → circle-question-mark-CzR7YRc-.js} +1 -1
- package/dist/web/assets/clock-C1CQUQPZ.js +1 -0
- package/dist/web/assets/{cobol-phPOE89d.js → cobol-DOynhWYj.js} +1 -1
- package/dist/web/assets/code-DJgYrFzm.js +1 -0
- package/dist/web/assets/code-editor-DtHXzNZ1.js +7 -0
- package/dist/web/assets/{coffee-Cjuukfz3.js → coffee-Bn6dbw4P.js} +1 -1
- package/dist/web/assets/{conflict-editor-Ni0PPsKu.js → conflict-editor-Cgb-z12D.js} +1 -1
- package/dist/web/assets/{context-menu-BEAa1qhd.js → context-menu-DfWZAxQy.js} +1 -1
- package/dist/web/assets/{copy-CbPdNp7E.js → copy-BewHj66E.js} +1 -1
- package/dist/web/assets/{cpp-CT7tGBC3.js → cpp-CoZR9t8r.js} +1 -1
- package/dist/web/assets/{createLucideIcon-ConhwJnb.js → createLucideIcon-D8pyde1A.js} +1 -1
- package/dist/web/assets/{crystal-D8U1Qnqh.js → crystal-BSheONSg.js} +1 -1
- package/dist/web/assets/{csv-preview-Coc2oed1.js → csv-preview-BZMHfFH3.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-B8WQkZDB.js → data-grid-overlay-editor-gwPoVSCU.js} +1 -1
- package/dist/web/assets/{data-grid-types-KpjaKAci.js → data-grid-types-Dq_H1IPa.js} +1 -1
- package/dist/web/assets/{database-BqyeGogo.js → database-Y3ESDK4-.js} +1 -1
- package/dist/web/assets/database-viewer-GVaQVMdb.js +1 -0
- package/dist/web/assets/{dialog-DrL3WEXd.js → dialog-BSfuiTkJ.js} +1 -1
- package/dist/web/assets/{diff-viewer-CT0lmDc9.js → diff-viewer-BG5_fH1E.js} +1 -1
- package/dist/web/assets/{dist-C2_dKAa_.js → dist-aDSxTR_v.js} +1 -1
- package/dist/web/assets/{docx-preview-76KDw1eE.js → docx-preview-DqVDOMmv.js} +1 -1
- package/dist/web/assets/{download-DAF7SXcf.js → download-DG3_9xXy.js} +1 -1
- package/dist/web/assets/{edge-Db4ze6bw.js → edge-DTtoslQu.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-DyC_-4My.js → edit-diff-preview-CXSbSUBX.js} +1 -1
- package/dist/web/assets/{elixir-RZPZ0472.js → elixir-CIBCZ3Zi.js} +1 -1
- package/dist/web/assets/{elm-DNZcQt_7.js → elm-BLmb7K1H.js} +1 -1
- package/dist/web/assets/{erb-DRONsZXs.js → erb-BBY8ApRd.js} +1 -1
- package/dist/web/assets/{erlang-DeG0-QwJ.js → erlang-D5Zb5_P0.js} +1 -1
- package/dist/web/assets/{esm-CZy-sSoK.js → esm-6F8v1jLL.js} +1 -1
- package/dist/web/assets/explorer-body-BPFnjxYj.js +2 -0
- package/dist/web/assets/explorer-window-content-Da1T9uM4.js +1 -0
- package/dist/web/assets/{extension-store-ClQpnxQr.js → extension-store-Bhu--fPX.js} +1 -1
- package/dist/web/assets/{extension-webview-BW5GuBnH.js → extension-webview-BSh6KGd7.js} +1 -1
- package/dist/web/assets/{external-link-ojDHcQxQ.js → external-link-B6Hl0U4X.js} +1 -1
- package/dist/web/assets/{eye-off-Rq1n2-oe.js → eye-off-CUdzgmX8.js} +1 -1
- package/dist/web/assets/{file-exclamation-point-Bpg-zDPP.js → file-exclamation-point-JzhTU2TX.js} +1 -1
- package/dist/web/assets/{file-store-28e44KP3.js → file-store-6HW2Lfl4.js} +1 -1
- package/dist/web/assets/{folder-symlink-LD-mr54V.js → folder-symlink-Dx1hOk8X.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-CK5VRJNd.js → fortran-fixed-form-CBdwkEUJ.js} +1 -1
- package/dist/web/assets/{fsharp-TVVgzHWp.js → fsharp-AT4XdVYH.js} +1 -1
- package/dist/web/assets/{gdresource-DiUcyewI.js → gdresource-D852fWBR.js} +1 -1
- package/dist/web/assets/{git-commit-CfOruFrg.js → git-commit-DWfWAyxy.js} +1 -1
- package/dist/web/assets/{git-log-panel-D1-YXZ0O.js → git-log-panel-DNC9fV0M.js} +1 -1
- package/dist/web/assets/{git-rebase-Bxyr24oc.js → git-rebase-C6opk8gr.js} +1 -1
- package/dist/web/assets/glide-data-grid-CICFjJon.js +141 -0
- package/dist/web/assets/{glimmer-js-CT5aJxz_.js → glimmer-js-DLi5mWYO.js} +1 -1
- package/dist/web/assets/{glimmer-ts-CRjP9Yey.js → glimmer-ts-D-scFX30.js} +1 -1
- package/dist/web/assets/{glsl-BX2--oI_.js → glsl-dbQzF4iA.js} +1 -1
- package/dist/web/assets/{graphql-BIQsvBxQ.js → graphql-D1m5Wen2.js} +1 -1
- package/dist/web/assets/group-chat-tab-DbUkcOaJ.js +1 -0
- package/dist/web/assets/{hack-CTWk0k1m.js → hack-CqYmpO7u.js} +1 -1
- package/dist/web/assets/{haml-DGh27hXc.js → haml-BXzKWEag.js} +1 -1
- package/dist/web/assets/{hand-DKSxDa5c.js → hand-x0ejq3fS.js} +1 -1
- package/dist/web/assets/{handlebars-BJL1jNrG.js → handlebars-DXnrtHtq.js} +1 -1
- package/dist/web/assets/{html-DALyDNqU.js → html-DhgQJ-xL.js} +1 -1
- package/dist/web/assets/{html-derivative-B4XcU57e.js → html-derivative-xZRft-YE.js} +1 -1
- package/dist/web/assets/{http-DfMo7nNu.js → http-B53mSj14.js} +1 -1
- package/dist/web/assets/{hurl-CGc_n2Qf.js → hurl-CsomVy04.js} +1 -1
- package/dist/web/assets/{hxml-40zZ1t4x.js → hxml-LffjWwKA.js} +1 -1
- package/dist/web/assets/{image-preview-Cp3XB8UO.js → image-preview-CNA6XMNS.js} +1 -1
- package/dist/web/assets/index-DTqbMiVc.js +52 -0
- package/dist/web/assets/{input-DTmI80l2.js → input-D0ol5jW0.js} +1 -1
- package/dist/web/assets/{jinja-D5zuIn9u.js → jinja-CCHURl7V.js} +1 -1
- package/dist/web/assets/{jison-Dxut9QwA.js → jison-7qBd8dnR.js} +1 -1
- package/dist/web/assets/{julia-Cu85tTPo.js → julia-BY9pnTcy.js} +1 -1
- package/dist/web/assets/{just-CctM4uLa.js → just-D7wRSNRr.js} +1 -1
- package/dist/web/assets/{keybindings-store-Bi0q9x4Q.js → keybindings-store-BGkB72gM.js} +1 -1
- package/dist/web/assets/keybindings-store-Bq6VWl5X.js +1 -0
- package/dist/web/assets/{keyboard-AdwmYDSu.js → keyboard-DBBaoQSu.js} +1 -1
- package/dist/web/assets/{latex-kpwS_CC7.js → latex-RgsCae55.js} +1 -1
- package/dist/web/assets/{lib-Ca7ai8xa.js → lib-v9x_qXgI.js} +1 -1
- package/dist/web/assets/{liquid-B8Rr0xue.js → liquid-CnDFzvOZ.js} +1 -1
- package/dist/web/assets/loader-circle-DouKgIXy.js +1 -0
- package/dist/web/assets/{lock-BEtt_eTO.js → lock-CNxUGpTk.js} +1 -1
- package/dist/web/assets/{lua-C_3gixwM.js → lua-0lj5W8sm.js} +1 -1
- package/dist/web/assets/markdown-renderer-DEAVpo0C.js +1 -0
- package/dist/web/assets/markdown-renderer-dbLIvU2M.js +3 -0
- package/dist/web/assets/{marko-B28rvNL-.js → marko-CQgqAhbV.js} +1 -1
- package/dist/web/assets/{mdc-DQ6-STFO.js → mdc-BkIcTiIy.js} +1 -1
- package/dist/web/assets/{media-url-BjvbfTTl.js → media-url-DDciwvLU.js} +1 -1
- package/dist/web/assets/{mobile-bottom-sheet-DXn5g2LB.js → mobile-bottom-sheet-pjcHKorA.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-CD_kl-jA.js +1 -0
- package/dist/web/assets/{monitor-CXI_XpWJ.js → monitor-CxPV_fQI.js} +1 -1
- package/dist/web/assets/{monitor-smartphone-C1wUPOSg.js → monitor-smartphone-DLZ6Q05s.js} +1 -1
- package/dist/web/assets/{nextflow-B1xq2O8L.js → nextflow-DvIW68DS.js} +1 -1
- package/dist/web/assets/{nginx-xsLxRGxO.js → nginx-O-hft0gG.js} +1 -1
- package/dist/web/assets/{nim-wqcek3WO.js → nim-BxrR4HwQ.js} +1 -1
- package/dist/web/assets/notification-store-Dgbsw9p9.js +1 -0
- package/dist/web/assets/{number-overlay-editor-1c4sgZJt.js → number-overlay-editor-BoLRYuZT.js} +1 -1
- package/dist/web/assets/{overview-panel-Cr2xsHc6.js → overview-panel-l3g1gT5e.js} +1 -1
- package/dist/web/assets/{panel-store-Dv28vz3L.js → panel-store-UOV8wrfR.js} +1 -1
- package/dist/web/assets/{pdf-preview-DKeIIJTv.js → pdf-preview-B2CJ_EKv.js} +1 -1
- package/dist/web/assets/{pencil-CHKLOgLs.js → pencil-BlIIg6RH.js} +1 -1
- package/dist/web/assets/{perl-BH54gN9q.js → perl-Da-uNqmL.js} +1 -1
- package/dist/web/assets/{php-Br5ZrEZm.js → php-JMu45mEx.js} +1 -1
- package/dist/web/assets/play-DpEEBJTT.js +1 -0
- package/dist/web/assets/{plus-BxqD2FIZ.js → plus-BR0qewJI.js} +1 -1
- package/dist/web/assets/portal-container-context-D-495m5C.js +1 -0
- package/dist/web/assets/{postgres-viewer-BJPr_FYL.js → postgres-viewer-DstZwUai.js} +3 -3
- package/dist/web/assets/{process-table-BqTcAJ-h.js → process-table-kmQ2-8qJ.js} +1 -1
- package/dist/web/assets/{project-store-KHjFTCxf.js → project-store-DTUCS65m.js} +1 -1
- package/dist/web/assets/{pug-8VCubZ3H.js → pug-CLzyQhia.js} +1 -1
- package/dist/web/assets/{qml-D8AF1RZ6.js → qml-BHXfGB1l.js} +1 -1
- package/dist/web/assets/{razor-6v042IJW.js → razor-BqetJLN3.js} +1 -1
- package/dist/web/assets/{react-DHs6efqI.js → react-B549sK_T.js} +1 -1
- package/dist/web/assets/{refresh-cw-BALzw7L3.js → refresh-cw-BQU5g2CY.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-KKTRNjOL.js +2 -0
- package/dist/web/assets/{remote-desktop-mobile-view-DaoB49k4.js → remote-desktop-mobile-view-C1ST4-Fm.js} +1 -1
- package/dist/web/assets/{remote-desktop-readiness-gate--LJgsuoj.js → remote-desktop-readiness-gate-BH3BE5wA.js} +1 -1
- package/dist/web/assets/{remote-desktop-window-content-Cihx4PNZ.js → remote-desktop-window-content-BaZJQt_N.js} +1 -1
- package/dist/web/assets/{rotate-ccw-CuvNBM80.js → rotate-ccw-CpjRo90J.js} +1 -1
- package/dist/web/assets/{rst-mTGFo4AQ.js → rst-iQdJZ0BP.js} +1 -1
- package/dist/web/assets/{ruby-BBaOP636.js → ruby-CNwzZGdH.js} +1 -1
- package/dist/web/assets/{run-in-terminal-Zl5iIx-B.js → run-in-terminal-CBXmVGWa.js} +1 -1
- package/dist/web/assets/{sas-D5QEkIH2.js → sas-BpBIT2eR.js} +1 -1
- package/dist/web/assets/{save-Dxq-Zepc.js → save-DYLiVzU8.js} +1 -1
- package/dist/web/assets/{scroll-area-B5yhPpZo.js → scroll-area-lc0ZHORr.js} +1 -1
- package/dist/web/assets/{scss-C5Mu6y2u.js → scss-D1MotuEp.js} +1 -1
- package/dist/web/assets/{send-Cits8q0l.js → send-C_sLFt56.js} +1 -1
- package/dist/web/assets/{send-to-chat-Cv7ikFUC.js → send-to-chat-DJAdlvJ8.js} +1 -1
- package/dist/web/assets/{settings-store-Dp4i8zeA.js → settings-store-zbPkG7o-.js} +2 -2
- package/dist/web/assets/settings-tab-Cn-AqUP_.js +1 -0
- package/dist/web/assets/{settings-tab-BtMvvKAF.js → settings-tab-xyRpt2gq.js} +1 -1
- package/dist/web/assets/{shaderlab-mLnpMpN3.js → shaderlab-BNqmcPQi.js} +1 -1
- package/dist/web/assets/{shallow-BBJ_qoEF.js → shallow-CmPMAPte.js} +1 -1
- package/dist/web/assets/{shellsession-qdcDfUuk.js → shellsession-BBLOVOO-.js} +1 -1
- package/dist/web/assets/{shield-check-CH7F1P7m.js → shield-check-CUI36aSc.js} +1 -1
- package/dist/web/assets/{shield-off-C9HiF0-6.js → shield-off-GdkkVhb3.js} +1 -1
- package/dist/web/assets/{soy-CGGf1G43.js → soy-BpTGo9uy.js} +1 -1
- package/dist/web/assets/{sparkles-BnuLH6Ck.js → sparkles-Dh-_J2Rd.js} +1 -1
- package/dist/web/assets/{sparql-tQHB50VD.js → sparql-D7A3yKob.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DZqnYIB7.js → sql-query-editor-B86NiZHI.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DXutos0e.js → sqlite-viewer-DB775F6C.js} +1 -1
- package/dist/web/assets/{stata-DVQNLMcR.js → stata-B7N56ptS.js} +1 -1
- package/dist/web/assets/{surrealql-lu576tBi.js → surrealql-CTmnD-lv.js} +1 -1
- package/dist/web/assets/{svelte-CSCrOBjT.js → svelte-BppwY9TR.js} +1 -1
- package/dist/web/assets/{switch-BPol3BFC.js → switch-BmjKoIpR.js} +1 -1
- package/dist/web/assets/{system-monitor-body-Ct8_GX_f.js → system-monitor-body-BDxHQvy9.js} +2 -2
- package/dist/web/assets/system-monitor-tab-BoTHLx6B.js +1 -0
- package/dist/web/assets/system-monitor-window-content-B9Y98UW6.js +1 -0
- package/dist/web/assets/{tab-host-window-content-C5kYP0-z.js → tab-host-window-content-C1j8zr68.js} +1 -1
- package/dist/web/assets/{tab-store-DS3YlUwR.js → tab-store-DzYSsw5a.js} +1 -1
- package/dist/web/assets/{table-BXDyImqk.js → table-D5Q4ap4W.js} +1 -1
- package/dist/web/assets/team-member-sheet-Cxz6h5Ne.js +2 -0
- package/dist/web/assets/{team-member-window-content-BJ_XFw8-.js → team-member-window-content-CUSuRHCB.js} +1 -1
- package/dist/web/assets/{templ-CPt8AS4Q.js → templ-CvTMKKra.js} +1 -1
- package/dist/web/assets/{terminal-tab-8bKQ3CWR.js → terminal-tab-B9I_HlES.js} +3 -3
- package/dist/web/assets/{tex-BrTByT7i.js → tex--c4POEUE.js} +1 -1
- package/dist/web/assets/{text-wrap-CzMjB6GD.js → text-wrap-DBvQD3kc.js} +1 -1
- package/dist/web/assets/{tool-cards-pIu_cmEL.js → tool-cards-D3KSzd2K.js} +4 -4
- package/dist/web/assets/{trash-2-BjShZ7if.js → trash-2-By4VMbC4.js} +1 -1
- package/dist/web/assets/{triangle-alert-qQ6Plydp.js → triangle-alert-CUTURFbG.js} +1 -1
- package/dist/web/assets/{ts-tags-DSqEZ7A8.js → ts-tags--kx7D-Hw.js} +1 -1
- package/dist/web/assets/{twig-DW9qc1bf.js → twig-UG9uMHGd.js} +1 -1
- package/dist/web/assets/{upload-7Zs6oaK5.js → upload-BNlX8D07.js} +1 -1
- package/dist/web/assets/{use-blob-url-DKbV7PM_.js → use-blob-url-Bdxopwwc.js} +1 -1
- package/dist/web/assets/{use-coarse-long-press-DGyC48NF.js → use-coarse-long-press-BmI_GVW5.js} +1 -1
- package/dist/web/assets/{use-is-mobile-ClE-UcPF.js → use-is-mobile-B9UZNwSm.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DaxAk3gB.js → use-monaco-theme-CVCye2kq.js} +1 -1
- package/dist/web/assets/use-open-team-member-D9EOB8-i.js +1 -0
- package/dist/web/assets/{use-remote-desktop-display-choice-BUA_1eaH.js → use-remote-desktop-display-choice-neUqyE3p.js} +1 -1
- package/dist/web/assets/{use-visual-viewport-CCX21c9w.js → use-visual-viewport-Sfb6CZ80.js} +1 -1
- package/dist/web/assets/{use-websocket-CWpG7PWN.js → use-websocket-xOOwBdwP.js} +1 -1
- package/dist/web/assets/vendor-markdown-Ls5AcU91.js +291 -0
- package/dist/web/assets/{vendor-ui-CXJtBULr.js → vendor-ui-CaeeFO0B.js} +1 -1
- package/dist/web/assets/video-preview-DvpgBDhM.js +1 -0
- package/dist/web/assets/{vue-html-CG5BKtq1.js → vue-html-8KEW6-I9.js} +1 -1
- package/dist/web/assets/{vue-CJNDL1g-.js → vue-jjTomlbt.js} +1 -1
- package/dist/web/assets/{vue-vine-Dd5HZ7T5.js → vue-vine-C5tohCkU.js} +1 -1
- package/dist/web/assets/{window-pip-registry-9H6Y-HqR.js → window-pip-registry-FdZdGF7h.js} +1 -1
- package/dist/web/assets/{window-store-CNzkeUyO.js → window-store-D0x_fxr0.js} +1 -1
- package/dist/web/assets/x-DiOUR_mp.js +1 -0
- package/dist/web/assets/{xml-DTOm5niz.js → xml-ByFFosqL.js} +1 -1
- package/dist/web/assets/{xsl-BD_kBC04.js → xsl-CZUwiLkj.js} +1 -1
- package/dist/web/index.html +59 -59
- package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
- package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
- package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
- package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
- package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/scripts/check-ppm-dir-usage.sh +0 -0
- package/scripts/install.sh +0 -0
- package/scripts/release.sh +0 -0
- package/src/providers/claude-agent-sdk.ts +51 -43
- package/src/providers/codex-app-server/codex-provider.ts +7 -1
- package/src/server/routes/chat.ts +28 -3
- package/src/server/ws/chat.ts +9 -4
- package/src/services/named-tunnel/named-tunnel-retry.ts +26 -0
- package/src/services/named-tunnel/network-ready.ts +63 -0
- package/src/services/supervisor.ts +98 -0
- package/src/services/transcript-images.ts +14 -7
- package/src/services/upgrade.service.ts +7 -2
- package/src/shared/image-limits.ts +37 -0
- package/src/types/api.ts +3 -1
- package/src/types/chat.ts +6 -0
- package/src/types/extension-messages.ts +2 -0
- package/src/web/components/chat/attachment-chips.tsx +10 -0
- package/src/web/components/chat/chat-tab.tsx +37 -13
- package/src/web/components/chat/message-input.tsx +102 -11
- package/src/web/components/chat/session-debug-dialog.tsx +2 -1
- package/src/web/components/chat/user-message-parse.ts +33 -11
- package/src/web/components/database/database-viewer.tsx +34 -11
- package/src/web/components/database/extract-query-table.ts +24 -0
- package/src/web/components/database/glide-data-grid.tsx +11 -4
- package/src/web/components/database/glide-grid-types.ts +4 -0
- package/src/web/components/database/split-sql-statements.ts +116 -0
- package/src/web/components/database/sql-completion-provider.ts +1 -26
- package/src/web/components/database/sql-query-editor.tsx +2 -1
- package/src/web/components/database/use-database.ts +16 -1
- package/src/web/components/database/use-glide-cell-content.ts +11 -6
- package/src/web/components/editor/code-editor.tsx +30 -56
- package/src/web/hooks/use-chat.ts +6 -3
- package/src/web/lib/attachment-marker-split.ts +44 -0
- package/src/web/lib/image-animation.ts +51 -0
- package/src/web/lib/image-resize-limits.ts +47 -0
- package/src/web/lib/image-resize.ts +133 -0
- package/dist/web/assets/api-client-DLWwuUkJ.js +0 -1
- package/dist/web/assets/api-settings-BFuaZrNC.js +0 -1
- package/dist/web/assets/chat-tab-bSOVoC5N.js +0 -15
- package/dist/web/assets/chevron-right-D7Pn5agw.js +0 -1
- package/dist/web/assets/circle-4g2coECH.js +0 -1
- package/dist/web/assets/clock-DbiNiwCG.js +0 -1
- package/dist/web/assets/code-DdKXqtf_.js +0 -1
- package/dist/web/assets/code-editor-COTa37oe.js +0 -10
- package/dist/web/assets/database-viewer-Dta7lSSk.js +0 -1
- package/dist/web/assets/explorer-body-BGjR2Gbt.js +0 -2
- package/dist/web/assets/explorer-window-content-DAhHh5EQ.js +0 -1
- package/dist/web/assets/glide-data-grid-BSsqxe2v.js +0 -138
- package/dist/web/assets/group-chat-tab-CMtzoaFc.js +0 -1
- package/dist/web/assets/index-BkHlMfPD.js +0 -52
- package/dist/web/assets/keybindings-store-_KfovSz7.js +0 -1
- package/dist/web/assets/loader-circle-DD65e9-7.js +0 -1
- package/dist/web/assets/markdown-renderer-DroTmKbJ.js +0 -1
- package/dist/web/assets/markdown-renderer-ntDi3Tx-.js +0 -3
- package/dist/web/assets/mobile-explorer-sheet-BP6iU4ma.js +0 -1
- package/dist/web/assets/notification-store-II5Kw9rD.js +0 -1
- package/dist/web/assets/play-CPbVzGgq.js +0 -1
- package/dist/web/assets/portal-container-context-BmG4--I-.js +0 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-CYZUjYTO.js +0 -2
- package/dist/web/assets/settings-tab-UPPycGRF.js +0 -1
- package/dist/web/assets/system-monitor-tab-DC_EerdI.js +0 -1
- package/dist/web/assets/system-monitor-window-content-D25DMNpw.js +0 -1
- package/dist/web/assets/team-member-sheet-DvmioeQL.js +0 -2
- package/dist/web/assets/use-open-team-member-CopUuDHG.js +0 -1
- package/dist/web/assets/vendor-markdown-CKTlmXbz.js +0 -291
- package/dist/web/assets/video-preview-C2_fdunE.js +0 -1
- package/dist/web/assets/x-Dzu9ecti.js +0 -1
- /package/dist/web/assets/{abap-Cjj2olMr.js → abap-C5DFF0s3.js} +0 -0
- /package/dist/web/assets/{actionscript-3-DCkW7kWC.js → actionscript-3-B0qyRNbc.js} +0 -0
- /package/dist/web/assets/{ada-AEWvZXuX.js → ada-bAJzplug.js} +0 -0
- /package/dist/web/assets/{andromeeda-D7cSpME6.js → andromeeda-DUCpISy6.js} +0 -0
- /package/dist/web/assets/{apache-DjvX0RSe.js → apache-BhAr84Z_.js} +0 -0
- /package/dist/web/assets/{apex-70xIZ0pH.js → apex-CRv8YnNq.js} +0 -0
- /package/dist/web/assets/{applescript-C3qKqxmA.js → applescript-CD-Cw8bt.js} +0 -0
- /package/dist/web/assets/{ara-iGt2zixk.js → ara-BUzeQYoG.js} +0 -0
- /package/dist/web/assets/{asciidoc-Bs2t-06W.js → asciidoc-CT2hTg-v.js} +0 -0
- /package/dist/web/assets/{asm-B-T0eqhE.js → asm-CAu35JeY.js} +0 -0
- /package/dist/web/assets/{aurora-x-D3h_9M3E.js → aurora-x-BeFJoIA1.js} +0 -0
- /package/dist/web/assets/{awk-CZwfwpPi.js → awk-Vau-DrGN.js} +0 -0
- /package/dist/web/assets/{ayu-dark-DulkglEA.js → ayu-dark-CLe-OTsQ.js} +0 -0
- /package/dist/web/assets/{ayu-light-DyNQiRpn.js → ayu-light-CJbQfu4M.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-CcEt4xkI.js → ayu-mirage-GWaKwdrx.js} +0 -0
- /package/dist/web/assets/{ballerina-OhQPjyHS.js → ballerina-DWu0USI5.js} +0 -0
- /package/dist/web/assets/{bat-DqKcJTUn.js → bat-_07Huw7E.js} +0 -0
- /package/dist/web/assets/{beancount-BdXlMi8T.js → beancount-EcvogrcF.js} +0 -0
- /package/dist/web/assets/{berry-DvgY6BkY.js → berry-BBB87SY7.js} +0 -0
- /package/dist/web/assets/{bibtex-BSRJq0yM.js → bibtex-DbueS87A.js} +0 -0
- /package/dist/web/assets/{bicep-DpAdumKt.js → bicep-tcmXnUo4.js} +0 -0
- /package/dist/web/assets/{bird2-rlbtp8di.js → bird2-B2qwilIR.js} +0 -0
- /package/dist/web/assets/{c-AjOLBXIs.js → c-Z58oivSt.js} +0 -0
- /package/dist/web/assets/{c3-DqIXz4Ia.js → c3-U40cGT9p.js} +0 -0
- /package/dist/web/assets/{cadence-DAJtUpOE.js → cadence-BFcD-LyM.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-DUPoeZfb.js → catppuccin-frappe-RkC9AX6J.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-CK3U68Ql.js → catppuccin-latte-Bsi4l-nO.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-BzteLnIX.js → catppuccin-macchiato-B8JIdTW-.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-ozLd0aBR.js → catppuccin-mocha-oH8wwrDj.js} +0 -0
- /package/dist/web/assets/{clarity-6KqEoBfz.js → clarity-C3WNeRCR.js} +0 -0
- /package/dist/web/assets/{clojure-B_uBL5C7.js → clojure-CFoAz7u9.js} +0 -0
- /package/dist/web/assets/{cmake-B3nxBNmj.js → cmake-Bcsqp9yR.js} +0 -0
- /package/dist/web/assets/{codeowners-DkS-51at.js → codeowners-DC2Z7heP.js} +0 -0
- /package/dist/web/assets/{codeql-rpr1z_mR.js → codeql-BfG68Al5.js} +0 -0
- /package/dist/web/assets/{common-lisp-BfNbv_W6.js → common-lisp-Z5P-lKQe.js} +0 -0
- /package/dist/web/assets/{coq-EAW-1nw0.js → coq-BJslqrnv.js} +0 -0
- /package/dist/web/assets/{csharp-BE3Gn5bq.js → csharp-DhhZTOTs.js} +0 -0
- /package/dist/web/assets/{css-Cr27CfPY.js → css-DiFIpMfE.js} +0 -0
- /package/dist/web/assets/{csv-B5VtaPtH.js → csv-DViLbHxI.js} +0 -0
- /package/dist/web/assets/{cue-jm6lYNrx.js → cue-CoG3mg2I.js} +0 -0
- /package/dist/web/assets/{cypher-DXkvK-pn.js → cypher-jTxC8NAB.js} +0 -0
- /package/dist/web/assets/{d-CgG1_yzd.js → d-6hKIUAbU.js} +0 -0
- /package/dist/web/assets/{dark-plus-CALzcIVB.js → dark-plus-D_-np2Sr.js} +0 -0
- /package/dist/web/assets/{dart-BEVoLlWg.js → dart-CVpxLpVn.js} +0 -0
- /package/dist/web/assets/{dax-H0BW3Q_P.js → dax-BPqJ8qIY.js} +0 -0
- /package/dist/web/assets/{desktop-BtYq1l_0.js → desktop-u_y5_Hr5.js} +0 -0
- /package/dist/web/assets/{diff-CnBYdfTg.js → diff-Duj1XMrd.js} +0 -0
- /package/dist/web/assets/{dist-CJJKpgR7.js → dist-CEZ4plgH.js} +0 -0
- /package/dist/web/assets/{docker-COBitpld.js → docker-8UGSCc7N.js} +0 -0
- /package/dist/web/assets/{dotenv-CG0x1m7c.js → dotenv-DVnj8kyR.js} +0 -0
- /package/dist/web/assets/{dracula-CLCi47X-.js → dracula-D0cEmSg7.js} +0 -0
- /package/dist/web/assets/{dracula-soft-BMv5cCAV.js → dracula-soft-DfGS5Yfr.js} +0 -0
- /package/dist/web/assets/{dream-maker-1Bd4OSww.js → dream-maker-qG8kMGTF.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-B-2zKLn-.js → emacs-lisp-iVqyMLyT.js} +0 -0
- /package/dist/web/assets/{everforest-dark-BGVynCbV.js → everforest-dark-Cruygd5U.js} +0 -0
- /package/dist/web/assets/{everforest-light--rn3R7Cm.js → everforest-light-BiJa8RHL.js} +0 -0
- /package/dist/web/assets/{fennel-SWg9Us_u.js → fennel-H6XSEIoz.js} +0 -0
- /package/dist/web/assets/{fish-B04oqgKk.js → fish-ho_7CxX6.js} +0 -0
- /package/dist/web/assets/{fluent-BefzAGMN.js → fluent-1l5ssJTF.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-alw7jMxb.js → fortran-free-form-Bo1x233A.js} +0 -0
- /package/dist/web/assets/{gdscript-BAyhvjCx.js → gdscript-DffFMp_k.js} +0 -0
- /package/dist/web/assets/{gdshader-Crl09X-B.js → gdshader-CvnpAiAF.js} +0 -0
- /package/dist/web/assets/{genie-BSIGRHu7.js → genie-2eOvAEzW.js} +0 -0
- /package/dist/web/assets/{gherkin-DJhSnSWH.js → gherkin-DL-5OUlV.js} +0 -0
- /package/dist/web/assets/{github-dark-r8C6qwug.js → github-dark-DrTZprgG.js} +0 -0
- /package/dist/web/assets/{github-dark-default-FVtXjcm8.js → github-dark-default-_mDH8IMP.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-CzrUPgSb.js → github-dark-dimmed-C48hJuLn.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-1s9rF5EB.js → github-dark-high-contrast-BQ-zGGNe.js} +0 -0
- /package/dist/web/assets/{github-light-BvIWCkl7.js → github-light-BYc90Yx8.js} +0 -0
- /package/dist/web/assets/{github-light-default-CVdhzrxV.js → github-light-default-Daecj4J5.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-CBAf6hjF.js → github-light-high-contrast-DoafCbsU.js} +0 -0
- /package/dist/web/assets/{gleam-DvL1q7aa.js → gleam-hUWWjmnp.js} +0 -0
- /package/dist/web/assets/{gn-BG5NvwAf.js → gn-7PJ3R70o.js} +0 -0
- /package/dist/web/assets/{gnuplot-BARnM-zG.js → gnuplot-V-Pxck7b.js} +0 -0
- /package/dist/web/assets/{go-DXvjjq4q.js → go-ARF6DprD.js} +0 -0
- /package/dist/web/assets/{groovy-CRylSKhr.js → groovy-CKGBIPOz.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-BVPjnqm7.js → gruvbox-dark-hard-Cm9wqdI3.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-U_PDgq0a.js → gruvbox-dark-medium-Cmsd6dG8.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-CkzNtpbr.js → gruvbox-dark-soft-BBGlGGCz.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-DfMmR2G8.js → gruvbox-light-hard-BCR7KmqG.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-BGCak-dd.js → gruvbox-light-medium-CP32bga8.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-DnpM3fkN.js → gruvbox-light-soft-qjxk8ps6.js} +0 -0
- /package/dist/web/assets/{haskell-DXgGIDD_.js → haskell-C5DzwGRa.js} +0 -0
- /package/dist/web/assets/{haxe-B7qlTpRz.js → haxe-C8f_jCVv.js} +0 -0
- /package/dist/web/assets/{hcl-ADWfVe_Q.js → hcl-DetJkf6E.js} +0 -0
- /package/dist/web/assets/{hjson-BaM9gLbq.js → hjson-DeZhwIdo.js} +0 -0
- /package/dist/web/assets/{hlsl-iqaRA4MG.js → hlsl-SMIDLRyX.js} +0 -0
- /package/dist/web/assets/{horizon-BPpTnhB9.js → horizon-BMAVDxMx.js} +0 -0
- /package/dist/web/assets/{horizon-bright-yBLg3QbE.js → horizon-bright-3BaV_VGD.js} +0 -0
- /package/dist/web/assets/{houston-eFOp0ySL.js → houston-BWBbEPvb.js} +0 -0
- /package/dist/web/assets/{hy-BALeMn3i.js → hy-DcsmE-Nm.js} +0 -0
- /package/dist/web/assets/{imba-CdGUm_GD.js → imba-CnkgnYMf.js} +0 -0
- /package/dist/web/assets/{ini-DG7imsqe.js → ini-BnvTdau7.js} +0 -0
- /package/dist/web/assets/{java-D5GTT474.js → java-BLFACXat.js} +0 -0
- /package/dist/web/assets/{javascript-V7WwJvRm.js → javascript-BKcFWENk.js} +0 -0
- /package/dist/web/assets/{json-B9FhwAgR.js → json-3AJtJADf.js} +0 -0
- /package/dist/web/assets/{json5--Abk7MUy.js → json5-DLyDVlwe.js} +0 -0
- /package/dist/web/assets/{jsonc-C8jb-Z6x.js → jsonc-B5DgF1jW.js} +0 -0
- /package/dist/web/assets/{jsonl-CaJNxfkm.js → jsonl-CV_O71a2.js} +0 -0
- /package/dist/web/assets/{jsonnet-CPrKMSCU.js → jsonnet-DZfPLgYz.js} +0 -0
- /package/dist/web/assets/{jssm-DEu-9_uo.js → jssm-Cr1zYOsY.js} +0 -0
- /package/dist/web/assets/{jsx-MfAsCWDW.js → jsx-VN-JS2_q.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-DnbcAduF.js → kanagawa-dragon-D9CcT3xg.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-yxsY3ew8.js → kanagawa-lotus-DLXsB8fr.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-C2RyvZaM.js → kanagawa-wave-weDs3Wjz.js} +0 -0
- /package/dist/web/assets/{kdl-CvssmvcU.js → kdl-0XF56jnT.js} +0 -0
- /package/dist/web/assets/{kotlin-jufO49mU.js → kotlin-BPkH9vzk.js} +0 -0
- /package/dist/web/assets/{kusto-BAFnylR7.js → kusto-DMVpADb9.js} +0 -0
- /package/dist/web/assets/{laserwave-BgxFC4Ge.js → laserwave-Cl6T2cGP.js} +0 -0
- /package/dist/web/assets/{lean-Bgt24V1D.js → lean-BvwzQGl1.js} +0 -0
- /package/dist/web/assets/{less-Bk_UaqsM.js → less-DI_03xkC.js} +0 -0
- /package/dist/web/assets/{light-plus-nMxFfInN.js → light-plus-hIiIsQN_.js} +0 -0
- /package/dist/web/assets/{llvm-3_PASDOO.js → llvm-B5hjjSWc.js} +0 -0
- /package/dist/web/assets/{log-CKm0CxB1.js → log-D28raQiE.js} +0 -0
- /package/dist/web/assets/{logo-DFP1_5U6.js → logo-6Cdd4ASb.js} +0 -0
- /package/dist/web/assets/{luau-BcZnoyh3.js → luau-CK9nKpOX.js} +0 -0
- /package/dist/web/assets/{make-CpNSwgwT.js → make-CVnxKCB6.js} +0 -0
- /package/dist/web/assets/{markdown-DhU3Djpv.js → markdown-B9mMQzD5.js} +0 -0
- /package/dist/web/assets/{material-theme-DD5L3OqF.js → material-theme-BZSs2o0j.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-CG2_tD3R.js → material-theme-darker-BeAQBzQ_.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-CzNkUc9Y.js → material-theme-lighter-BAe0IXcs.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-BtLiAaF_.js → material-theme-ocean-CiPIf0Np.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-Brvjz035.js → material-theme-palenight-DJBb4_XN.js} +0 -0
- /package/dist/web/assets/{matlab-CCY0KtMD.js → matlab-CY0dbp_s.js} +0 -0
- /package/dist/web/assets/{mdx-CH6YY0aS.js → mdx-DU9zbF7h.js} +0 -0
- /package/dist/web/assets/{mermaid-Bqh3gFI0.js → mermaid-UmmjGwdh.js} +0 -0
- /package/dist/web/assets/{min-dark-BUwfTzI6.js → min-dark-C59GkzOm.js} +0 -0
- /package/dist/web/assets/{min-light-oLanXCOk.js → min-light-B9QglrdQ.js} +0 -0
- /package/dist/web/assets/{mipsasm-CTh3hPWK.js → mipsasm-Dc6Jsr_D.js} +0 -0
- /package/dist/web/assets/{mojo-DXdszZKa.js → mojo-D5ij-nGW.js} +0 -0
- /package/dist/web/assets/{monokai-TNE_tRxc.js → monokai-QAtE_yCP.js} +0 -0
- /package/dist/web/assets/{moonbit-jrk-Gx1X.js → moonbit-BMtIdiTL.js} +0 -0
- /package/dist/web/assets/{move-DItn61bH.js → move-BB_jMHYZ.js} +0 -0
- /package/dist/web/assets/{narrat-3kB4dkh0.js → narrat-BvvoKGVn.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-CSKtwETr.js → nextflow-groovy-CqxtAsnF.js} +0 -0
- /package/dist/web/assets/{night-owl-Dm91nSIq.js → night-owl-CLoE9kjt.js} +0 -0
- /package/dist/web/assets/{night-owl-light-riwrcPRg.js → night-owl-light-BwIFT5Xd.js} +0 -0
- /package/dist/web/assets/{nix-sLgl9zGF.js → nix-BKlnVT9r.js} +0 -0
- /package/dist/web/assets/{nord-TWkJPI6M.js → nord-B7R1KRAK.js} +0 -0
- /package/dist/web/assets/{nushell-CDZi4-Gf.js → nushell-CzUme-NJ.js} +0 -0
- /package/dist/web/assets/{objective-c-DW9PboNp.js → objective-c-D-UKKiOc.js} +0 -0
- /package/dist/web/assets/{objective-cpp-2XCCX6q1.js → objective-cpp-BucWn4Fx.js} +0 -0
- /package/dist/web/assets/{ocaml-CxPdnjrh.js → ocaml-EJCNXMGo.js} +0 -0
- /package/dist/web/assets/{odin-BJjlKpkM.js → odin-CEjnRU_3.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-K1KOmBZ7.js → one-dark-pro-Bv469NX5.js} +0 -0
- /package/dist/web/assets/{one-light-DQB-vUyZ.js → one-light-BlzO5RWI.js} +0 -0
- /package/dist/web/assets/{openscad-DCoLwn4j.js → openscad-Czl6alLw.js} +0 -0
- /package/dist/web/assets/{pascal-z2ktbbWp.js → pascal-BI9fLvG4.js} +0 -0
- /package/dist/web/assets/{pkl-Dyq4-v6z.js → pkl-ChQLsyjU.js} +0 -0
- /package/dist/web/assets/{plastic-C4Rq5PnD.js → plastic-DGL3mtdL.js} +0 -0
- /package/dist/web/assets/{plsql-BCIjO63y.js → plsql-BJDjqcWp.js} +0 -0
- /package/dist/web/assets/{po-_kXpd_0n.js → po-DK5O3OpF.js} +0 -0
- /package/dist/web/assets/{poimandres-C9XggjB3.js → poimandres-CnuqZT4T.js} +0 -0
- /package/dist/web/assets/{polar-DV_1L04z.js → polar-DCsUxSsQ.js} +0 -0
- /package/dist/web/assets/{postcss-BuqS91mQ.js → postcss-8UjdFmis.js} +0 -0
- /package/dist/web/assets/{powerquery-DK13B2Dc.js → powerquery-CAp0CGQj.js} +0 -0
- /package/dist/web/assets/{powershell-tkuhhtuP.js → powershell-CbBsKZJP.js} +0 -0
- /package/dist/web/assets/{prisma-WNXfBtpr.js → prisma-BnNpXCJN.js} +0 -0
- /package/dist/web/assets/{prolog-BzEdNKfO.js → prolog-CC-l9E8Y.js} +0 -0
- /package/dist/web/assets/{proto-B5meVsrA.js → proto-CV_1k2rL.js} +0 -0
- /package/dist/web/assets/{puppet-C7h_p4r2.js → puppet-2D8HU2bv.js} +0 -0
- /package/dist/web/assets/{purescript-qH1KxAs6.js → purescript-BEJAORgU.js} +0 -0
- /package/dist/web/assets/{python-DeTIYd5V.js → python-BBdbb7nn.js} +0 -0
- /package/dist/web/assets/{qmldir-4Rm5AvD3.js → qmldir-D5ZYI4Ie.js} +0 -0
- /package/dist/web/assets/{qss-YRbeNtr0.js → qss-BOJVGyXE.js} +0 -0
- /package/dist/web/assets/{r-DHwHK0Jh.js → r-B5dlEtkj.js} +0 -0
- /package/dist/web/assets/{racket-gKn4SFLQ.js → racket-BUAEygJ6.js} +0 -0
- /package/dist/web/assets/{raku-DWTmAj3A.js → raku-BG0Ss0ID.js} +0 -0
- /package/dist/web/assets/{red-BFuHRZFS.js → red-C5yyQbtG.js} +0 -0
- /package/dist/web/assets/{reg-DYoOF5tH.js → reg-DHoBWtLf.js} +0 -0
- /package/dist/web/assets/{regexp-6rXQHD2-.js → regexp-DDtJrlKv.js} +0 -0
- /package/dist/web/assets/{rel-Z_tTruCU.js → rel-CR6WJDRD.js} +0 -0
- /package/dist/web/assets/{riscv-DfTfKuvU.js → riscv-2V9tC-nx.js} +0 -0
- /package/dist/web/assets/{ron-CsUtW8gQ.js → ron-QLTmqGUw.js} +0 -0
- /package/dist/web/assets/{rose-pine-Dng_vCo6.js → rose-pine-be0__X3A.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-Ds88Nezf.js → rose-pine-dawn-n_WQJwsW.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-DWpzrqNz.js → rose-pine-moon-t2aLtzgB.js} +0 -0
- /package/dist/web/assets/{rosmsg-h2sO84oD.js → rosmsg-Ce1JD8vt.js} +0 -0
- /package/dist/web/assets/{rust-Dx86yfCi.js → rust-5fHH-ZoW.js} +0 -0
- /package/dist/web/assets/{sass-BwwVhdT9.js → sass-DPmGFiOJ.js} +0 -0
- /package/dist/web/assets/{scala-WJqNEN-w.js → scala-BRMVFbZF.js} +0 -0
- /package/dist/web/assets/{scheme-uwj-t1Rp.js → scheme-BMSJWevF.js} +0 -0
- /package/dist/web/assets/{sdbl-BYy5hgEz.js → sdbl-DTZ-pEHI.js} +0 -0
- /package/dist/web/assets/{shellscript-DWTW_w2K.js → shellscript-6-9yg5Cz.js} +0 -0
- /package/dist/web/assets/{slack-dark-BvhGq5vQ.js → slack-dark-DKJgiR7e.js} +0 -0
- /package/dist/web/assets/{slack-ochin-C9g0ZRFr.js → slack-ochin-WjehVgyK.js} +0 -0
- /package/dist/web/assets/{smalltalk-ZEiZ6i7U.js → smalltalk-BChf4SN1.js} +0 -0
- /package/dist/web/assets/{snazzy-light-29OA24q5.js → snazzy-light-C1WAwOx6.js} +0 -0
- /package/dist/web/assets/{solarized-dark-Bnq9IOn_.js → solarized-dark-BxREZrGx.js} +0 -0
- /package/dist/web/assets/{solarized-light-CYdI7T5g.js → solarized-light-DG29B1pR.js} +0 -0
- /package/dist/web/assets/{solidity-DcuIDB-_.js → solidity-bDbtGwdR.js} +0 -0
- /package/dist/web/assets/{splunk-D5ONE91g.js → splunk-Ceqx5njI.js} +0 -0
- /package/dist/web/assets/{sql-DIwJJDU-.js → sql-OwMEKbz8.js} +0 -0
- /package/dist/web/assets/{ssh-config-D8717tLI.js → ssh-config-WAR3lu2k.js} +0 -0
- /package/dist/web/assets/{stylus-Cm1lO4Y6.js → stylus-Cjmoz3d8.js} +0 -0
- /package/dist/web/assets/{swift-X8oWpSeU.js → swift-S_ZSBG1q.js} +0 -0
- /package/dist/web/assets/{synthwave-84-DF-t1UKY.js → synthwave-84-DiUeWMSh.js} +0 -0
- /package/dist/web/assets/{system-verilog-JYyHFpPf.js → system-verilog-RRjqPjUV.js} +0 -0
- /package/dist/web/assets/{systemd-Da-5Ma_c.js → systemd-BD3SkXoU.js} +0 -0
- /package/dist/web/assets/{talonscript-CXraVPho.js → talonscript-DWefISOm.js} +0 -0
- /package/dist/web/assets/{tasl-BCmWezH9.js → tasl-DCjSS-09.js} +0 -0
- /package/dist/web/assets/{tcl-BTufzfbf.js → tcl-DTYqT_66.js} +0 -0
- /package/dist/web/assets/{terraform-CtUA9c8W.js → terraform-CkbP5QnR.js} +0 -0
- /package/dist/web/assets/{tokyo-night-BCThYXmz.js → tokyo-night-qVSqO9xy.js} +0 -0
- /package/dist/web/assets/{toml-D5YlDr6Y.js → toml-D9F5Osqs.js} +0 -0
- /package/dist/web/assets/{tsv-CBB-P_Sl.js → tsv-C5tpHoxm.js} +0 -0
- /package/dist/web/assets/{tsx-D5B4juDp.js → tsx-CMWkGtGm.js} +0 -0
- /package/dist/web/assets/{turtle-D8PCeJ-v.js → turtle-Cn4z3xOi.js} +0 -0
- /package/dist/web/assets/{typescript-CMPnTawp.js → typescript-DSyAEWsn.js} +0 -0
- /package/dist/web/assets/{typespec-CHxldxIW.js → typespec-B93Qap7y.js} +0 -0
- /package/dist/web/assets/{typst-Q8CqX5OR.js → typst-DtlOmFlX.js} +0 -0
- /package/dist/web/assets/{v-Bf2hKAMu.js → v-Dfzf-szS.js} +0 -0
- /package/dist/web/assets/{vala-Cbete-UE.js → vala-CyiQZQ9f.js} +0 -0
- /package/dist/web/assets/{vb-CA-xWAcX.js → vb-C7DGSldd.js} +0 -0
- /package/dist/web/assets/{verilog-B3wb8CfT.js → verilog-kF-hClY2.js} +0 -0
- /package/dist/web/assets/{vesper-D6r3VJq_.js → vesper-BmKcADuz.js} +0 -0
- /package/dist/web/assets/{vhdl-C42S_797.js → vhdl-CDnb16KJ.js} +0 -0
- /package/dist/web/assets/{viml-BePrIAs8.js → viml-B8cs6Ady.js} +0 -0
- /package/dist/web/assets/{vitesse-black-DRqn61Ck.js → vitesse-black-BR6o9Gto.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-39_8UE-w.js → vitesse-dark-Dye14-KB.js} +0 -0
- /package/dist/web/assets/{vitesse-light-B1jwrxDm.js → vitesse-light-BLpswIxf.js} +0 -0
- /package/dist/web/assets/{vyper-tDJv3imF.js → vyper-DU51Z97W.js} +0 -0
- /package/dist/web/assets/{wasm-DR7eskZH.js → wasm-CVqlJAR2.js} +0 -0
- /package/dist/web/assets/{wenyan-CezVfqN3.js → wenyan-DhCa3hCn.js} +0 -0
- /package/dist/web/assets/{wgsl-CwOcIiIG.js → wgsl-Bj1Ec4vv.js} +0 -0
- /package/dist/web/assets/{wikitext-C5coomYc.js → wikitext-C90DHNa0.js} +0 -0
- /package/dist/web/assets/{wit-C9yyX6-U.js → wit-DUgbOD7Z.js} +0 -0
- /package/dist/web/assets/{wolfram-BWi0rMAa.js → wolfram-BgwUqD1I.js} +0 -0
- /package/dist/web/assets/{yaml-CGXhKUtT.js → yaml-DEQulj-8.js} +0 -0
- /package/dist/web/assets/{zenscript-Do7WLoJh.js → zenscript-C7Jp7TgT.js} +0 -0
- /package/dist/web/assets/{zig-Cr80Qrru.js → zig-9x4qXYLg.js} +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Is the network back yet?" gate for the named-tunnel spawn path.
|
|
3
|
+
*
|
|
4
|
+
* A connector started before the machine's Wi-Fi has reassociated cannot
|
|
5
|
+
* register, and the readiness wait then expires on a failure that would have
|
|
6
|
+
* cleared itself seconds later. Observed live after a laptop resumed from
|
|
7
|
+
* sleep: the supervisor regenerated the tunnel immediately, named timed out,
|
|
8
|
+
* and the permanent hostname served 530 until someone intervened.
|
|
9
|
+
*
|
|
10
|
+
* Only the named path waits: a quick tunnel that fails is retried with a fresh
|
|
11
|
+
* URL anyway, while a named failure costs the user their fixed address.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Cheap, unauthenticated, and the exact host the connector needs anyway. */
|
|
15
|
+
const PROBE_URL = "https://api.cloudflare.com/client/v4/";
|
|
16
|
+
|
|
17
|
+
export interface NetworkReadyOptions {
|
|
18
|
+
/** Give up after this long and let the caller try regardless. */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
/** Wait between probes. */
|
|
21
|
+
intervalMs?: number;
|
|
22
|
+
/** Per-probe timeout. */
|
|
23
|
+
probeTimeoutMs?: number;
|
|
24
|
+
/** Injectable for tests; defaults to a HEAD against Cloudflare's API. */
|
|
25
|
+
probe?: () => Promise<boolean>;
|
|
26
|
+
sleep?: (ms: number) => Promise<void>;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultSleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
31
|
+
|
|
32
|
+
async function defaultProbe(probeTimeoutMs: number): Promise<boolean> {
|
|
33
|
+
try {
|
|
34
|
+
// Any HTTP answer proves DNS + TCP + TLS to Cloudflare work; the status
|
|
35
|
+
// code is irrelevant (401/404 are just as good a signal as 200).
|
|
36
|
+
await fetch(PROBE_URL, { method: "HEAD", signal: AbortSignal.timeout(probeTimeoutMs) });
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolves `true` as soon as Cloudflare is reachable, `false` if the budget
|
|
45
|
+
* runs out. Never throws: a failed wait is a hint, not a veto — the caller
|
|
46
|
+
* still attempts the spawn, because a probe can fail for reasons (proxy, DNS
|
|
47
|
+
* filtering) that would not stop the connector itself.
|
|
48
|
+
*/
|
|
49
|
+
export async function waitForCloudflareReachable(opts: NetworkReadyOptions = {}): Promise<boolean> {
|
|
50
|
+
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
51
|
+
const intervalMs = opts.intervalMs ?? 2_000;
|
|
52
|
+
const probeTimeoutMs = opts.probeTimeoutMs ?? 3_000;
|
|
53
|
+
const probe = opts.probe ?? (() => defaultProbe(probeTimeoutMs));
|
|
54
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
55
|
+
const now = opts.now ?? Date.now;
|
|
56
|
+
|
|
57
|
+
const deadline = now() + timeoutMs;
|
|
58
|
+
for (;;) {
|
|
59
|
+
if (await probe()) return true;
|
|
60
|
+
if (now() >= deadline) return false;
|
|
61
|
+
await sleep(intervalMs);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
import type { ResolvedTunnelConfig, TunnelMode } from "./named-tunnel/named-tunnel-config.ts";
|
|
23
23
|
import { readTunnelConfigFresh, chooseTunnelSpawn } from "./named-tunnel/named-tunnel-runtime.ts";
|
|
24
24
|
import { waitForLogLine } from "./named-tunnel/named-tunnel-readiness.ts";
|
|
25
|
+
import { nextNamedRetryDelayMs } from "./named-tunnel/named-tunnel-retry.ts";
|
|
26
|
+
import { waitForCloudflareReachable } from "./named-tunnel/network-ready.ts";
|
|
25
27
|
import { decideNamedProbeAction, publicHostnameIsOurs } from "./named-tunnel/named-tunnel-probe-state.ts";
|
|
26
28
|
import { isCloudflaredPid } from "./tunnel-registry.service.ts";
|
|
27
29
|
import { getQuickTunnelArgs } from "./cloudflared.service.ts";
|
|
@@ -102,6 +104,18 @@ let namedTunnelMode: ResolvedTunnelConfig | null = null;
|
|
|
102
104
|
// but live=quick, and every status write / throttle / restart decision must
|
|
103
105
|
// key off what's actually running.
|
|
104
106
|
let lastSpawnMode: TunnelMode = "quick";
|
|
107
|
+
// How long to wait for the network before attempting a named tunnel.
|
|
108
|
+
const NETWORK_WAIT_MS = 60_000;
|
|
109
|
+
// Falling back to quick used to be permanent until the next supervisor start.
|
|
110
|
+
// These drive a bounded set of retries so a transient failure (network not up
|
|
111
|
+
// yet after a resume) heals itself instead of leaving the hostname dark.
|
|
112
|
+
let namedRetryAttempt = 0;
|
|
113
|
+
let namedRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
114
|
+
// A retry that finds the machine still offline re-arms the same rung instead
|
|
115
|
+
// of burning it. Bounded so a laptop left off the network doesn't log a
|
|
116
|
+
// deferral every minute for the rest of its uptime.
|
|
117
|
+
let namedRetryDeferrals = 0;
|
|
118
|
+
const MAX_NAMED_RETRY_DEFERRALS = 10;
|
|
105
119
|
// Set once the named probe has already tried a restart-and-hope; a second
|
|
106
120
|
// consecutive failure then warns and stops instead of looping kills forever.
|
|
107
121
|
let namedProbeRestartAttempted = false;
|
|
@@ -293,6 +307,72 @@ async function ensureBindablePort(preferred: number, host: string): Promise<numb
|
|
|
293
307
|
* The trycloudflare URL will change — unavoidable, the old quick-tunnel session
|
|
294
308
|
* is dead. Fire-and-forget: spawnTunnel's own loop owns liveness afterwards.
|
|
295
309
|
*/
|
|
310
|
+
/** Cancel a pending named retry — a newer decision supersedes it. */
|
|
311
|
+
function cancelNamedRetry(): void {
|
|
312
|
+
if (namedRetryTimer) { clearTimeout(namedRetryTimer); namedRetryTimer = null; }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* After a downgrade to quick, try named again on a bounded backoff.
|
|
317
|
+
*
|
|
318
|
+
* Re-checks everything at fire time rather than trusting the state captured
|
|
319
|
+
* when the timer was set: the user may have switched to quick deliberately, a
|
|
320
|
+
* `retunnel` may already have restored named, or the supervisor may be going
|
|
321
|
+
* down. Any of those makes the retry pointless or actively wrong.
|
|
322
|
+
*/
|
|
323
|
+
function scheduleNamedRetry(port: number): void {
|
|
324
|
+
cancelNamedRetry();
|
|
325
|
+
const delay = nextNamedRetryDelayMs(namedRetryAttempt);
|
|
326
|
+
if (delay === null) {
|
|
327
|
+
log("WARN", "Named tunnel still failing after the last retry — staying on the quick URL");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const attemptNumber = namedRetryAttempt + 1;
|
|
331
|
+
log("INFO", `Named tunnel downgraded to quick — retrying in ${Math.round(delay / 1000)}s (#${attemptNumber})`);
|
|
332
|
+
namedRetryTimer = setTimeout(() => {
|
|
333
|
+
namedRetryTimer = null;
|
|
334
|
+
if (shuttingDown || getState() !== "running") return;
|
|
335
|
+
if (namedTunnelMode?.mode !== "named") return; // user switched to quick meanwhile
|
|
336
|
+
if (lastSpawnMode === "named") return; // already recovered
|
|
337
|
+
// Restarting tears down the quick tunnel that is currently serving the
|
|
338
|
+
// machine, so only do it once the network can actually carry a connector —
|
|
339
|
+
// otherwise a still-offline laptop loses its working temporary URL too and
|
|
340
|
+
// gets a new one for nothing. Offline: re-arm the same rung, don't burn it.
|
|
341
|
+
void waitForCloudflareReachable({ timeoutMs: 5_000, intervalMs: 1_000 }).then((reachable) => {
|
|
342
|
+
if (shuttingDown || getState() !== "running") return;
|
|
343
|
+
if (namedTunnelMode?.mode !== "named" || lastSpawnMode === "named") return;
|
|
344
|
+
if (!reachable) {
|
|
345
|
+
if (++namedRetryDeferrals > MAX_NAMED_RETRY_DEFERRALS) {
|
|
346
|
+
log("WARN", "Named retry abandoned — network still down after repeated attempts");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
log("INFO", `Named retry #${attemptNumber} deferred — network still down`);
|
|
350
|
+
scheduleNamedRetry(port);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
namedRetryDeferrals = 0;
|
|
354
|
+
namedRetryAttempt = attemptNumber;
|
|
355
|
+
log("INFO", `Retrying the named tunnel (#${attemptNumber})`);
|
|
356
|
+
restartTunnel(port);
|
|
357
|
+
});
|
|
358
|
+
}, delay);
|
|
359
|
+
namedRetryTimer.unref?.();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Called once per successful spawn: arm the retry ladder when we had to fall
|
|
364
|
+
* back to quick, clear it the moment named is actually live again.
|
|
365
|
+
*/
|
|
366
|
+
function noteSpawnOutcomeForNamedRetry(mode: TunnelMode, downgraded: boolean, port: number): void {
|
|
367
|
+
if (mode === "named") {
|
|
368
|
+
cancelNamedRetry();
|
|
369
|
+
namedRetryAttempt = 0;
|
|
370
|
+
namedRetryDeferrals = 0;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (downgraded) scheduleNamedRetry(port);
|
|
374
|
+
}
|
|
375
|
+
|
|
296
376
|
function restartTunnel(port: number) {
|
|
297
377
|
lastTunnelRegenAt = Date.now();
|
|
298
378
|
if (tunnelChild) { try { tunnelChild.kill(); } catch {} tunnelChild = null; }
|
|
@@ -744,6 +824,15 @@ export async function spawnTunnel(port: number, generation: number = ++tunnelGen
|
|
|
744
824
|
}
|
|
745
825
|
|
|
746
826
|
const attempts = buildTunnelAttempts(namedTunnelMode, port);
|
|
827
|
+
// Only the named path pays for this: its failure costs the user their fixed
|
|
828
|
+
// address, while a quick tunnel just retries with a new URL. Spawning a
|
|
829
|
+
// connector before the machine's network is back is the difference between
|
|
830
|
+
// "reconnects in 5s" and "hostname dark until someone notices".
|
|
831
|
+
if (attempts[0]?.mode === "named") {
|
|
832
|
+
const reachable = await waitForCloudflareReachable({ timeoutMs: NETWORK_WAIT_MS });
|
|
833
|
+
if (!reachable) log("WARN", `Cloudflare unreachable after ${NETWORK_WAIT_MS / 1000}s — attempting the tunnel anyway`);
|
|
834
|
+
if (generation !== tunnelGeneration) return; // superseded while waiting
|
|
835
|
+
}
|
|
747
836
|
const logPath = cloudflaredLogPath();
|
|
748
837
|
// Truncate stale content from a prior generation; best-effort — Windows can
|
|
749
838
|
// silently fail this while a previous detached cloudflared still holds the
|
|
@@ -821,6 +910,7 @@ export async function spawnTunnel(port: number, generation: number = ++tunnelGen
|
|
|
821
910
|
adoptedTunnelPid = winPid;
|
|
822
911
|
tunnelChild = null;
|
|
823
912
|
lastSpawnMode = successfulMode;
|
|
913
|
+
noteSpawnOutcomeForNamedRetry(successfulMode, downgraded, port);
|
|
824
914
|
// `namedProbeRestartAttempted` is deliberately NOT touched here — see the
|
|
825
915
|
// probe's `decideNamedProbeAction` state machine. A successful spawn only
|
|
826
916
|
// proves cloudflared reconnected to Cloudflare's edge, not that the
|
|
@@ -908,6 +998,7 @@ export async function spawnTunnel(port: number, generation: number = ++tunnelGen
|
|
|
908
998
|
}
|
|
909
999
|
|
|
910
1000
|
lastSpawnMode = successfulMode;
|
|
1001
|
+
noteSpawnOutcomeForNamedRetry(successfulMode, downgraded, port);
|
|
911
1002
|
// `namedProbeRestartAttempted` is deliberately NOT touched here — see the
|
|
912
1003
|
// probe's `decideNamedProbeAction` state machine and the win32 branch above
|
|
913
1004
|
// for why a bare spawn success must never re-arm the one-restart budget.
|
|
@@ -1708,6 +1799,7 @@ export async function softStop() {
|
|
|
1708
1799
|
export function shutdown() {
|
|
1709
1800
|
if (shuttingDown) return;
|
|
1710
1801
|
shuttingDown = true;
|
|
1802
|
+
cancelNamedRetry();
|
|
1711
1803
|
log("INFO", "Supervisor shutting down");
|
|
1712
1804
|
|
|
1713
1805
|
// Unblock if paused
|
|
@@ -1910,6 +2002,10 @@ export async function runSupervisor(opts: {
|
|
|
1910
2002
|
// over from the outage trips "restart once" after a handful of probes.
|
|
1911
2003
|
namedProbeRestartAttempted = false;
|
|
1912
2004
|
tunnelFailCount = 0;
|
|
2005
|
+
// A deliberate retunnel supersedes any pending automatic retry, and
|
|
2006
|
+
// restarts the ladder so a later transient failure gets a full budget.
|
|
2007
|
+
cancelNamedRetry();
|
|
2008
|
+
namedRetryAttempt = 0;
|
|
1913
2009
|
restartTunnel(_opts.port);
|
|
1914
2010
|
// Deliberate fall-through (no return): a bare `ppm restart` sends a
|
|
1915
2011
|
// bare SIGUSR2 with no command file of its own, and a `retunnel` that
|
|
@@ -2059,6 +2155,8 @@ export async function runSupervisor(opts: {
|
|
|
2059
2155
|
// rather than inheriting a stale exhausted one (flag AND counter).
|
|
2060
2156
|
namedProbeRestartAttempted = false;
|
|
2061
2157
|
tunnelFailCount = 0;
|
|
2158
|
+
cancelNamedRetry();
|
|
2159
|
+
namedRetryAttempt = 0;
|
|
2062
2160
|
restartTunnel(_opts.port);
|
|
2063
2161
|
});
|
|
2064
2162
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readImageDimensions } from "./image-dimensions.ts";
|
|
2
2
|
import { base64ByteLength, imagePlaceholderText } from "../shared/tool-result-content.ts";
|
|
3
|
+
import { MAX_IMAGE_DIMENSION } from "../shared/image-limits.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Auditing and removal of image payloads inside a Claude Code session transcript.
|
|
@@ -11,9 +12,14 @@ import { base64ByteLength, imagePlaceholderText } from "../shared/tool-result-co
|
|
|
11
12
|
*
|
|
12
13
|
* Images live in two places, and they are not equally safe to remove. A tool result can be
|
|
13
14
|
* produced again by re-reading the file it came from, so those are removed by default. An
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* attachment is left alone unless the caller opts in.
|
|
16
|
+
*
|
|
17
|
+
* That default is now more cautious than it needs to be for images this app sent itself: the
|
|
18
|
+
* composer uploads a copy before embedding the payload, and leaves the path in the message, so
|
|
19
|
+
* such an attachment is as re-readable as a tool result. It stays opt-in because a transcript
|
|
20
|
+
* also collects attachments from elsewhere — another client, an older build, a session shared
|
|
21
|
+
* with the CLI — and for those the payload really is the last copy. The caller must be able to
|
|
22
|
+
* reach it either way: one oversized attachment fails every later turn of the session.
|
|
17
23
|
*
|
|
18
24
|
* A tool result's payload sits at `message.content[].content[]`; an attachment sits directly
|
|
19
25
|
* at `message.content[]`. A record also carries an image-shaped echo at the top-level
|
|
@@ -28,7 +34,7 @@ import { base64ByteLength, imagePlaceholderText } from "../shared/tool-result-co
|
|
|
28
34
|
* Comparisons here are `>=` rather than `>` for that reason: treating 2000px as acceptable
|
|
29
35
|
* hides the one image the API is rejecting and makes a cleanup look like a no-op.
|
|
30
36
|
*/
|
|
31
|
-
export const MANY_IMAGE_DIMENSION_LIMIT =
|
|
37
|
+
export const MANY_IMAGE_DIMENSION_LIMIT = MAX_IMAGE_DIMENSION;
|
|
32
38
|
|
|
33
39
|
/** Base64 characters decoded to inspect a header — a JPEG frame can sit behind EXIF. */
|
|
34
40
|
const HEADER_CHARS = 4096;
|
|
@@ -38,9 +44,10 @@ export type StripMode = "oversized" | "all";
|
|
|
38
44
|
/** Which of the two image locations a scan or strip should cover. */
|
|
39
45
|
export interface ImageScopeOpts {
|
|
40
46
|
/**
|
|
41
|
-
* Also cover images the user attached to their own messages. Off by default
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
* Also cover images the user attached to their own messages. Off by default, because a
|
|
48
|
+
* transcript may hold attachments this app never uploaded — from another client or an older
|
|
49
|
+
* build — for which the embedded payload is the last copy. Attachments the composer sent
|
|
50
|
+
* keep an uploaded file and its path, so removing those is recoverable.
|
|
44
51
|
*/
|
|
45
52
|
includeAttachments?: boolean;
|
|
46
53
|
}
|
|
@@ -117,10 +117,15 @@ let upgradeInProgress = false;
|
|
|
117
117
|
|
|
118
118
|
/** Build the global-install command. Absolute runtime path (not a bare
|
|
119
119
|
* "bun"/"npm") because autostart (launchd/systemd) may not have the runtime's
|
|
120
|
-
* bin dir on $PATH, where a bare name fails with "Executable not found".
|
|
120
|
+
* bin dir on $PATH, where a bare name fails with "Executable not found".
|
|
121
|
+
*
|
|
122
|
+
* bun gets `--no-cache`: it resolves versions from a cached package manifest
|
|
123
|
+
* and, right after a release, that manifest predates the version the update
|
|
124
|
+
* check just found — `bun install -g pkg@new` then fails with "No version
|
|
125
|
+
* matching … (but package exists)" until the cache happens to expire. */
|
|
121
126
|
export function buildUpgradeCommand(method: "bun" | "npm", pkg: string): string[] {
|
|
122
127
|
return method === "bun"
|
|
123
|
-
? [process.execPath, "install", "-g", pkg]
|
|
128
|
+
? [process.execPath, "install", "-g", "--no-cache", pkg]
|
|
124
129
|
: [resolveNpmBin(), "install", "-g", pkg];
|
|
125
130
|
}
|
|
126
131
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-image dimension ceiling the API applies, and the arithmetic for staying under it.
|
|
3
|
+
*
|
|
4
|
+
* Shared because both ends of the pipeline need the same number: the browser downscales an
|
|
5
|
+
* attachment before it is ever uploaded, and the transcript tooling reports on images that
|
|
6
|
+
* slipped in from elsewhere (another client, an older build) still carrying the full size.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Longest side, in pixels, at which the API starts refusing an image once a request carries
|
|
11
|
+
* several of them. The value is a ceiling, so an image measuring exactly this is already over
|
|
12
|
+
* it — comparisons are `>=`, and a downscale targets strictly below.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_IMAGE_DIMENSION = 2000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Dimensions that bring an image under the cap, or null when it is already small enough.
|
|
18
|
+
*
|
|
19
|
+
* Scales the longest side to one pixel below the cap and takes the other side with it, so the
|
|
20
|
+
* aspect ratio survives and the result cannot land back on the boundary through rounding.
|
|
21
|
+
*/
|
|
22
|
+
export function fitWithin(
|
|
23
|
+
width: number,
|
|
24
|
+
height: number,
|
|
25
|
+
max: number = MAX_IMAGE_DIMENSION,
|
|
26
|
+
): { width: number; height: number } | null {
|
|
27
|
+
if (!(width > 0) || !(height > 0)) return null;
|
|
28
|
+
const longest = Math.max(width, height);
|
|
29
|
+
if (longest < max) return null;
|
|
30
|
+
|
|
31
|
+
const target = max - 1;
|
|
32
|
+
const scale = target / longest;
|
|
33
|
+
return {
|
|
34
|
+
width: width >= height ? target : Math.max(1, Math.round(width * scale)),
|
|
35
|
+
height: height > width ? target : Math.max(1, Math.round(height * scale)),
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/types/api.ts
CHANGED
|
@@ -23,7 +23,9 @@ export type TerminalWsMessage =
|
|
|
23
23
|
|
|
24
24
|
/** WebSocket message types (chat) */
|
|
25
25
|
export type ChatWsClientMessage =
|
|
26
|
-
| { type: "message"; content: string; permissionMode?: string; priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }>;
|
|
26
|
+
| { type: "message"; content: string; permissionMode?: string; priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }>;
|
|
27
|
+
/** Uploaded paths for the same images, for providers that take a file not a payload. */
|
|
28
|
+
imagePaths?: string[]; model?: string; effort?: string; thinking?: boolean }
|
|
27
29
|
| { type: "cancel" }
|
|
28
30
|
| { type: "set_model"; model: string }
|
|
29
31
|
| { type: "set_effort"; effort: string }
|
package/src/types/chat.ts
CHANGED
|
@@ -2,6 +2,12 @@ export interface SendMessageOpts {
|
|
|
2
2
|
permissionMode?: import("./config").PermissionMode | string;
|
|
3
3
|
priority?: 'now' | 'next' | 'later';
|
|
4
4
|
images?: Array<{ data: string; mediaType: string }>;
|
|
5
|
+
/**
|
|
6
|
+
* Uploaded paths for the same attachments, for providers that take a file rather than a
|
|
7
|
+
* payload. Codex's turn input accepts `localImage` by path and has no base64 form, so an
|
|
8
|
+
* image reaches it this way or not at all.
|
|
9
|
+
*/
|
|
10
|
+
imagePaths?: string[];
|
|
5
11
|
/** Per-session model override; falls back to provider config model when absent */
|
|
6
12
|
model?: string;
|
|
7
13
|
/** Override the provider's 1M-context setting for this call (false = never add the
|
|
@@ -22,6 +22,8 @@ export interface TreeItemMsg {
|
|
|
22
22
|
description?: string;
|
|
23
23
|
tooltip?: string;
|
|
24
24
|
icon?: string;
|
|
25
|
+
/** Label wraps instead of truncating — for rows whose text is the content. */
|
|
26
|
+
wrap?: boolean;
|
|
25
27
|
collapsibleState: "none" | "collapsed" | "expanded";
|
|
26
28
|
command?: string;
|
|
27
29
|
children?: TreeItemMsg[];
|
|
@@ -52,6 +52,16 @@ export function AttachmentChips({ attachments, onRemove }: AttachmentChipsProps)
|
|
|
52
52
|
<Loader2 className="size-3 shrink-0 animate-spin text-text-subtle" />
|
|
53
53
|
) : att.status === "error" ? (
|
|
54
54
|
<span className="text-error shrink-0" title="Upload failed">!</span>
|
|
55
|
+
) : att.resized ? (
|
|
56
|
+
// Says what happened to the image. A screenshot arriving at the model smaller
|
|
57
|
+
// than the one that was pasted is worth stating rather than leaving to be
|
|
58
|
+
// discovered, and the measurement was already being recorded for nothing.
|
|
59
|
+
<span
|
|
60
|
+
className="shrink-0 text-[10px] text-text-subtle"
|
|
61
|
+
title={`Shrunk from ${att.resized.from.width}x${att.resized.from.height} to ${att.resized.to.width}x${att.resized.to.height}`}
|
|
62
|
+
>
|
|
63
|
+
{att.resized.to.width}×{att.resized.to.height}
|
|
64
|
+
</span>
|
|
55
65
|
) : null}
|
|
56
66
|
|
|
57
67
|
{/* Remove button */}
|
|
@@ -2,6 +2,8 @@ import { useState, useCallback, useRef, useEffect } from "react";
|
|
|
2
2
|
import { Loader2, Upload, X } from "lucide-react";
|
|
3
3
|
import { toast } from "sonner";
|
|
4
4
|
import { api, projectUrl } from "@/lib/api-client";
|
|
5
|
+
import { selectInlineImages } from "@/lib/image-resize-limits";
|
|
6
|
+
import { splitAttachmentMarkers } from "@/lib/attachment-marker-split";
|
|
5
7
|
import { useChat } from "@/hooks/use-chat";
|
|
6
8
|
import { useUsage } from "@/hooks/use-usage";
|
|
7
9
|
import { useTabStore } from "@/stores/tab-store";
|
|
@@ -56,7 +58,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
56
58
|
);
|
|
57
59
|
|
|
58
60
|
// Pending message to send after WS connects (replaces unreliable setTimeout)
|
|
59
|
-
const pendingSendRef = useRef<{ content: string; permissionMode?: string } | null>(null);
|
|
61
|
+
const pendingSendRef = useRef<{ content: string; permissionMode?: string; images?: Array<{ data: string; mediaType: string }>; imagePaths?: string[] } | null>(null);
|
|
60
62
|
|
|
61
63
|
// Drag-and-drop state
|
|
62
64
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -142,9 +144,9 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
142
144
|
// Flush pending message once WS connects (replaces unreliable setTimeout)
|
|
143
145
|
useEffect(() => {
|
|
144
146
|
if (isConnected && pendingSendRef.current) {
|
|
145
|
-
const { content, permissionMode: pm } = pendingSendRef.current;
|
|
147
|
+
const { content, permissionMode: pm, images: pendingImages, imagePaths: pendingPaths } = pendingSendRef.current;
|
|
146
148
|
pendingSendRef.current = null;
|
|
147
|
-
sendMessage(content, { permissionMode: pm });
|
|
149
|
+
sendMessage(content, { permissionMode: pm, ...(pendingImages?.length && { images: pendingImages }), ...(pendingPaths?.length && { imagePaths: pendingPaths }) });
|
|
148
150
|
}
|
|
149
151
|
}, [isConnected, sendMessage]);
|
|
150
152
|
|
|
@@ -350,7 +352,11 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
350
352
|
|
|
351
353
|
/** Build message content with file references and inline text snippets prepended */
|
|
352
354
|
const buildMessageWithAttachments = useCallback(
|
|
353
|
-
(
|
|
355
|
+
(
|
|
356
|
+
content: string,
|
|
357
|
+
attachments: ChatAttachment[],
|
|
358
|
+
inlineImages: Array<{ data: string; mediaType: string }> = [],
|
|
359
|
+
): string => {
|
|
354
360
|
if (attachments.length === 0) return content;
|
|
355
361
|
|
|
356
362
|
const parts: string[] = [];
|
|
@@ -360,12 +366,27 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
360
366
|
if (att.textContent) parts.push(att.textContent);
|
|
361
367
|
}
|
|
362
368
|
|
|
363
|
-
// Server-uploaded file references
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
369
|
+
// Server-uploaded file references. An image keeps its path even when the payload rides
|
|
370
|
+
// along with the message, because the transcript is what the chat re-renders from and
|
|
371
|
+
// the path is the only thing in it a thumbnail can be drawn from.
|
|
372
|
+
//
|
|
373
|
+
// An image sent inline says so in the marker. A bare path in a user message reads as an
|
|
374
|
+
// invitation to open it, and a model that takes it pays for the same picture twice —
|
|
375
|
+
// once inline, once as a tool result, both replayed on every later turn.
|
|
376
|
+
//
|
|
377
|
+
// Which marker an image gets follows the payloads that are actually going out, not
|
|
378
|
+
// `imageData` alone: the per-message caps can leave one behind, and announcing that its
|
|
379
|
+
// contents are included would hand the model neither the picture nor a reason to open
|
|
380
|
+
// the file. `inlineImages` defaults to none, so a caller that sends no payloads at all
|
|
381
|
+
// (the edit-fork path takes only text) gets the plain markers throughout.
|
|
382
|
+
const { inlineImagePaths, pathOnlyPaths } = splitAttachmentMarkers(attachments, inlineImages);
|
|
383
|
+
for (const path of inlineImagePaths) {
|
|
384
|
+
parts.push(`[Attached image (contents included in this message): ${path}]`);
|
|
385
|
+
}
|
|
386
|
+
if (pathOnlyPaths.length > 0) {
|
|
387
|
+
const fileRefs = pathOnlyPaths.join("\n");
|
|
367
388
|
parts.push(
|
|
368
|
-
|
|
389
|
+
pathOnlyPaths.length === 1
|
|
369
390
|
? `[Attached file: ${fileRefs}]`
|
|
370
391
|
: `[Attached files:\n${fileRefs}\n]`,
|
|
371
392
|
);
|
|
@@ -379,8 +400,11 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
379
400
|
|
|
380
401
|
const handleSend = useCallback(
|
|
381
402
|
async (content: string, attachments: ChatAttachment[] = [], priority?: MessagePriority) => {
|
|
382
|
-
const
|
|
383
|
-
|
|
403
|
+
const images = selectInlineImages(attachments);
|
|
404
|
+
const fullContent = buildMessageWithAttachments(content, attachments, images);
|
|
405
|
+
// Providers that take a file rather than a payload (codex) read these instead.
|
|
406
|
+
const imagePaths = attachments.filter((a) => a.isImage && a.serverPath).map((a) => a.serverPath!);
|
|
407
|
+
if (!fullContent.trim() && images.length === 0) return;
|
|
384
408
|
|
|
385
409
|
if (!sessionId) {
|
|
386
410
|
try {
|
|
@@ -394,14 +418,14 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
394
418
|
setSessionId(session.id);
|
|
395
419
|
setProviderId(session.providerId);
|
|
396
420
|
// Queue message — will be sent by effect when WS reports isConnected
|
|
397
|
-
pendingSendRef.current = { content: fullContent, permissionMode };
|
|
421
|
+
pendingSendRef.current = { content: fullContent, permissionMode, images, imagePaths };
|
|
398
422
|
return;
|
|
399
423
|
} catch (e) {
|
|
400
424
|
console.error("Failed to create session:", e);
|
|
401
425
|
return;
|
|
402
426
|
}
|
|
403
427
|
}
|
|
404
|
-
sendMessage(fullContent, { permissionMode, priority });
|
|
428
|
+
sendMessage(fullContent, { permissionMode, priority, ...(images.length > 0 && { images }), ...(imagePaths.length > 0 && { imagePaths }) });
|
|
405
429
|
},
|
|
406
430
|
[sessionId, providerId, projectName, sendMessage, buildMessageWithAttachments, permissionMode, metadata],
|
|
407
431
|
);
|
|
@@ -2,6 +2,8 @@ import { useState, useRef, useCallback, useEffect, memo, type KeyboardEvent, typ
|
|
|
2
2
|
import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff, Zap, ListOrdered, Clock, Bot, X } from "lucide-react";
|
|
3
3
|
import { useVoiceInput } from "@/hooks/use-voice-input";
|
|
4
4
|
import { api, projectUrl, getAuthToken } from "@/lib/api-client";
|
|
5
|
+
import { downscaleImage } from "@/lib/image-resize";
|
|
6
|
+
import { INLINE_IMAGE_LIMITS } from "@/lib/image-resize-limits";
|
|
5
7
|
import { randomId } from "@/lib/utils";
|
|
6
8
|
import { ownsGlobalShortcut } from "@/lib/owns-global-shortcut";
|
|
7
9
|
import { SEND_TO_CHAT_EVENT, SEND_TO_CHAT_ACK_EVENT, type SendToChatDetail } from "@/lib/send-to-chat";
|
|
@@ -17,6 +19,29 @@ import { fetchSlashItems, clearSlashItemsCache } from "@/lib/slash-items-cache";
|
|
|
17
19
|
import type { FileNode } from "../../../types/project";
|
|
18
20
|
import { useFileStore } from "@/stores/file-store";
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Base64 payload for an image file, or undefined when it cannot be read.
|
|
24
|
+
*
|
|
25
|
+
* Reads through FileReader rather than assembling the string by hand: a phone photo is
|
|
26
|
+
* millions of characters, and building it in chunks on the main thread freezes the composer
|
|
27
|
+
* for as long as it takes.
|
|
28
|
+
*/
|
|
29
|
+
async function readImageData(file: File): Promise<{ data: string; mediaType: string } | undefined> {
|
|
30
|
+
try {
|
|
31
|
+
const dataUrl = await new Promise<string>((resolve, reject) => {
|
|
32
|
+
const reader = new FileReader();
|
|
33
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
34
|
+
reader.onerror = () => reject(reader.error);
|
|
35
|
+
reader.readAsDataURL(file);
|
|
36
|
+
});
|
|
37
|
+
const comma = dataUrl.indexOf(",");
|
|
38
|
+
const data = comma >= 0 ? dataUrl.slice(comma + 1) : "";
|
|
39
|
+
return data ? { data, mediaType: file.type || "image/png" } : undefined;
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
20
45
|
export interface ChatAttachment {
|
|
21
46
|
id: string;
|
|
22
47
|
name: string;
|
|
@@ -27,6 +52,16 @@ export interface ChatAttachment {
|
|
|
27
52
|
serverPath?: string;
|
|
28
53
|
/** Inline text content (e.g. terminal output) — no upload needed */
|
|
29
54
|
textContent?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Base64 payload for an image, sent as part of the message itself.
|
|
57
|
+
*
|
|
58
|
+
* Passing the path instead would make the model spend a whole extra round trip calling Read
|
|
59
|
+
* to fetch it, and every round trip re-sends the entire transcript. The uploaded copy is
|
|
60
|
+
* still kept, so removing the payload from a transcript later stays recoverable.
|
|
61
|
+
*/
|
|
62
|
+
imageData?: { data: string; mediaType: string };
|
|
63
|
+
/** Dimensions before and after downscaling, when one happened — surfaced in the chip. */
|
|
64
|
+
resized?: { from: { width: number; height: number }; to: { width: number; height: number } };
|
|
30
65
|
status: "uploading" | "ready" | "error";
|
|
31
66
|
}
|
|
32
67
|
|
|
@@ -436,6 +471,27 @@ export const MessageInput = memo(function MessageInput({
|
|
|
436
471
|
onExternalPathsConsumed?.();
|
|
437
472
|
}, [externalPaths]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
438
473
|
|
|
474
|
+
/** Delete an upload the user removed before sending. Best effort — nothing depends on it. */
|
|
475
|
+
const discardUpload = useCallback(
|
|
476
|
+
async (serverPath: string) => {
|
|
477
|
+
if (!projectName) return;
|
|
478
|
+
const filename = serverPath.split(/[\\/]/).pop();
|
|
479
|
+
if (!filename) return;
|
|
480
|
+
try {
|
|
481
|
+
const headers: HeadersInit = {};
|
|
482
|
+
const token = getAuthToken();
|
|
483
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
484
|
+
await fetch(`${projectUrl(projectName)}/chat/uploads/${encodeURIComponent(filename)}`, {
|
|
485
|
+
method: "DELETE",
|
|
486
|
+
headers,
|
|
487
|
+
});
|
|
488
|
+
} catch {
|
|
489
|
+
// An orphaned file is not worth surfacing to the user.
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
[projectName],
|
|
493
|
+
);
|
|
494
|
+
|
|
439
495
|
/** Upload a single file to the server, return server path */
|
|
440
496
|
const uploadFile = useCallback(
|
|
441
497
|
async (file: File): Promise<string | null> => {
|
|
@@ -466,32 +522,64 @@ export const MessageInput = memo(function MessageInput({
|
|
|
466
522
|
/** Process files — always uploads to server. Path resolution only happens via @ picker. */
|
|
467
523
|
const processFiles = useCallback(
|
|
468
524
|
async (files: File[]) => {
|
|
469
|
-
for (const
|
|
525
|
+
for (const original of files) {
|
|
470
526
|
const id = randomId();
|
|
471
|
-
const isImg = isImageFile(
|
|
472
|
-
const previewUrl = isImg ? URL.createObjectURL(file) : undefined;
|
|
527
|
+
const isImg = isImageFile(original);
|
|
473
528
|
|
|
474
529
|
const att: ChatAttachment = {
|
|
475
530
|
id,
|
|
476
|
-
name:
|
|
477
|
-
file,
|
|
531
|
+
name: original.name,
|
|
532
|
+
file: original,
|
|
478
533
|
isImage: isImg,
|
|
479
|
-
previewUrl,
|
|
534
|
+
previewUrl: isImg ? URL.createObjectURL(original) : undefined,
|
|
480
535
|
status: "uploading",
|
|
481
536
|
};
|
|
482
537
|
|
|
483
538
|
setAttachments((prev) => [...prev, att]);
|
|
484
539
|
|
|
485
|
-
//
|
|
486
|
-
|
|
540
|
+
// Shrink first, so the upload, the preview and the message all carry the same
|
|
541
|
+
// already-reduced image — an oversized original must not reach the transcript.
|
|
542
|
+
//
|
|
543
|
+
// Only a `scaled` or `inlineable` result is attached inline. An image that could not
|
|
544
|
+
// be measured or re-encoded travels by path alone: sending unverified dimensions
|
|
545
|
+
// inline risks a payload the API refuses, and that refusal outlives the turn, since
|
|
546
|
+
// the transcript replays it into every later one.
|
|
547
|
+
try {
|
|
548
|
+
const outcome = isImg ? await downscaleImage(original) : null;
|
|
549
|
+
const file = outcome?.file ?? original;
|
|
550
|
+
// Point the thumbnail at the reduced file and let the original go. It is the larger
|
|
551
|
+
// of the two and nothing needs it once the resize is done, but it stayed alive until
|
|
552
|
+
// the message was sent because the preview still referenced it.
|
|
553
|
+
const previewUrl = isImg && file !== original ? URL.createObjectURL(file) : undefined;
|
|
554
|
+
const serverPath = await uploadFile(file);
|
|
555
|
+
const inlineable = outcome ? outcome.kind !== "asis" : false;
|
|
556
|
+
const imageData = inlineable ? await readImageData(file) : undefined;
|
|
557
|
+
const withinPayloadCap =
|
|
558
|
+
!!imageData && imageData.data.length <= INLINE_IMAGE_LIMITS.maxBase64PerImage;
|
|
559
|
+
|
|
560
|
+
if (previewUrl && att.previewUrl) URL.revokeObjectURL(att.previewUrl);
|
|
487
561
|
setAttachments((prev) =>
|
|
488
562
|
prev.map((a) =>
|
|
489
563
|
a.id === id
|
|
490
|
-
? {
|
|
564
|
+
? {
|
|
565
|
+
...a,
|
|
566
|
+
file,
|
|
567
|
+
...(previewUrl && { previewUrl }),
|
|
568
|
+
serverPath: serverPath ?? undefined,
|
|
569
|
+
imageData: withinPayloadCap ? imageData : undefined,
|
|
570
|
+
resized: outcome?.to ? { from: outcome.from!, to: outcome.to } : undefined,
|
|
571
|
+
// An image can still be sent inline when the upload failed; anything
|
|
572
|
+
// else has nothing left to send without its path.
|
|
573
|
+
status: serverPath || withinPayloadCap ? "ready" : "error",
|
|
574
|
+
}
|
|
491
575
|
: a,
|
|
492
576
|
),
|
|
493
577
|
);
|
|
494
|
-
}
|
|
578
|
+
} catch {
|
|
579
|
+
// Nothing here may leave an attachment stuck on "uploading" — both the send button
|
|
580
|
+
// and the auto-send effect wait on that state, so a swallowed throw hangs the composer.
|
|
581
|
+
setAttachments((prev) => prev.map((a) => (a.id === id ? { ...a, status: "error" } : a)));
|
|
582
|
+
}
|
|
495
583
|
}
|
|
496
584
|
(mobileTextareaRef.current ?? textareaRef.current)?.focus();
|
|
497
585
|
},
|
|
@@ -502,9 +590,12 @@ export const MessageInput = memo(function MessageInput({
|
|
|
502
590
|
setAttachments((prev) => {
|
|
503
591
|
const att = prev.find((a) => a.id === id);
|
|
504
592
|
if (att?.previewUrl) URL.revokeObjectURL(att.previewUrl);
|
|
593
|
+
// Uploads are never swept, because chat history points at them indefinitely, so one
|
|
594
|
+
// abandoned before it was ever sent would sit there with nothing referencing it.
|
|
595
|
+
if (att?.serverPath) void discardUpload(att.serverPath);
|
|
505
596
|
return prev.filter((a) => a.id !== id);
|
|
506
597
|
});
|
|
507
|
-
}, []);
|
|
598
|
+
}, [discardUpload]);
|
|
508
599
|
|
|
509
600
|
/** Execute the actual send (called directly or after uploads complete) */
|
|
510
601
|
const executeSend = useCallback(() => {
|