@band-app/server 0.26.1 → 0.27.1
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/dist/client/assets/{DockviewBrowserContainer-CNieygqn.js → DockviewBrowserContainer-BT0Xf-Qd.js} +1 -1
- package/dist/client/assets/{DockviewTerminalContainer-rpSQamnG.js → DockviewTerminalContainer-Cfq2Horm.js} +2 -2
- package/dist/client/assets/{TerminalPanel-BSSgNpJl.js → TerminalPanel-BmTjgsHC.js} +1 -1
- package/dist/client/assets/{_basePickBy-D4sc4sgx.js → _basePickBy-C5e3lPfw.js} +1 -1
- package/dist/client/assets/_baseUniq-BWawrl1m.js +1 -0
- package/dist/client/assets/{arc-BP7O8Ofj.js → arc-kL4UMvqY.js} +1 -1
- package/dist/client/assets/{architectureDiagram-VXUJARFQ-CLeIf9de.js → architectureDiagram-VXUJARFQ-CgqIs59Y.js} +1 -1
- package/dist/client/assets/{blockDiagram-VD42YOAC-GF1d7qvY.js → blockDiagram-VD42YOAC-CMeufhUZ.js} +1 -1
- package/dist/client/assets/{c4Diagram-YG6GDRKO-CPOD-mTp.js → c4Diagram-YG6GDRKO-CAuYwvwS.js} +1 -1
- package/dist/client/assets/channel-C29TlOk_.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-lgknUVTn.js → chunk-4BX2VUAB-6Xmnr9re.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-BZZgpdJp.js → chunk-55IACEB6-Dyqy3jY2.js} +1 -1
- package/dist/client/assets/{chunk-B4BG7PRW-D5j_7n78.js → chunk-B4BG7PRW-CHkzDkkl.js} +1 -1
- package/dist/client/assets/{chunk-DI55MBZ5-CNWAKrv-.js → chunk-DI55MBZ5-Bm8dHcM7.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-C4ZfwGxd.js → chunk-FMBD7UC4-DUSTPPko.js} +1 -1
- package/dist/client/assets/{chunk-QN33PNHL-B-1Abtg1.js → chunk-QN33PNHL-lANEHpc4.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-BpzKtAWD.js → chunk-QZHKN3VN-CdOUQvqm.js} +1 -1
- package/dist/client/assets/{chunk-TZMSLE5B-SXcvrXdW.js → chunk-TZMSLE5B-5WqV_fuV.js} +1 -1
- package/dist/client/assets/classDiagram-2ON5EDUG-Ce71vgj4.js +1 -0
- package/dist/client/assets/classDiagram-v2-WZHVMYZB-Ce71vgj4.js +1 -0
- package/dist/client/assets/clone-DV3pdAAp.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-Bct5sd16.js → cose-bilkent-S5V4N54A-DysgQPxt.js} +1 -1
- package/dist/client/assets/{dagre-6UL2VRFP-DLF3mura.js → dagre-6UL2VRFP-Dlzo9Msc.js} +1 -1
- package/dist/client/assets/{diagram-PSM6KHXK-ChWVmXI7.js → diagram-PSM6KHXK-BYb3WSVe.js} +1 -1
- package/dist/client/assets/{diagram-QEK2KX5R-DhzUuHM4.js → diagram-QEK2KX5R-DQ47fOtM.js} +1 -1
- package/dist/client/assets/{diagram-S2PKOQOG-D1i0mdIg.js → diagram-S2PKOQOG-8W5mRzVZ.js} +1 -1
- package/dist/client/assets/{erDiagram-Q2GNP2WA-BRj386XH.js → erDiagram-Q2GNP2WA-mgQREmQi.js} +1 -1
- package/dist/client/assets/{flowDiagram-NV44I4VS-DStEWZIF.js → flowDiagram-NV44I4VS-B3xMnHpg.js} +1 -1
- package/dist/client/assets/{ganttDiagram-JELNMOA3-B9PBn8dz.js → ganttDiagram-JELNMOA3-uAMwC_fw.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-V2S2FVAM-laJAIbJ3.js → gitGraphDiagram-V2S2FVAM-CMzRWjLs.js} +1 -1
- package/dist/client/assets/{graph-CbQhcBO1.js → graph-BmHmHBlj.js} +1 -1
- package/dist/client/assets/{highlighted-body-B3W2YXNL-DBxcPCX_.js → highlighted-body-B3W2YXNL-CktG8_2q.js} +1 -1
- package/dist/client/assets/{index-CK16S7B-.js → index-7ksyuzec.js} +1 -1
- package/dist/client/assets/{index-Ct5D5rHM.js → index-B5eLE5g9.js} +1 -1
- package/dist/client/assets/{index-D7C86bYw.js → index-BAkL31T9.js} +1 -1
- package/dist/client/assets/{index-CWLwMHnR.js → index-BVM1YUvk.js} +1 -1
- package/dist/client/assets/{index-DsEib-PT.js → index-Bl_qcgmA.js} +1 -1
- package/dist/client/assets/{index-Cd26dS4P.js → index-C17JkYK9.js} +1 -1
- package/dist/client/assets/{index-Dev2D0I9.js → index-CE-YdiiQ.js} +1 -1
- package/dist/client/assets/{index-86TW9gUX.js → index-CO5pYvVV.js} +1 -1
- package/dist/client/assets/{index-C5U256M6.js → index-CVhvgW2f.js} +1 -1
- package/dist/client/assets/{index-9XCTqmje.js → index-CmBpodpp.js} +1 -1
- package/dist/client/assets/{index-CLa-0ZwC.js → index-CuFyr77F.js} +1 -1
- package/dist/client/assets/{index-CfO5yuew.js → index-CyIB4sGJ.js} +1 -1
- package/dist/client/assets/{index-BqMDNoZp.js → index-DDjGWZqJ.js} +1 -1
- package/dist/client/assets/{index-BzwcSYCa.js → index-DOcXD6ZB.js} +1 -1
- package/dist/client/assets/{index-Cd5F9rkU.js → index-DWrfvgIb.js} +1 -1
- package/dist/client/assets/{index-DGvFFhe0.js → index-Db-V2kYr.js} +1 -1
- package/dist/client/assets/{index-cRf4PoQf.js → index-Dt1N14f0.js} +1 -1
- package/dist/client/assets/{index-CyO2GJFA.js → index-cr1Bo2U9.js} +1 -1
- package/dist/client/assets/{infoDiagram-HS3SLOUP-0QLwbPV-.js → infoDiagram-HS3SLOUP-BfGNNAH4.js} +1 -1
- package/dist/client/assets/{journeyDiagram-XKPGCS4Q-xGdny9DO.js → journeyDiagram-XKPGCS4Q-Ds8ah4VH.js} +1 -1
- package/dist/client/assets/{kanban-definition-3W4ZIXB7-Don0Okra.js → kanban-definition-3W4ZIXB7-C8PaE0y4.js} +1 -1
- package/dist/client/assets/{layout-WN5NQdX3.js → layout-D5O5pfhp.js} +1 -1
- package/dist/client/assets/{main-Bbh731ex.js → main-CWjuaHE2.js} +221 -221
- package/dist/client/assets/main-Di-3Gk2i.css +1 -0
- package/dist/client/assets/{mindmap-definition-VGOIOE7T-CWF6i4Zi.js → mindmap-definition-VGOIOE7T-DmB2qgHj.js} +1 -1
- package/dist/client/assets/{pieDiagram-ADFJNKIX-CXk9oMu_.js → pieDiagram-ADFJNKIX-BhZD1Pjg.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-AYHSOK5B-D2s0zA4_.js → quadrantDiagram-AYHSOK5B-Dm615XC6.js} +1 -1
- package/dist/client/assets/{requirementDiagram-UZGBJVZJ-B5g3eZDa.js → requirementDiagram-UZGBJVZJ-ezxSkCjp.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-TZEHDZUN-BKYy9nLE.js → sankeyDiagram-TZEHDZUN-NLwGtj2u.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-WL72ISMW-Of-7JsaP.js → sequenceDiagram-WL72ISMW-D8C_g8jv.js} +1 -1
- package/dist/client/assets/{square-terminal-wqR_rG8g.js → square-terminal-DAMJq3wS.js} +1 -1
- package/dist/client/assets/{stateDiagram-FKZM4ZOC-CLygxzeu.js → stateDiagram-FKZM4ZOC-CkXjpkTE.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-4FDKWEC3-Cp2lp6Ub.js +1 -0
- package/dist/client/assets/{timeline-definition-IT6M3QCI-HEdTZ9lg.js → timeline-definition-IT6M3QCI-DyPfcP9V.js} +1 -1
- package/dist/client/assets/{treemap-GDKQZRPO-BLCSYUuQ.js → treemap-GDKQZRPO-rH_7Hpkq.js} +1 -1
- package/dist/client/assets/workspace._workspaceId-CEIguAmj.js +2 -0
- package/dist/client/assets/{workspace._workspaceId-CvIXlcVP.js → workspace._workspaceId-COpAN_jv.js} +1 -1
- package/dist/client/assets/{xychartDiagram-PRI3JC2R-Da4m2pn4.js → xychartDiagram-PRI3JC2R-DkwSq2gu.js} +1 -1
- package/dist/migrations/20260707132201_moaning_cerebro/migration.sql +2 -0
- package/dist/migrations/20260707132201_moaning_cerebro/snapshot.json +1187 -0
- package/dist/openapi.json +69 -15
- package/dist/server/assets/{DockviewBrowserContainer-BxukCnRj.js → DockviewBrowserContainer-CxzZY1qq.js} +1 -1
- package/dist/server/assets/{DockviewTerminalContainer-DQYYK-79.js → DockviewTerminalContainer-BIJPTTun.js} +3 -3
- package/dist/server/assets/{TerminalPanel-TU2tfPvh.js → TerminalPanel-kEsEEcuZ.js} +1 -1
- package/dist/server/assets/{_basePickBy-BSdtfHB0.js → _basePickBy-n4A54Vhz.js} +2 -2
- package/dist/server/assets/{_baseUniq-DkSqb3YR.js → _baseUniq-BuxfXYiw.js} +1 -1
- package/dist/server/assets/_tanstack-start-manifest_v-CbRnkjTe.js +4 -0
- package/dist/server/assets/{arc-DxGkupwT.js → arc-C0mFTnQ4.js} +1 -1
- package/dist/server/assets/{architecture-7HQA4BMR-N3irVT54.js → architecture-7HQA4BMR-D3tywrA5.js} +6 -6
- package/dist/server/assets/{architectureDiagram-VXUJARFQ-B0sHbG9y.js → architectureDiagram-VXUJARFQ-B9hdFdgc.js} +6 -6
- package/dist/server/assets/{blockDiagram-VD42YOAC-1FtDdqBo.js → blockDiagram-VD42YOAC-Bjf6umts.js} +6 -6
- package/dist/server/assets/{c4Diagram-YG6GDRKO-Bw9IfZMM.js → c4Diagram-YG6GDRKO-B1Go7qzd.js} +2 -2
- package/dist/server/assets/{channel-D4IKgr-x.js → channel-Dqy7JuGG.js} +1 -1
- package/dist/server/assets/{chunk-4BX2VUAB-ByMuI7Fp.js → chunk-4BX2VUAB-DABywcCU.js} +1 -1
- package/dist/server/assets/{chunk-55IACEB6-BAdInBZF.js → chunk-55IACEB6-WikJyoE2.js} +1 -1
- package/dist/server/assets/{chunk-B4BG7PRW-C8vV9ouR.js → chunk-B4BG7PRW-CUCrrQLt.js} +4 -4
- package/dist/server/assets/{chunk-DI55MBZ5-BnPjxwDt.js → chunk-DI55MBZ5-CAaqT17X.js} +3 -3
- package/dist/server/assets/{chunk-FMBD7UC4-DxER1-MJ.js → chunk-FMBD7UC4-CX_v_cac.js} +1 -1
- package/dist/server/assets/{chunk-QN33PNHL-DzFGKjvS.js → chunk-QN33PNHL-VLbTMtxB.js} +1 -1
- package/dist/server/assets/{chunk-QZHKN3VN-DKIreZRT.js → chunk-QZHKN3VN-QB2sRPFp.js} +1 -1
- package/dist/server/assets/{chunk-TZMSLE5B-Dyw_E6V-.js → chunk-TZMSLE5B-C9MxtgP2.js} +1 -1
- package/dist/server/assets/{classDiagram-v2-WZHVMYZB-Bu-X2Gbw.js → classDiagram-2ON5EDUG-UX-9XQ-6.js} +5 -5
- package/dist/server/assets/{classDiagram-2ON5EDUG-Bu-X2Gbw.js → classDiagram-v2-WZHVMYZB-UX-9XQ-6.js} +5 -5
- package/dist/server/assets/{clone-CwBTz6wc.js → clone-Jt6nkb9-.js} +1 -1
- package/dist/server/assets/{cose-bilkent-S5V4N54A-BsFFjtud.js → cose-bilkent-S5V4N54A-j4JEpP33.js} +1 -1
- package/dist/server/assets/{dagre-6UL2VRFP-CdgGxkBj.js → dagre-6UL2VRFP-CRjo2D7K.js} +6 -6
- package/dist/server/assets/{diagram-PSM6KHXK-C-LWq-ju.js → diagram-PSM6KHXK-CdcM_CoU.js} +7 -7
- package/dist/server/assets/{diagram-QEK2KX5R-CFa3sLiJ.js → diagram-QEK2KX5R-DXbEv4yb.js} +6 -6
- package/dist/server/assets/{diagram-S2PKOQOG-CyAHBi5H.js → diagram-S2PKOQOG-Bzc1B5Be.js} +6 -6
- package/dist/server/assets/{erDiagram-Q2GNP2WA-COrhvUZD.js → erDiagram-Q2GNP2WA-Gt2ujaN1.js} +4 -4
- package/dist/server/assets/{flowDiagram-NV44I4VS-DxR9v2a8.js → flowDiagram-NV44I4VS-CONOX4ru.js} +5 -5
- package/dist/server/assets/{ganttDiagram-JELNMOA3-COoz3lCc.js → ganttDiagram-JELNMOA3-LO1CZhBF.js} +1 -1
- package/dist/server/assets/{gitGraph-G5XIXVHT-BtVXZr1d.js → gitGraph-G5XIXVHT-CYME5GNN.js} +6 -6
- package/dist/server/assets/{gitGraphDiagram-V2S2FVAM-DrH_1hhd.js → gitGraphDiagram-V2S2FVAM-JAQOltND.js} +7 -7
- package/dist/server/assets/{graph-BqKB55Uo.js → graph-DDw3y63Y.js} +2 -2
- package/dist/server/assets/{highlighted-body-B3W2YXNL-C6VVTevi.js → highlighted-body-B3W2YXNL-Bl56ZglV.js} +1 -1
- package/dist/server/assets/{index-BUnwtpbx.js → index-B9mYm2Iv.js} +3 -3
- package/dist/server/assets/{index-BHRz1U1c.js → index-BDXNcXpX.js} +2 -2
- package/dist/server/assets/{index-Cdu8cpPJ.js → index-BFxM6c1Y.js} +2 -2
- package/dist/server/assets/{index-Bkx5Foei.js → index-BFyODGGJ.js} +4 -4
- package/dist/server/assets/{index-ChJXmItC.js → index-BPA-V89G.js} +2 -2
- package/dist/server/assets/{index-DAqzmsPy.js → index-BVjGe10C.js} +1 -1
- package/dist/server/assets/{index-CQ12PKsH.js → index-BuqNnUeC.js} +2 -2
- package/dist/server/assets/{index-DeNkOFC0.js → index-CEHzgT6Q.js} +5 -5
- package/dist/server/assets/{index-nrGOopfX.js → index-CGdiDxDM.js} +2 -2
- package/dist/server/assets/{index-Dnp4nLLc.js → index-CX4yKGY7.js} +2 -2
- package/dist/server/assets/{index-BvmYCz2n.js → index-CXNHt9Li.js} +2 -2
- package/dist/server/assets/{index-IxUUG90S.js → index-CZITjlKR.js} +1 -1
- package/dist/server/assets/{index-FVQ1p-ZQ.js → index-D022D6yP.js} +5 -5
- package/dist/server/assets/{index-CkbMi8ML.js → index-DRNvWTdD.js} +2 -2
- package/dist/server/assets/{index-DIZ-5H2r.js → index-DsR_UKQx.js} +3 -3
- package/dist/server/assets/{index-DLOoQUcR.js → index-DsyhmvSr.js} +2 -2
- package/dist/server/assets/{index-DgpTNIz9.js → index-Dvcd3xwL.js} +2 -2
- package/dist/server/assets/{index-fzh31gPI.js → index-jCxJanyY.js} +2 -2
- package/dist/server/assets/{info-VBDWY6EO-C1EJd9ah.js → info-VBDWY6EO-DnY-exPR.js} +6 -6
- package/dist/server/assets/{infoDiagram-HS3SLOUP-BgR_JFRD.js → infoDiagram-HS3SLOUP-wZZ4hEZ9.js} +5 -5
- package/dist/server/assets/{journeyDiagram-XKPGCS4Q-TInLDEBY.js → journeyDiagram-XKPGCS4Q-CBD0jzUu.js} +4 -4
- package/dist/server/assets/{kanban-definition-3W4ZIXB7-Ds9-Rky6.js → kanban-definition-3W4ZIXB7-9SKGIQ4n.js} +2 -2
- package/dist/server/assets/{layout-02chZ-oO.js → layout-C-xcL44a.js} +4 -4
- package/dist/server/assets/{mermaid-3ZIDBTTL-CF0njLRi.js → mermaid-3ZIDBTTL-DyXquuLm.js} +1 -1
- package/dist/server/assets/{mermaid-parser.core-DGyqTX8L.js → mermaid-parser.core-DruUthGx.js} +11 -11
- package/dist/server/assets/{mindmap-definition-VGOIOE7T-B2VHBojz.js → mindmap-definition-VGOIOE7T-jjxDAUjJ.js} +3 -3
- package/dist/server/assets/{packet-DYOGHKS2-BPQQ4vpy.js → packet-DYOGHKS2-CyXoagV-.js} +6 -6
- package/dist/server/assets/{pie-VRWISCQL-D_ISlmZV.js → pie-VRWISCQL-1fjbRV6a.js} +6 -6
- package/dist/server/assets/{pieDiagram-ADFJNKIX-DfLOLRYh.js → pieDiagram-ADFJNKIX-5evhP6Hm.js} +7 -7
- package/dist/server/assets/{quadrantDiagram-AYHSOK5B-C-S0tUnS.js → quadrantDiagram-AYHSOK5B-Cg2-kPDC.js} +1 -1
- package/dist/server/assets/{radar-ZZBFDIW7-Btz0_zMX.js → radar-ZZBFDIW7-DCBXoSHQ.js} +6 -6
- package/dist/server/assets/{requirementDiagram-UZGBJVZJ-CX-LPkZO.js → requirementDiagram-UZGBJVZJ-DjOyZt1t.js} +3 -3
- package/dist/server/assets/{router-6QHcrvmQ.js → router-BtsNvWXM.js} +766 -615
- package/dist/server/assets/{sankeyDiagram-TZEHDZUN-C3bzmTeJ.js → sankeyDiagram-TZEHDZUN-Bun0ZHSn.js} +1 -1
- package/dist/server/assets/{sequenceDiagram-WL72ISMW-BlZBlq8N.js → sequenceDiagram-WL72ISMW-fn9HXFXZ.js} +3 -3
- package/dist/server/assets/{square-terminal-1wS601vv.js → square-terminal-CDDUQc-O.js} +1 -1
- package/dist/server/assets/{stateDiagram-FKZM4ZOC-BIRMcaiS.js → stateDiagram-FKZM4ZOC-Ct7juWxD.js} +8 -8
- package/dist/server/assets/{stateDiagram-v2-4FDKWEC3-D2NYWxQw.js → stateDiagram-v2-4FDKWEC3-Bp505XWR.js} +4 -4
- package/dist/server/assets/{timeline-definition-IT6M3QCI-BiY_XUAp.js → timeline-definition-IT6M3QCI-Dw8oeGiX.js} +2 -2
- package/dist/server/assets/{treemap-GDKQZRPO-CYGEAd8a.js → treemap-GDKQZRPO-BdIdWFqZ.js} +6 -6
- package/dist/server/assets/{workspace._workspaceId-MLWQzLSj.js → workspace._workspaceId-GwRuI3oS.js} +10 -4
- package/dist/server/assets/{workspace._workspaceId-CHMs1pW4.js → workspace._workspaceId-joQQNCnQ.js} +1 -1
- package/dist/server/assets/{xychartDiagram-PRI3JC2R-Cv8buTCr.js → xychartDiagram-PRI3JC2R-BRCOemWX.js} +1 -1
- package/dist/server/server.js +2 -2
- package/dist/start-server.mjs +1398 -1140
- package/package.json +4 -4
- package/dist/client/assets/_baseUniq-CExXcLYT.js +0 -1
- package/dist/client/assets/channel-BAHGob6X.js +0 -1
- package/dist/client/assets/classDiagram-2ON5EDUG-_5AM6YM0.js +0 -1
- package/dist/client/assets/classDiagram-v2-WZHVMYZB-_5AM6YM0.js +0 -1
- package/dist/client/assets/clone-C12DbMN-.js +0 -1
- package/dist/client/assets/main-DpPgdfV2.css +0 -1
- package/dist/client/assets/stateDiagram-v2-4FDKWEC3-D3J4rLvX.js +0 -1
- package/dist/client/assets/workspace._workspaceId-9mP5lL7t.js +0 -2
- package/dist/server/assets/_tanstack-start-manifest_v-CjnZwsJN.js +0 -4
package/dist/start-server.mjs
CHANGED
|
@@ -30394,6 +30394,20 @@ var init_claude_code = __esm({
|
|
|
30394
30394
|
args: [prompt]
|
|
30395
30395
|
};
|
|
30396
30396
|
}
|
|
30397
|
+
/**
|
|
30398
|
+
* Headless one-shot invocation for automated terminal dispatch (cronjobs,
|
|
30399
|
+
* issue #581). `claude -p "<prompt>"` runs the prompt in print
|
|
30400
|
+
* (non-interactive) mode and exits when the turn completes — unlike
|
|
30401
|
+
* `cliInvocation`'s bare `claude "<prompt>"`, which opens the REPL and
|
|
30402
|
+
* stays parked. The prompt is the value of `-p`, so a leading `-` in it is
|
|
30403
|
+
* consumed as the flag's argument rather than parsed as a flag.
|
|
30404
|
+
*/
|
|
30405
|
+
cliHeadlessInvocation(prompt) {
|
|
30406
|
+
return {
|
|
30407
|
+
command: this.executablePath ?? CLAUDE_CODE_DEFAULT_BINARY,
|
|
30408
|
+
args: ["-p", prompt]
|
|
30409
|
+
};
|
|
30410
|
+
}
|
|
30397
30411
|
/**
|
|
30398
30412
|
* Resume CLI invocation for the chat tab's "Continue in terminal" action.
|
|
30399
30413
|
* `claude --resume <session-id>` reopens the interactive REPL with that
|
|
@@ -31148,6 +31162,18 @@ var init_cursor_cli = __esm({
|
|
|
31148
31162
|
reason: "Cursor CLI has no interactive prompt-loading invocation; falling back to chat."
|
|
31149
31163
|
};
|
|
31150
31164
|
}
|
|
31165
|
+
/**
|
|
31166
|
+
* Cursor CLI is SDK-only (JSON over a managed subprocess) with no
|
|
31167
|
+
* non-interactive vendor CLI to run in a pane, so headless terminal
|
|
31168
|
+
* dispatch (cronjobs `via: "terminal"`, issue #581) also returns the
|
|
31169
|
+
* `unsupported` sentinel — the cronjob service then falls back to chat.
|
|
31170
|
+
*/
|
|
31171
|
+
cliHeadlessInvocation(_prompt) {
|
|
31172
|
+
return {
|
|
31173
|
+
unsupported: true,
|
|
31174
|
+
reason: "Cursor CLI has no non-interactive CLI invocation; falling back to chat."
|
|
31175
|
+
};
|
|
31176
|
+
}
|
|
31151
31177
|
/**
|
|
31152
31178
|
* The chat tab's "Continue in terminal" action has no Cursor equivalent:
|
|
31153
31179
|
* the agent runs over the SDK's managed JSON subprocess, with no
|
|
@@ -32346,6 +32372,18 @@ var init_codex = __esm({
|
|
|
32346
32372
|
args: [prompt]
|
|
32347
32373
|
};
|
|
32348
32374
|
}
|
|
32375
|
+
/**
|
|
32376
|
+
* Headless one-shot invocation for automated terminal dispatch (cronjobs,
|
|
32377
|
+
* issue #581). `codex exec "<prompt>"` runs the prompt non-interactively
|
|
32378
|
+
* and exits on completion — unlike `cliInvocation`'s bare `codex
|
|
32379
|
+
* "<prompt>"`, which opens the interactive TUI and stays parked.
|
|
32380
|
+
*/
|
|
32381
|
+
cliHeadlessInvocation(prompt) {
|
|
32382
|
+
return {
|
|
32383
|
+
command: this.executablePath ?? CODEX_DEFAULT_BINARY,
|
|
32384
|
+
args: ["exec", prompt]
|
|
32385
|
+
};
|
|
32386
|
+
}
|
|
32349
32387
|
/**
|
|
32350
32388
|
* Resume CLI invocation for the chat tab's "Continue in terminal" action.
|
|
32351
32389
|
* `codex resume <session-id>` reopens the interactive Codex TUI with that
|
|
@@ -32735,6 +32773,25 @@ var init_gemini_cli = __esm({
|
|
|
32735
32773
|
args: ["--", prompt]
|
|
32736
32774
|
};
|
|
32737
32775
|
}
|
|
32776
|
+
/**
|
|
32777
|
+
* Headless one-shot invocation for automated terminal dispatch (cronjobs,
|
|
32778
|
+
* issue #581). `gemini --prompt=<prompt>` runs non-interactively and exits
|
|
32779
|
+
* on completion — unlike `cliInvocation`'s `gemini -- "<prompt>"`, which
|
|
32780
|
+
* opens the interactive REPL and stays parked. The `--prompt=<value>`
|
|
32781
|
+
* joined form (rather than `--prompt <value>`) binds the prompt as the
|
|
32782
|
+
* flag's argument even when it starts with `-`, mirroring the leading-dash
|
|
32783
|
+
* safety the interactive `--` guard provides.
|
|
32784
|
+
*
|
|
32785
|
+
* `--output-format` is deliberately omitted (unlike `runSession`, which asks
|
|
32786
|
+
* for NDJSON): a cron terminal shows plain human-readable output in the pane,
|
|
32787
|
+
* not a machine-parsed stream.
|
|
32788
|
+
*/
|
|
32789
|
+
cliHeadlessInvocation(prompt) {
|
|
32790
|
+
return {
|
|
32791
|
+
command: this.executablePath,
|
|
32792
|
+
args: [`--prompt=${prompt}`]
|
|
32793
|
+
};
|
|
32794
|
+
}
|
|
32738
32795
|
/**
|
|
32739
32796
|
* The chat tab's "Continue in terminal" action has no Gemini equivalent:
|
|
32740
32797
|
* the Gemini CLI has no session model, so there's no session ID to resume
|
|
@@ -33230,6 +33287,21 @@ var init_opencode = __esm({
|
|
|
33230
33287
|
args: ["--prompt", prompt]
|
|
33231
33288
|
};
|
|
33232
33289
|
}
|
|
33290
|
+
/**
|
|
33291
|
+
* Headless one-shot invocation for automated terminal dispatch (cronjobs,
|
|
33292
|
+
* issue #581). `opencode run "<prompt>"` executes the prompt
|
|
33293
|
+
* non-interactively and exits on completion — unlike `cliInvocation`'s
|
|
33294
|
+
* `opencode --prompt "<prompt>"`, which opens the interactive TUI and stays
|
|
33295
|
+
* parked. `run` takes the prompt as its trailing positional (same shape as
|
|
33296
|
+
* the SDK `runSession` path above, minus the JSON formatting we only need
|
|
33297
|
+
* when parsing programmatically).
|
|
33298
|
+
*/
|
|
33299
|
+
cliHeadlessInvocation(prompt) {
|
|
33300
|
+
return {
|
|
33301
|
+
command: this.executablePath,
|
|
33302
|
+
args: ["run", prompt]
|
|
33303
|
+
};
|
|
33304
|
+
}
|
|
33233
33305
|
/**
|
|
33234
33306
|
* Resume CLI invocation for the chat tab's "Continue in terminal" action.
|
|
33235
33307
|
* `opencode --session <id>` launches the interactive TUI on that exact
|
|
@@ -101849,10 +101921,19 @@ var cronjobs = sqliteTable("cronjobs", {
|
|
|
101849
101921
|
cronExpression: text2("cron_expression").notNull(),
|
|
101850
101922
|
scope: text2("scope", { enum: ["project", "workspace"] }).notNull(),
|
|
101851
101923
|
workspaceId: text2("workspace_id"),
|
|
101924
|
+
// Where a fire dispatches the prompt (issue #581): "chat" submits a task to
|
|
101925
|
+
// the workspace's cron chat pane (default, backward-compatible); "terminal"
|
|
101926
|
+
// spawns the agent's vendor CLI in a fresh self-closing PTY pane. Mirrors the
|
|
101927
|
+
// `via` discriminator on `workspaces.create` (#551).
|
|
101928
|
+
via: text2("via", { enum: ["chat", "terminal"] }).notNull().default("chat"),
|
|
101852
101929
|
enabled: integer2("enabled", { mode: "boolean" }).notNull().default(true),
|
|
101853
101930
|
createdAt: text2("created_at").notNull(),
|
|
101854
101931
|
lastRunAt: text2("last_run_at"),
|
|
101855
|
-
lastRunStatus: text2("last_run_status", { enum: ["completed", "failed", "skipped"] })
|
|
101932
|
+
lastRunStatus: text2("last_run_status", { enum: ["completed", "failed", "skipped"] }),
|
|
101933
|
+
// For via="terminal" jobs, the id of the terminal spawned by the most recent
|
|
101934
|
+
// fire. Used as the overlap-check handle: if this PTY is still alive when the
|
|
101935
|
+
// next tick fires, the run is skipped rather than launching a second agent.
|
|
101936
|
+
lastTerminalId: text2("last_terminal_id")
|
|
101856
101937
|
});
|
|
101857
101938
|
var usageEvents = sqliteTable(
|
|
101858
101939
|
"usage_events",
|
|
@@ -111498,7 +111579,7 @@ var badgeVariants = cva(
|
|
|
111498
111579
|
// ../../packages/ui/src/components/button.tsx
|
|
111499
111580
|
var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
|
|
111500
111581
|
var buttonVariants = cva(
|
|
111501
|
-
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-
|
|
111582
|
+
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,opacity,transform] outline-none active:scale-[0.97] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
111502
111583
|
{
|
|
111503
111584
|
variants: {
|
|
111504
111585
|
variant: {
|
|
@@ -111597,10 +111678,10 @@ function DialogOverlay2({
|
|
|
111597
111678
|
);
|
|
111598
111679
|
}
|
|
111599
111680
|
var DIALOG_CONTENT_VARIANTS = {
|
|
111600
|
-
default: "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
|
111681
|
+
default: "ease-out fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
|
111601
111682
|
"bottom-sheet": [
|
|
111602
111683
|
// Shared
|
|
111603
|
-
"fixed z-50 flex flex-col bg-background shadow-lg duration-200 outline-none",
|
|
111684
|
+
"ease-out fixed z-50 flex flex-col bg-background shadow-lg duration-200 outline-none",
|
|
111604
111685
|
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
|
111605
111686
|
// Mobile (< lg): bottom drawer
|
|
111606
111687
|
"inset-x-0 bottom-0 w-full max-w-none rounded-t-2xl border border-b-0 p-6",
|
|
@@ -111612,8 +111693,11 @@ var DIALOG_CONTENT_VARIANTS = {
|
|
|
111612
111693
|
].join(" "),
|
|
111613
111694
|
"command-palette": [
|
|
111614
111695
|
// Shared
|
|
111615
|
-
"fixed z-50 flex flex-col bg-background shadow-lg
|
|
111616
|
-
|
|
111696
|
+
"fixed z-50 flex flex-col bg-background shadow-lg outline-none",
|
|
111697
|
+
// Mobile-only open/close animation: the drawer slide is touch-initiated.
|
|
111698
|
+
// Desktop (lg+) gets NO open/close animation — this is a keyboard-summoned
|
|
111699
|
+
// surface hit dozens of times a day; it must appear instantly.
|
|
111700
|
+
"max-lg:duration-200 max-lg:data-[state=closed]:animate-out max-lg:data-[state=closed]:fade-out-0 max-lg:data-[state=open]:animate-in max-lg:data-[state=open]:fade-in-0",
|
|
111617
111701
|
// Mobile (< lg): bottom drawer with the input pinned to the bottom. Flip
|
|
111618
111702
|
// the command's flex direction so the input sits below the results list,
|
|
111619
111703
|
// and flip the input wrapper's divider to a top border to match. The sheet
|
|
@@ -111626,8 +111710,7 @@ var DIALOG_CONTENT_VARIANTS = {
|
|
|
111626
111710
|
"max-lg:data-[state=open]:slide-in-from-bottom max-lg:data-[state=closed]:slide-out-to-bottom",
|
|
111627
111711
|
// Desktop (lg+): anchored in the upper third by its TOP edge — no
|
|
111628
111712
|
// `translate-y`, so the input never moves as the list grows downward.
|
|
111629
|
-
"lg:inset-x-auto lg:bottom-auto lg:top-[12vh] lg:left-1/2 lg:w-full lg:max-w-lg lg:max-h-[70vh] lg:-translate-x-1/2 lg:rounded-lg lg:border"
|
|
111630
|
-
"lg:data-[state=open]:zoom-in-95 lg:data-[state=closed]:zoom-out-95"
|
|
111713
|
+
"lg:inset-x-auto lg:bottom-auto lg:top-[12vh] lg:left-1/2 lg:w-full lg:max-w-lg lg:max-h-[70vh] lg:-translate-x-1/2 lg:rounded-lg lg:border"
|
|
111631
111714
|
].join(" ")
|
|
111632
111715
|
};
|
|
111633
111716
|
var keyboardInsetHolders = /* @__PURE__ */ new Set();
|
|
@@ -111663,7 +111746,15 @@ function DialogContent2({
|
|
|
111663
111746
|
}) {
|
|
111664
111747
|
useKeyboardInset(variant === "command-palette");
|
|
111665
111748
|
return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(DialogPortal2, { "data-slot": "dialog-portal", children: [
|
|
111666
|
-
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
|
|
111749
|
+
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
|
|
111750
|
+
DialogOverlay2,
|
|
111751
|
+
{
|
|
111752
|
+
className: cn(
|
|
111753
|
+
variant === "command-palette" && "lg:data-[state=open]:animate-none lg:data-[state=closed]:animate-none",
|
|
111754
|
+
overlayClassName
|
|
111755
|
+
)
|
|
111756
|
+
}
|
|
111757
|
+
),
|
|
111667
111758
|
/* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
|
|
111668
111759
|
dist_exports2.Content,
|
|
111669
111760
|
{
|
|
@@ -111774,7 +111865,7 @@ function ContextMenuContent2({
|
|
|
111774
111865
|
if (event.button === 2) event.stopPropagation();
|
|
111775
111866
|
},
|
|
111776
111867
|
className: cn(
|
|
111777
|
-
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
111868
|
+
"ease-out duration-150 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
111778
111869
|
className
|
|
111779
111870
|
),
|
|
111780
111871
|
...props
|
|
@@ -111868,7 +111959,7 @@ function TooltipContent2({
|
|
|
111868
111959
|
"data-slot": "tooltip-content",
|
|
111869
111960
|
sideOffset,
|
|
111870
111961
|
className: cn(
|
|
111871
|
-
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background
|
|
111962
|
+
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in fade-in-0 duration-100 ease-out rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:duration-75",
|
|
111872
111963
|
className
|
|
111873
111964
|
),
|
|
111874
111965
|
...props,
|
|
@@ -114728,8 +114819,24 @@ function SetupStatusIndicator({ setup }) {
|
|
|
114728
114819
|
return null;
|
|
114729
114820
|
}
|
|
114730
114821
|
|
|
114731
|
-
// src/dashboard/components/
|
|
114822
|
+
// src/dashboard/components/WorkspaceLabel.tsx
|
|
114732
114823
|
var import_jsx_runtime58 = __toESM(require_jsx_runtime(), 1);
|
|
114824
|
+
function WorkspaceLabel({
|
|
114825
|
+
name: name3,
|
|
114826
|
+
projectName,
|
|
114827
|
+
isActive,
|
|
114828
|
+
tone = "sidebar"
|
|
114829
|
+
}) {
|
|
114830
|
+
const nameClass = tone === "switcher" ? `text-foreground ${isActive ? "font-semibold" : "font-medium"}` : isActive ? "font-bold text-foreground" : "font-medium text-muted-foreground";
|
|
114831
|
+
const projectClass = tone === "switcher" ? "text-foreground/70" : isActive ? "text-foreground/80" : "text-muted-foreground";
|
|
114832
|
+
return /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)("div", { className: "flex flex-col min-w-0 leading-tight", children: [
|
|
114833
|
+
/* @__PURE__ */ (0, import_jsx_runtime58.jsx)("span", { className: cn("text-sm truncate", nameClass), children: name3 }),
|
|
114834
|
+
/* @__PURE__ */ (0, import_jsx_runtime58.jsx)("span", { className: cn("text-xs truncate", projectClass), children: projectName })
|
|
114835
|
+
] });
|
|
114836
|
+
}
|
|
114837
|
+
|
|
114838
|
+
// src/dashboard/components/WorkspaceCard.tsx
|
|
114839
|
+
var import_jsx_runtime59 = __toESM(require_jsx_runtime(), 1);
|
|
114733
114840
|
var RECENT_ACTIVATION_WINDOW_MS = 300;
|
|
114734
114841
|
var recentlyActivatedWorkspaceId = null;
|
|
114735
114842
|
var recentlyActivatedTimer = null;
|
|
@@ -114835,21 +114942,21 @@ var WorkspaceCard = (0, import_react22.memo)(function WorkspaceCard2({
|
|
|
114835
114942
|
});
|
|
114836
114943
|
}
|
|
114837
114944
|
};
|
|
114838
|
-
return /* @__PURE__ */ (0,
|
|
114839
|
-
/* @__PURE__ */ (0,
|
|
114840
|
-
/* @__PURE__ */ (0,
|
|
114841
|
-
/* @__PURE__ */ (0,
|
|
114945
|
+
return /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenu2, { children: [
|
|
114946
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(ContextMenuTrigger2, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { ...containerProps, children: [
|
|
114947
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(Tooltip2, { delayDuration: 800, children: [
|
|
114948
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(TooltipTrigger2, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "flex items-center gap-2 min-w-0 overflow-hidden", children: [
|
|
114842
114949
|
isRoot ? (
|
|
114843
114950
|
// On the root card the house is AgentStatusIndicator's idle
|
|
114844
114951
|
// fallback (the slot other cards use for the branch glyph),
|
|
114845
114952
|
// so an active agent's status dot replaces the house rather
|
|
114846
114953
|
// than sitting beside it.
|
|
114847
|
-
/* @__PURE__ */ (0,
|
|
114954
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
|
|
114848
114955
|
AgentStatusIndicator,
|
|
114849
114956
|
{
|
|
114850
114957
|
agent: status?.agent,
|
|
114851
114958
|
isActive,
|
|
114852
|
-
fallback: /* @__PURE__ */ (0,
|
|
114959
|
+
fallback: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
|
|
114853
114960
|
import_lucide_react5.Home,
|
|
114854
114961
|
{
|
|
114855
114962
|
"data-testid": "workspace-card__home-icon",
|
|
@@ -114858,29 +114965,22 @@ var WorkspaceCard = (0, import_react22.memo)(function WorkspaceCard2({
|
|
|
114858
114965
|
)
|
|
114859
114966
|
}
|
|
114860
114967
|
)
|
|
114861
|
-
) : /* @__PURE__ */ (0,
|
|
114968
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(AgentStatusIndicator, { agent: status?.agent, isActive }),
|
|
114862
114969
|
showProjectName ? (
|
|
114863
114970
|
// Pinned section: stack the branch name over the project name
|
|
114864
114971
|
// as a compact two-line block so a mixed list of pinned cards
|
|
114865
114972
|
// stays scannable without a long, mid-truncated
|
|
114866
|
-
// `project/branch` string on one line.
|
|
114867
|
-
|
|
114868
|
-
|
|
114869
|
-
|
|
114870
|
-
|
|
114871
|
-
|
|
114872
|
-
|
|
114873
|
-
|
|
114874
|
-
|
|
114875
|
-
|
|
114876
|
-
|
|
114877
|
-
{
|
|
114878
|
-
className: `text-xs truncate ${isActive ? "text-foreground/80" : "text-muted-foreground"}`,
|
|
114879
|
-
children: projectName
|
|
114880
|
-
}
|
|
114881
|
-
)
|
|
114882
|
-
] })
|
|
114883
|
-
) : /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
|
|
114973
|
+
// `project/branch` string on one line. Shared with the ⌘K
|
|
114974
|
+
// workspace picker via `WorkspaceLabel`.
|
|
114975
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
|
|
114976
|
+
WorkspaceLabel,
|
|
114977
|
+
{
|
|
114978
|
+
name: worktree.name,
|
|
114979
|
+
projectName,
|
|
114980
|
+
isActive
|
|
114981
|
+
}
|
|
114982
|
+
)
|
|
114983
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
|
|
114884
114984
|
"span",
|
|
114885
114985
|
{
|
|
114886
114986
|
className: `text-sm truncate ${isActive ? "font-bold text-foreground" : "font-medium text-muted-foreground"}`,
|
|
@@ -114888,46 +114988,46 @@ var WorkspaceCard = (0, import_react22.memo)(function WorkspaceCard2({
|
|
|
114888
114988
|
}
|
|
114889
114989
|
)
|
|
114890
114990
|
] }) }),
|
|
114891
|
-
/* @__PURE__ */ (0,
|
|
114991
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(TooltipContent2, { side: "right", children: `${projectName}/${worktree.name}` })
|
|
114892
114992
|
] }),
|
|
114893
|
-
/* @__PURE__ */ (0,
|
|
114894
|
-
/* @__PURE__ */ (0,
|
|
114895
|
-
!isPlain && branchStatus && /* @__PURE__ */ (0,
|
|
114896
|
-
!isPlain && branchStatus && /* @__PURE__ */ (0,
|
|
114993
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "hidden @[10rem]:flex group-hover:flex items-center gap-2 shrink-0 ml-auto pl-2", children: [
|
|
114994
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(SetupStatusIndicator, { setup: setupStatus }),
|
|
114995
|
+
!isPlain && branchStatus && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(GitStatusIndicator, { git: branchStatus.git }),
|
|
114996
|
+
!isPlain && branchStatus && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(CIStatusIndicator, { ci: branchStatus.ci })
|
|
114897
114997
|
] })
|
|
114898
114998
|
] }) }),
|
|
114899
|
-
/* @__PURE__ */ (0,
|
|
114900
|
-
/* @__PURE__ */ (0,
|
|
114901
|
-
isPinned ? /* @__PURE__ */ (0,
|
|
114999
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuContent2, { children: [
|
|
115000
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuItem2, { onClick: () => onTogglePinned(projectName, worktree.name, isPinned), children: [
|
|
115001
|
+
isPinned ? /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.PinOff, {}) : /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.Pin, {}),
|
|
114902
115002
|
isPinned ? "Unpin workspace" : "Pin workspace"
|
|
114903
115003
|
] }),
|
|
114904
|
-
/* @__PURE__ */ (0,
|
|
114905
|
-
capabilities.copyPath && /* @__PURE__ */ (0,
|
|
114906
|
-
/* @__PURE__ */ (0,
|
|
115004
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(ContextMenuSeparator2, {}),
|
|
115005
|
+
capabilities.copyPath && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuItem2, { onClick: () => navigator.clipboard.writeText(worktree.path), children: [
|
|
115006
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.Clipboard, {}),
|
|
114907
115007
|
"Copy path"
|
|
114908
115008
|
] }),
|
|
114909
|
-
capabilities.revealInFinder && /* @__PURE__ */ (0,
|
|
114910
|
-
/* @__PURE__ */ (0,
|
|
115009
|
+
capabilities.revealInFinder && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuItem2, { onClick: () => capabilities.revealInFinder(worktree.path), children: [
|
|
115010
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.FolderOpen, {}),
|
|
114911
115011
|
"Open in Finder"
|
|
114912
115012
|
] }),
|
|
114913
|
-
worktree.hasSetup && /* @__PURE__ */ (0,
|
|
114914
|
-
/* @__PURE__ */ (0,
|
|
115013
|
+
worktree.hasSetup && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuItem2, { onClick: () => runScript(worktree.path, "setup"), children: [
|
|
115014
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.Play, {}),
|
|
114915
115015
|
"Run setup"
|
|
114916
115016
|
] }),
|
|
114917
|
-
worktree.hasTeardown && /* @__PURE__ */ (0,
|
|
114918
|
-
/* @__PURE__ */ (0,
|
|
115017
|
+
worktree.hasTeardown && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuItem2, { onClick: () => runScript(worktree.path, "teardown"), children: [
|
|
115018
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.Square, {}),
|
|
114919
115019
|
"Run teardown"
|
|
114920
115020
|
] }),
|
|
114921
|
-
!isPlain && /* @__PURE__ */ (0,
|
|
114922
|
-
/* @__PURE__ */ (0,
|
|
115021
|
+
!isPlain && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuItem2, { onClick: () => gitPull(projectName, worktree.name), children: [
|
|
115022
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.ArrowDownToLine, {}),
|
|
114923
115023
|
"Git pull"
|
|
114924
115024
|
] }),
|
|
114925
|
-
!isPlain && /* @__PURE__ */ (0,
|
|
114926
|
-
/* @__PURE__ */ (0,
|
|
115025
|
+
!isPlain && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuItem2, { onClick: () => gitPush(projectName, worktree.name), children: [
|
|
115026
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.ArrowUpFromLine, {}),
|
|
114927
115027
|
"Git push"
|
|
114928
115028
|
] }),
|
|
114929
|
-
!isPlain && worktree.name !== defaultBranch && /* @__PURE__ */ (0,
|
|
114930
|
-
/* @__PURE__ */ (0,
|
|
115029
|
+
!isPlain && worktree.name !== defaultBranch && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(ContextMenuItem2, { variant: "destructive", onClick: handleDelete, children: [
|
|
115030
|
+
/* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_lucide_react5.Trash2, {}),
|
|
114931
115031
|
"Delete workspace"
|
|
114932
115032
|
] })
|
|
114933
115033
|
] })
|
|
@@ -114935,7 +115035,7 @@ var WorkspaceCard = (0, import_react22.memo)(function WorkspaceCard2({
|
|
|
114935
115035
|
});
|
|
114936
115036
|
|
|
114937
115037
|
// src/dashboard/components/ProjectList.tsx
|
|
114938
|
-
var
|
|
115038
|
+
var import_jsx_runtime60 = __toESM(require_jsx_runtime(), 1);
|
|
114939
115039
|
|
|
114940
115040
|
// src/dashboard/components/SettingsPage.tsx
|
|
114941
115041
|
var import_react25 = __toESM(require_react(), 1);
|
|
@@ -114944,16 +115044,16 @@ var import_react25 = __toESM(require_react(), 1);
|
|
|
114944
115044
|
var import_react24 = __toESM(require_react(), 1);
|
|
114945
115045
|
|
|
114946
115046
|
// src/dashboard/components/settings/SettingsRow.tsx
|
|
114947
|
-
var
|
|
115047
|
+
var import_jsx_runtime61 = __toESM(require_jsx_runtime(), 1);
|
|
114948
115048
|
|
|
114949
115049
|
// src/dashboard/components/settings/SettingsSection.tsx
|
|
114950
|
-
var
|
|
115050
|
+
var import_jsx_runtime62 = __toESM(require_jsx_runtime(), 1);
|
|
114951
115051
|
|
|
114952
115052
|
// src/dashboard/components/SettingsPage.tsx
|
|
114953
|
-
var
|
|
115053
|
+
var import_jsx_runtime63 = __toESM(require_jsx_runtime(), 1);
|
|
114954
115054
|
|
|
114955
115055
|
// src/dashboard/components/DashboardShell.tsx
|
|
114956
|
-
var
|
|
115056
|
+
var import_jsx_runtime64 = __toESM(require_jsx_runtime(), 1);
|
|
114957
115057
|
var isElectron = typeof window !== "undefined" && "__BAND_DESKTOP__" in window;
|
|
114958
115058
|
|
|
114959
115059
|
// src/dashboard/components/DiffView.tsx
|
|
@@ -115219,18 +115319,18 @@ function joinWorkspacePath(workspaceRoot, relativePath) {
|
|
|
115219
115319
|
}
|
|
115220
115320
|
|
|
115221
115321
|
// src/dashboard/components/FileStatusBadge.tsx
|
|
115222
|
-
var
|
|
115322
|
+
var import_jsx_runtime65 = __toESM(require_jsx_runtime(), 1);
|
|
115223
115323
|
|
|
115224
115324
|
// src/dashboard/components/ChangesFileTree.tsx
|
|
115225
|
-
var
|
|
115325
|
+
var import_jsx_runtime66 = __toESM(require_jsx_runtime(), 1);
|
|
115226
115326
|
|
|
115227
115327
|
// src/dashboard/components/RevertFileDialog.tsx
|
|
115228
|
-
var
|
|
115328
|
+
var import_jsx_runtime67 = __toESM(require_jsx_runtime(), 1);
|
|
115229
115329
|
|
|
115230
115330
|
// src/dashboard/components/SearchBar.tsx
|
|
115231
115331
|
var import_lucide_react6 = __toESM(require_lucide_react(), 1);
|
|
115232
115332
|
var import_react34 = __toESM(require_react(), 1);
|
|
115233
|
-
var
|
|
115333
|
+
var import_jsx_runtime68 = __toESM(require_jsx_runtime(), 1);
|
|
115234
115334
|
var DEFAULT_VISIBLE_OPTIONS = ["caseSensitive", "wholeWord", "regex"];
|
|
115235
115335
|
function ToggleButton({
|
|
115236
115336
|
active,
|
|
@@ -115238,7 +115338,7 @@ function ToggleButton({
|
|
|
115238
115338
|
title,
|
|
115239
115339
|
children
|
|
115240
115340
|
}) {
|
|
115241
|
-
return /* @__PURE__ */ (0,
|
|
115341
|
+
return /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
|
|
115242
115342
|
"button",
|
|
115243
115343
|
{
|
|
115244
115344
|
type: "button",
|
|
@@ -115279,7 +115379,7 @@ var SearchBar = (0, import_react34.forwardRef)(function SearchBar2({
|
|
|
115279
115379
|
const toggleRegex = (0, import_react34.useCallback)(() => {
|
|
115280
115380
|
onOptionsChange({ ...options2, regex: !options2.regex });
|
|
115281
115381
|
}, [options2, onOptionsChange]);
|
|
115282
|
-
return /* @__PURE__ */ (0,
|
|
115382
|
+
return /* @__PURE__ */ (0, import_jsx_runtime68.jsxs)(
|
|
115283
115383
|
"div",
|
|
115284
115384
|
{
|
|
115285
115385
|
className: cn(
|
|
@@ -115287,8 +115387,8 @@ var SearchBar = (0, import_react34.forwardRef)(function SearchBar2({
|
|
|
115287
115387
|
className
|
|
115288
115388
|
),
|
|
115289
115389
|
children: [
|
|
115290
|
-
/* @__PURE__ */ (0,
|
|
115291
|
-
/* @__PURE__ */ (0,
|
|
115390
|
+
/* @__PURE__ */ (0, import_jsx_runtime68.jsx)(import_lucide_react6.Search, { className: "size-3.5 shrink-0 text-muted-foreground" }),
|
|
115391
|
+
/* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
|
|
115292
115392
|
"input",
|
|
115293
115393
|
{
|
|
115294
115394
|
ref: inputRef,
|
|
@@ -115309,11 +115409,11 @@ var SearchBar = (0, import_react34.forwardRef)(function SearchBar2({
|
|
|
115309
115409
|
}
|
|
115310
115410
|
}
|
|
115311
115411
|
),
|
|
115312
|
-
showCase && /* @__PURE__ */ (0,
|
|
115313
|
-
showWholeWord && /* @__PURE__ */ (0,
|
|
115314
|
-
showRegex && /* @__PURE__ */ (0,
|
|
115315
|
-
matchInfo && query && /* @__PURE__ */ (0,
|
|
115316
|
-
onPrevious && /* @__PURE__ */ (0,
|
|
115412
|
+
showCase && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(ToggleButton, { active: options2.caseSensitive, onClick: toggleCase, title: "Match Case", children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(import_lucide_react6.CaseSensitive, { className: "size-4" }) }),
|
|
115413
|
+
showWholeWord && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(ToggleButton, { active: options2.wholeWord, onClick: toggleWholeWord, title: "Match Whole Word", children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(import_lucide_react6.WholeWord, { className: "size-4" }) }),
|
|
115414
|
+
showRegex && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(ToggleButton, { active: options2.regex, onClick: toggleRegex, title: "Use Regular Expression", children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(import_lucide_react6.Regex, { className: "size-4" }) }),
|
|
115415
|
+
matchInfo && query && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("span", { className: "shrink-0 text-xs text-muted-foreground tabular-nums", children: matchInfo.total > 0 ? `${matchInfo.current} of ${matchInfo.total}` : "No results" }),
|
|
115416
|
+
onPrevious && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
|
|
115317
115417
|
"button",
|
|
115318
115418
|
{
|
|
115319
115419
|
type: "button",
|
|
@@ -115321,10 +115421,10 @@ var SearchBar = (0, import_react34.forwardRef)(function SearchBar2({
|
|
|
115321
115421
|
disabled: !matchInfo || matchInfo.total === 0,
|
|
115322
115422
|
className: "inline-flex size-6 items-center justify-center rounded text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
115323
115423
|
title: "Previous match (Shift+Enter)",
|
|
115324
|
-
children: /* @__PURE__ */ (0,
|
|
115424
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(import_lucide_react6.ChevronUp, { className: "size-3.5" })
|
|
115325
115425
|
}
|
|
115326
115426
|
),
|
|
115327
|
-
onNext && /* @__PURE__ */ (0,
|
|
115427
|
+
onNext && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
|
|
115328
115428
|
"button",
|
|
115329
115429
|
{
|
|
115330
115430
|
type: "button",
|
|
@@ -115332,17 +115432,17 @@ var SearchBar = (0, import_react34.forwardRef)(function SearchBar2({
|
|
|
115332
115432
|
disabled: !matchInfo || matchInfo.total === 0,
|
|
115333
115433
|
className: "inline-flex size-6 items-center justify-center rounded text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
115334
115434
|
title: "Next match (Enter)",
|
|
115335
|
-
children: /* @__PURE__ */ (0,
|
|
115435
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(import_lucide_react6.ChevronDown, { className: "size-3.5" })
|
|
115336
115436
|
}
|
|
115337
115437
|
),
|
|
115338
|
-
onClose && /* @__PURE__ */ (0,
|
|
115438
|
+
onClose && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
|
|
115339
115439
|
"button",
|
|
115340
115440
|
{
|
|
115341
115441
|
type: "button",
|
|
115342
115442
|
onClick: onClose,
|
|
115343
115443
|
className: "inline-flex size-6 items-center justify-center rounded text-muted-foreground hover:text-foreground",
|
|
115344
115444
|
title: "Close (Escape)",
|
|
115345
|
-
children: /* @__PURE__ */ (0,
|
|
115445
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(import_lucide_react6.X, { className: "size-3.5" })
|
|
115346
115446
|
}
|
|
115347
115447
|
)
|
|
115348
115448
|
]
|
|
@@ -115351,7 +115451,7 @@ var SearchBar = (0, import_react34.forwardRef)(function SearchBar2({
|
|
|
115351
115451
|
});
|
|
115352
115452
|
|
|
115353
115453
|
// src/dashboard/components/DiffView.tsx
|
|
115354
|
-
var
|
|
115454
|
+
var import_jsx_runtime69 = __toESM(require_jsx_runtime(), 1);
|
|
115355
115455
|
var diffTheme = EditorView.theme({
|
|
115356
115456
|
".cm-insertedLine": { backgroundColor: "rgba(34, 197, 94, 0.1)" },
|
|
115357
115457
|
".cm-deletedLine": { backgroundColor: "rgba(239, 68, 68, 0.1)" },
|
|
@@ -115403,7 +115503,7 @@ var diffTheme = EditorView.theme({
|
|
|
115403
115503
|
// src/dashboard/components/FileBrowser.tsx
|
|
115404
115504
|
var import_lucide_react7 = __toESM(require_lucide_react(), 1);
|
|
115405
115505
|
var import_react36 = __toESM(require_react(), 1);
|
|
115406
|
-
var
|
|
115506
|
+
var import_jsx_runtime70 = __toESM(require_jsx_runtime(), 1);
|
|
115407
115507
|
var expandedStateCache = /* @__PURE__ */ new Map();
|
|
115408
115508
|
var dirContentsCache = /* @__PURE__ */ new Map();
|
|
115409
115509
|
function getCachedExpanded(wsId) {
|
|
@@ -115484,21 +115584,21 @@ function EntryNameInput({
|
|
|
115484
115584
|
}
|
|
115485
115585
|
};
|
|
115486
115586
|
const Icon = kind === "directory" ? import_lucide_react7.Folder : import_lucide_react7.File;
|
|
115487
|
-
return /* @__PURE__ */ (0,
|
|
115488
|
-
/* @__PURE__ */ (0,
|
|
115587
|
+
return /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)("div", { className: "flex w-full flex-col", style: { paddingLeft: `${depth * indent + basePad}px` }, children: [
|
|
115588
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115489
115589
|
"div",
|
|
115490
115590
|
{
|
|
115491
115591
|
className: `flex items-center ${compact ? "h-[28px] gap-1 pr-3 text-[13px]" : "h-[32px] gap-1.5 pr-4 text-[15px]"}`,
|
|
115492
115592
|
style: { height },
|
|
115493
115593
|
children: [
|
|
115494
|
-
/* @__PURE__ */ (0,
|
|
115495
|
-
/* @__PURE__ */ (0,
|
|
115594
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)("span", { className: "size-3.5 shrink-0" }),
|
|
115595
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
115496
115596
|
Icon,
|
|
115497
115597
|
{
|
|
115498
115598
|
className: kind === "directory" ? "size-4 shrink-0 text-blue-600 dark:text-blue-400" : "size-4 shrink-0 text-muted-foreground"
|
|
115499
115599
|
}
|
|
115500
115600
|
),
|
|
115501
|
-
/* @__PURE__ */ (0,
|
|
115601
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
115502
115602
|
"input",
|
|
115503
115603
|
{
|
|
115504
115604
|
ref: inputRef,
|
|
@@ -115535,7 +115635,7 @@ function EntryNameInput({
|
|
|
115535
115635
|
]
|
|
115536
115636
|
}
|
|
115537
115637
|
),
|
|
115538
|
-
error2 && /* @__PURE__ */ (0,
|
|
115638
|
+
error2 && /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
115539
115639
|
"div",
|
|
115540
115640
|
{
|
|
115541
115641
|
className: "pointer-events-none text-xs text-destructive",
|
|
@@ -115603,7 +115703,7 @@ function TreeNode2({
|
|
|
115603
115703
|
onOpenFilePinned(entryPath);
|
|
115604
115704
|
}
|
|
115605
115705
|
};
|
|
115606
|
-
const button = /* @__PURE__ */ (0,
|
|
115706
|
+
const button = /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115607
115707
|
"button",
|
|
115608
115708
|
{
|
|
115609
115709
|
ref: isSelected ? selectedRef : void 0,
|
|
@@ -115617,15 +115717,15 @@ function TreeNode2({
|
|
|
115617
115717
|
className: `flex w-full items-center text-left select-none hover:bg-accent/50 [-webkit-touch-callout:none] ${compact ? "h-[28px] gap-1 pr-3 text-[13px]" : "h-[32px] gap-1.5 pr-4 text-[15px]"} ${isSelected ? "bg-blue-500/30 text-foreground outline outline-1 -outline-offset-1 outline-blue-400/60 hover:bg-blue-500/30 dark:bg-blue-500/40 dark:outline-blue-400/70 dark:hover:bg-blue-500/40" : ""} ${isCut ? "opacity-50" : ""}`,
|
|
115618
115718
|
style: { paddingLeft: `${depth * indent + basePad}px` },
|
|
115619
115719
|
children: [
|
|
115620
|
-
isDir ? isExpanded ? /* @__PURE__ */ (0,
|
|
115720
|
+
isDir ? isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.ChevronDown, { className: "size-3.5 shrink-0 text-muted-foreground/70" }) : /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.ChevronRight, { className: "size-3.5 shrink-0 text-muted-foreground/70" }) : /* @__PURE__ */ (0, import_jsx_runtime70.jsx)("span", { className: "size-3.5 shrink-0" }),
|
|
115621
115721
|
isDir ? (() => {
|
|
115622
115722
|
const FolderIcon = getFolderIcon(entry.name, isExpanded);
|
|
115623
|
-
return /* @__PURE__ */ (0,
|
|
115723
|
+
return /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(FolderIcon, { className: "size-4 shrink-0" });
|
|
115624
115724
|
})() : (() => {
|
|
115625
115725
|
const FileIcon = getFileIcon(entry.name);
|
|
115626
|
-
return /* @__PURE__ */ (0,
|
|
115726
|
+
return /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(FileIcon, { className: "size-4 shrink-0" });
|
|
115627
115727
|
})(),
|
|
115628
|
-
/* @__PURE__ */ (0,
|
|
115728
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)("span", { className: "min-w-0 flex-1 truncate", children: entry.name })
|
|
115629
115729
|
]
|
|
115630
115730
|
}
|
|
115631
115731
|
);
|
|
@@ -115647,7 +115747,7 @@ function TreeNode2({
|
|
|
115647
115747
|
// a padding div with `depth={0}` would double-apply `basePad`, shifting
|
|
115648
115748
|
// the input ~4px to the right and giving it visibly less horizontal
|
|
115649
115749
|
// room than the rest of the tree.
|
|
115650
|
-
/* @__PURE__ */ (0,
|
|
115750
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
115651
115751
|
EntryNameInput,
|
|
115652
115752
|
{
|
|
115653
115753
|
kind: isDir ? "directory" : "file",
|
|
@@ -115661,97 +115761,97 @@ function TreeNode2({
|
|
|
115661
115761
|
},
|
|
115662
115762
|
`rename-${entryPath}`
|
|
115663
115763
|
)
|
|
115664
|
-
) : /* @__PURE__ */ (0,
|
|
115764
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115665
115765
|
ContextMenu2,
|
|
115666
115766
|
{
|
|
115667
115767
|
onOpenChange: (open3) => {
|
|
115668
115768
|
if (open3) onSelectRow(entryPath, isDir ? "directory" : "file");
|
|
115669
115769
|
},
|
|
115670
115770
|
children: [
|
|
115671
|
-
/* @__PURE__ */ (0,
|
|
115672
|
-
/* @__PURE__ */ (0,
|
|
115673
|
-
isDir && /* @__PURE__ */ (0,
|
|
115674
|
-
/* @__PURE__ */ (0,
|
|
115771
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(ContextMenuTrigger2, { asChild: true, children: button }),
|
|
115772
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(ContextMenuContent2, { onCloseAutoFocus: menu.flush, children: [
|
|
115773
|
+
isDir && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(import_jsx_runtime70.Fragment, { children: [
|
|
115774
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115675
115775
|
ContextMenuItem2,
|
|
115676
115776
|
{
|
|
115677
115777
|
onSelect: () => menu.queue(() => onRequestNewEntry(entryPath, "file")),
|
|
115678
115778
|
children: [
|
|
115679
|
-
/* @__PURE__ */ (0,
|
|
115779
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.File, { className: "size-4" }),
|
|
115680
115780
|
"New File"
|
|
115681
115781
|
]
|
|
115682
115782
|
}
|
|
115683
115783
|
),
|
|
115684
|
-
/* @__PURE__ */ (0,
|
|
115784
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115685
115785
|
ContextMenuItem2,
|
|
115686
115786
|
{
|
|
115687
115787
|
onSelect: () => menu.queue(() => onRequestNewEntry(entryPath, "directory")),
|
|
115688
115788
|
children: [
|
|
115689
|
-
/* @__PURE__ */ (0,
|
|
115789
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.FolderPlus, { className: "size-4" }),
|
|
115690
115790
|
"New Folder"
|
|
115691
115791
|
]
|
|
115692
115792
|
}
|
|
115693
115793
|
),
|
|
115694
|
-
/* @__PURE__ */ (0,
|
|
115794
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(ContextMenuSeparator2, {})
|
|
115695
115795
|
] }),
|
|
115696
|
-
canCut && /* @__PURE__ */ (0,
|
|
115796
|
+
canCut && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115697
115797
|
ContextMenuItem2,
|
|
115698
115798
|
{
|
|
115699
115799
|
onSelect: () => menu.queue(() => onCut(entryPath, isDir ? "directory" : "file")),
|
|
115700
115800
|
children: [
|
|
115701
|
-
/* @__PURE__ */ (0,
|
|
115801
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.Scissors, { className: "size-4" }),
|
|
115702
115802
|
"Cut"
|
|
115703
115803
|
]
|
|
115704
115804
|
}
|
|
115705
115805
|
),
|
|
115706
|
-
canCopy && /* @__PURE__ */ (0,
|
|
115806
|
+
canCopy && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115707
115807
|
ContextMenuItem2,
|
|
115708
115808
|
{
|
|
115709
115809
|
onSelect: () => menu.queue(() => onCopy(entryPath, isDir ? "directory" : "file")),
|
|
115710
115810
|
children: [
|
|
115711
|
-
/* @__PURE__ */ (0,
|
|
115811
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.Copy, { className: "size-4" }),
|
|
115712
115812
|
"Copy"
|
|
115713
115813
|
]
|
|
115714
115814
|
}
|
|
115715
115815
|
),
|
|
115716
|
-
isDir && canPaste && /* @__PURE__ */ (0,
|
|
115717
|
-
/* @__PURE__ */ (0,
|
|
115816
|
+
isDir && canPaste && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(ContextMenuItem2, { onSelect: () => menu.queue(() => void onPaste(entryPath)), children: [
|
|
115817
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.ClipboardPaste, { className: "size-4" }),
|
|
115718
115818
|
"Paste"
|
|
115719
115819
|
] }),
|
|
115720
|
-
(canCut || canCopy || isDir && canPaste) && /* @__PURE__ */ (0,
|
|
115721
|
-
/* @__PURE__ */ (0,
|
|
115820
|
+
(canCut || canCopy || isDir && canPaste) && /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(ContextMenuSeparator2, {}),
|
|
115821
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115722
115822
|
ContextMenuItem2,
|
|
115723
115823
|
{
|
|
115724
115824
|
"data-testid": "file-tree__copy-relative-path",
|
|
115725
115825
|
onSelect: () => menu.queue(() => void writeClipboardText(entryPath)),
|
|
115726
115826
|
children: [
|
|
115727
|
-
/* @__PURE__ */ (0,
|
|
115827
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.ClipboardCopy, { className: "size-4" }),
|
|
115728
115828
|
"Copy relative path"
|
|
115729
115829
|
]
|
|
115730
115830
|
}
|
|
115731
115831
|
),
|
|
115732
|
-
workspacePath && /* @__PURE__ */ (0,
|
|
115832
|
+
workspacePath && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115733
115833
|
ContextMenuItem2,
|
|
115734
115834
|
{
|
|
115735
115835
|
"data-testid": "file-tree__copy-absolute-path",
|
|
115736
115836
|
onSelect: () => menu.queue(() => void writeClipboardText(joinWorkspacePath(workspacePath, entryPath))),
|
|
115737
115837
|
children: [
|
|
115738
|
-
/* @__PURE__ */ (0,
|
|
115838
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.ClipboardCopy, { className: "size-4" }),
|
|
115739
115839
|
"Copy absolute path"
|
|
115740
115840
|
]
|
|
115741
115841
|
}
|
|
115742
115842
|
),
|
|
115743
|
-
(canRename || canDelete) && /* @__PURE__ */ (0,
|
|
115744
|
-
canRename && /* @__PURE__ */ (0,
|
|
115745
|
-
/* @__PURE__ */ (0,
|
|
115843
|
+
(canRename || canDelete) && /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(ContextMenuSeparator2, {}),
|
|
115844
|
+
canRename && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(ContextMenuItem2, { onSelect: () => menu.queue(() => onRequestRename(entryPath)), children: [
|
|
115845
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.Pencil, { className: "size-4" }),
|
|
115746
115846
|
"Rename"
|
|
115747
115847
|
] }),
|
|
115748
|
-
canDelete && /* @__PURE__ */ (0,
|
|
115848
|
+
canDelete && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
115749
115849
|
ContextMenuItem2,
|
|
115750
115850
|
{
|
|
115751
115851
|
variant: "destructive",
|
|
115752
115852
|
onSelect: () => menu.queue(() => onRequestDelete(entryPath, isDir ? "directory" : "file")),
|
|
115753
115853
|
children: [
|
|
115754
|
-
/* @__PURE__ */ (0,
|
|
115854
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.Trash2, { className: "size-4" }),
|
|
115755
115855
|
"Delete"
|
|
115756
115856
|
]
|
|
115757
115857
|
}
|
|
@@ -115761,13 +115861,13 @@ function TreeNode2({
|
|
|
115761
115861
|
}
|
|
115762
115862
|
);
|
|
115763
115863
|
const showInlineInput = newEntry?.parentPath === entryPath;
|
|
115764
|
-
return /* @__PURE__ */ (0,
|
|
115864
|
+
return /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(import_jsx_runtime70.Fragment, { children: [
|
|
115765
115865
|
row,
|
|
115766
115866
|
isExpanded && (() => {
|
|
115767
115867
|
const splitIdx = children?.findIndex((c2) => c2.type === "file") ?? -1;
|
|
115768
115868
|
const folderChildren = splitIdx === -1 ? children ?? [] : (children ?? []).slice(0, splitIdx);
|
|
115769
115869
|
const fileChildren = splitIdx === -1 ? [] : (children ?? []).slice(splitIdx);
|
|
115770
|
-
const renderChild = (child) => /* @__PURE__ */ (0,
|
|
115870
|
+
const renderChild = (child) => /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
115771
115871
|
TreeNode2,
|
|
115772
115872
|
{
|
|
115773
115873
|
entry: child,
|
|
@@ -115805,7 +115905,7 @@ function TreeNode2({
|
|
|
115805
115905
|
},
|
|
115806
115906
|
child.name
|
|
115807
115907
|
);
|
|
115808
|
-
const newEntryInput = showInlineInput ? /* @__PURE__ */ (0,
|
|
115908
|
+
const newEntryInput = showInlineInput ? /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
115809
115909
|
EntryNameInput,
|
|
115810
115910
|
{
|
|
115811
115911
|
kind: newEntry.kind,
|
|
@@ -115817,9 +115917,9 @@ function TreeNode2({
|
|
|
115817
115917
|
},
|
|
115818
115918
|
`new-${newEntry.kind}-${entryPath}`
|
|
115819
115919
|
) : null;
|
|
115820
|
-
return /* @__PURE__ */ (0,
|
|
115920
|
+
return /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(import_jsx_runtime70.Fragment, { children: [
|
|
115821
115921
|
showInlineInput && newEntry.kind === "directory" && newEntryInput,
|
|
115822
|
-
isLoading && !children?.length && /* @__PURE__ */ (0,
|
|
115922
|
+
isLoading && !children?.length && /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
115823
115923
|
"div",
|
|
115824
115924
|
{
|
|
115825
115925
|
className: `flex items-center text-muted-foreground/70 ${compact ? "text-[13px]" : "text-[15px]"}`,
|
|
@@ -115830,7 +115930,7 @@ function TreeNode2({
|
|
|
115830
115930
|
children: "Loading\u2026"
|
|
115831
115931
|
}
|
|
115832
115932
|
),
|
|
115833
|
-
!isLoading && !showInlineInput && children && children.length === 0 && /* @__PURE__ */ (0,
|
|
115933
|
+
!isLoading && !showInlineInput && children && children.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
115834
115934
|
"div",
|
|
115835
115935
|
{
|
|
115836
115936
|
className: `flex items-center italic text-muted-foreground/50 ${compact ? "text-[13px]" : "text-[15px]"}`,
|
|
@@ -116276,7 +116376,7 @@ var FileBrowser = (0, import_react36.forwardRef)(function FileBrowser2({
|
|
|
116276
116376
|
);
|
|
116277
116377
|
const rootMenu = useDeferredMenuAction();
|
|
116278
116378
|
if (!adapter.listWorkspaceFiles) {
|
|
116279
|
-
return /* @__PURE__ */ (0,
|
|
116379
|
+
return /* @__PURE__ */ (0, import_jsx_runtime70.jsx)("div", { className: "flex h-32 items-center justify-center text-sm text-muted-foreground", children: "File browsing not supported" });
|
|
116280
116380
|
}
|
|
116281
116381
|
const rootEntries = dirContents.get("") ?? [];
|
|
116282
116382
|
const rootLoading = loadingPaths.has("");
|
|
@@ -116313,20 +116413,20 @@ var FileBrowser = (0, import_react36.forwardRef)(function FileBrowser2({
|
|
|
116313
116413
|
void pasteInto(resolveDefaultTarget());
|
|
116314
116414
|
}
|
|
116315
116415
|
};
|
|
116316
|
-
return /* @__PURE__ */ (0,
|
|
116317
|
-
/* @__PURE__ */ (0,
|
|
116416
|
+
return /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)("div", { className: "flex h-full flex-col overflow-hidden", onKeyDown: handleKeyDown, children: [
|
|
116417
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(
|
|
116318
116418
|
ContextMenu2,
|
|
116319
116419
|
{
|
|
116320
116420
|
onOpenChange: (open3) => {
|
|
116321
116421
|
if (open3) clearTreeSelection();
|
|
116322
116422
|
},
|
|
116323
116423
|
children: [
|
|
116324
|
-
/* @__PURE__ */ (0,
|
|
116424
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(ContextMenuTrigger2, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)("div", { className: "min-h-0 flex-1 overflow-y-auto py-1 pl-px", children: [
|
|
116325
116425
|
(() => {
|
|
116326
116426
|
const rootSplitIdx = rootEntries.findIndex((e2) => e2.type === "file");
|
|
116327
116427
|
const rootFolders = rootSplitIdx === -1 ? rootEntries : rootEntries.slice(0, rootSplitIdx);
|
|
116328
116428
|
const rootFiles = rootSplitIdx === -1 ? [] : rootEntries.slice(rootSplitIdx);
|
|
116329
|
-
const renderRow = (entry) => /* @__PURE__ */ (0,
|
|
116429
|
+
const renderRow = (entry) => /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
116330
116430
|
TreeNode2,
|
|
116331
116431
|
{
|
|
116332
116432
|
entry,
|
|
@@ -116364,7 +116464,7 @@ var FileBrowser = (0, import_react36.forwardRef)(function FileBrowser2({
|
|
|
116364
116464
|
},
|
|
116365
116465
|
entry.name
|
|
116366
116466
|
);
|
|
116367
|
-
const rootInput = showRootInput ? /* @__PURE__ */ (0,
|
|
116467
|
+
const rootInput = showRootInput ? /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
116368
116468
|
EntryNameInput,
|
|
116369
116469
|
{
|
|
116370
116470
|
kind: newEntry.kind,
|
|
@@ -116376,30 +116476,30 @@ var FileBrowser = (0, import_react36.forwardRef)(function FileBrowser2({
|
|
|
116376
116476
|
},
|
|
116377
116477
|
`new-${newEntry.kind}-root`
|
|
116378
116478
|
) : null;
|
|
116379
|
-
return /* @__PURE__ */ (0,
|
|
116479
|
+
return /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(import_jsx_runtime70.Fragment, { children: [
|
|
116380
116480
|
showRootInput && newEntry.kind === "directory" && rootInput,
|
|
116381
|
-
rootLoading && rootEntries.length === 0 && !showRootInput && /* @__PURE__ */ (0,
|
|
116382
|
-
!rootLoading && rootEntries.length === 0 && !showRootInput && /* @__PURE__ */ (0,
|
|
116481
|
+
rootLoading && rootEntries.length === 0 && !showRootInput && /* @__PURE__ */ (0, import_jsx_runtime70.jsx)("div", { className: "flex h-32 items-center justify-center text-sm text-muted-foreground", children: "Loading\u2026" }),
|
|
116482
|
+
!rootLoading && rootEntries.length === 0 && !showRootInput && /* @__PURE__ */ (0, import_jsx_runtime70.jsx)("div", { className: "flex h-32 items-center justify-center text-sm text-muted-foreground", children: "Empty directory" }),
|
|
116383
116483
|
rootFolders.map(renderRow),
|
|
116384
116484
|
showRootInput && newEntry.kind === "file" && rootInput,
|
|
116385
116485
|
rootFiles.map(renderRow)
|
|
116386
116486
|
] });
|
|
116387
116487
|
})(),
|
|
116388
|
-
/* @__PURE__ */ (0,
|
|
116488
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)("div", { className: "min-h-[40px] flex-1" })
|
|
116389
116489
|
] }) }),
|
|
116390
|
-
/* @__PURE__ */ (0,
|
|
116391
|
-
/* @__PURE__ */ (0,
|
|
116392
|
-
/* @__PURE__ */ (0,
|
|
116490
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(ContextMenuContent2, { onCloseAutoFocus: rootMenu.flush, children: [
|
|
116491
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(ContextMenuItem2, { onSelect: () => rootMenu.queue(() => requestNewEntry("", "file")), children: [
|
|
116492
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.File, { className: "size-4" }),
|
|
116393
116493
|
"New File"
|
|
116394
116494
|
] }),
|
|
116395
|
-
/* @__PURE__ */ (0,
|
|
116396
|
-
/* @__PURE__ */ (0,
|
|
116495
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(ContextMenuItem2, { onSelect: () => rootMenu.queue(() => requestNewEntry("", "directory")), children: [
|
|
116496
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.FolderPlus, { className: "size-4" }),
|
|
116397
116497
|
"New Folder"
|
|
116398
116498
|
] }),
|
|
116399
|
-
canPaste && /* @__PURE__ */ (0,
|
|
116400
|
-
/* @__PURE__ */ (0,
|
|
116401
|
-
/* @__PURE__ */ (0,
|
|
116402
|
-
/* @__PURE__ */ (0,
|
|
116499
|
+
canPaste && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(import_jsx_runtime70.Fragment, { children: [
|
|
116500
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(ContextMenuSeparator2, {}),
|
|
116501
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(ContextMenuItem2, { onSelect: () => rootMenu.queue(() => void pasteInto("")), children: [
|
|
116502
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.ClipboardPaste, { className: "size-4" }),
|
|
116403
116503
|
"Paste"
|
|
116404
116504
|
] })
|
|
116405
116505
|
] })
|
|
@@ -116407,42 +116507,42 @@ var FileBrowser = (0, import_react36.forwardRef)(function FileBrowser2({
|
|
|
116407
116507
|
]
|
|
116408
116508
|
}
|
|
116409
116509
|
),
|
|
116410
|
-
/* @__PURE__ */ (0,
|
|
116510
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
116411
116511
|
Dialog2,
|
|
116412
116512
|
{
|
|
116413
116513
|
open: pendingDelete !== null,
|
|
116414
116514
|
onOpenChange: (open3) => {
|
|
116415
116515
|
if (!open3) cancelDelete();
|
|
116416
116516
|
},
|
|
116417
|
-
children: /* @__PURE__ */ (0,
|
|
116418
|
-
/* @__PURE__ */ (0,
|
|
116419
|
-
/* @__PURE__ */ (0,
|
|
116517
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(DialogContent2, { className: "sm:max-w-[425px]", onClick: (e2) => e2.stopPropagation(), children: [
|
|
116518
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(DialogHeader, { children: [
|
|
116519
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(DialogTitle2, { children: [
|
|
116420
116520
|
"Delete ",
|
|
116421
116521
|
pendingDelete?.kind === "directory" ? "folder" : "file"
|
|
116422
116522
|
] }),
|
|
116423
|
-
/* @__PURE__ */ (0,
|
|
116523
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(DialogDescription2, { children: [
|
|
116424
116524
|
"Are you sure you want to delete ",
|
|
116425
|
-
/* @__PURE__ */ (0,
|
|
116525
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)("strong", { children: pendingDelete?.path }),
|
|
116426
116526
|
"?"
|
|
116427
116527
|
] })
|
|
116428
116528
|
] }),
|
|
116429
|
-
/* @__PURE__ */ (0,
|
|
116430
|
-
pendingDelete?.kind === "directory" && /* @__PURE__ */ (0,
|
|
116431
|
-
/* @__PURE__ */ (0,
|
|
116432
|
-
/* @__PURE__ */ (0,
|
|
116529
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)("div", { className: "flex flex-col gap-2 text-sm", children: [
|
|
116530
|
+
pendingDelete?.kind === "directory" && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)("div", { className: "flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3", children: [
|
|
116531
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.AlertTriangle, { className: "size-4 shrink-0 text-yellow-500 mt-0.5" }),
|
|
116532
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)("span", { children: "The folder and all of its contents will be deleted from disk. This cannot be undone." })
|
|
116433
116533
|
] }),
|
|
116434
|
-
pendingDelete?.kind === "file" && /* @__PURE__ */ (0,
|
|
116435
|
-
/* @__PURE__ */ (0,
|
|
116436
|
-
/* @__PURE__ */ (0,
|
|
116534
|
+
pendingDelete?.kind === "file" && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)("div", { className: "flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3", children: [
|
|
116535
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.AlertTriangle, { className: "size-4 shrink-0 text-yellow-500 mt-0.5" }),
|
|
116536
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)("span", { children: "The file will be deleted from disk. This cannot be undone." })
|
|
116437
116537
|
] }),
|
|
116438
|
-
deleteError && /* @__PURE__ */ (0,
|
|
116439
|
-
/* @__PURE__ */ (0,
|
|
116440
|
-
/* @__PURE__ */ (0,
|
|
116538
|
+
deleteError && /* @__PURE__ */ (0, import_jsx_runtime70.jsxs)("div", { className: "flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-destructive", children: [
|
|
116539
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(import_lucide_react7.AlertTriangle, { className: "size-4 shrink-0 mt-0.5" }),
|
|
116540
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)("span", { children: deleteError })
|
|
116441
116541
|
] })
|
|
116442
116542
|
] }),
|
|
116443
|
-
/* @__PURE__ */ (0,
|
|
116444
|
-
/* @__PURE__ */ (0,
|
|
116445
|
-
/* @__PURE__ */ (0,
|
|
116543
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsxs)(DialogFooter, { children: [
|
|
116544
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(Button, { variant: "ghost", onClick: cancelDelete, disabled: deleteSubmitting, children: "Cancel" }),
|
|
116545
|
+
/* @__PURE__ */ (0, import_jsx_runtime70.jsx)(
|
|
116446
116546
|
Button,
|
|
116447
116547
|
{
|
|
116448
116548
|
variant: "destructive",
|
|
@@ -116463,32 +116563,32 @@ var import_react39 = __toESM(require_react(), 1);
|
|
|
116463
116563
|
|
|
116464
116564
|
// src/dashboard/components/ImagePreview.tsx
|
|
116465
116565
|
var import_react37 = __toESM(require_react(), 1);
|
|
116466
|
-
var
|
|
116566
|
+
var import_jsx_runtime71 = __toESM(require_jsx_runtime(), 1);
|
|
116467
116567
|
|
|
116468
116568
|
// src/dashboard/components/LanguagePickerDialog.tsx
|
|
116469
116569
|
var import_react38 = __toESM(require_react(), 1);
|
|
116470
|
-
var
|
|
116570
|
+
var import_jsx_runtime72 = __toESM(require_jsx_runtime(), 1);
|
|
116471
116571
|
|
|
116472
116572
|
// src/dashboard/components/PdfPreview.tsx
|
|
116473
|
-
var
|
|
116573
|
+
var import_jsx_runtime73 = __toESM(require_jsx_runtime(), 1);
|
|
116474
116574
|
|
|
116475
116575
|
// src/dashboard/components/FileViewer.tsx
|
|
116476
|
-
var
|
|
116576
|
+
var import_jsx_runtime74 = __toESM(require_jsx_runtime(), 1);
|
|
116477
116577
|
|
|
116478
116578
|
// src/dashboard/components/QuickOpenDialog.tsx
|
|
116479
116579
|
var import_react40 = __toESM(require_react(), 1);
|
|
116480
|
-
var
|
|
116580
|
+
var import_jsx_runtime75 = __toESM(require_jsx_runtime(), 1);
|
|
116481
116581
|
|
|
116482
116582
|
// src/dashboard/components/SearchFilesDialog.tsx
|
|
116483
116583
|
var import_react41 = __toESM(require_react(), 1);
|
|
116484
|
-
var
|
|
116584
|
+
var import_jsx_runtime76 = __toESM(require_jsx_runtime(), 1);
|
|
116485
116585
|
|
|
116486
116586
|
// src/dashboard/components/WorkspacePickerDialog.tsx
|
|
116487
116587
|
var import_react42 = __toESM(require_react(), 1);
|
|
116488
|
-
var
|
|
116588
|
+
var import_jsx_runtime77 = __toESM(require_jsx_runtime(), 1);
|
|
116489
116589
|
|
|
116490
116590
|
// src/dashboard/components/WorkspaceTabNav.tsx
|
|
116491
|
-
var
|
|
116591
|
+
var import_jsx_runtime78 = __toESM(require_jsx_runtime(), 1);
|
|
116492
116592
|
|
|
116493
116593
|
// src/dashboard/hooks/use-editor-history.ts
|
|
116494
116594
|
var import_react43 = __toESM(require_react(), 1);
|
|
@@ -120629,7 +120729,7 @@ function mimeTypeFromFilename(filename) {
|
|
|
120629
120729
|
init_src();
|
|
120630
120730
|
init_zod();
|
|
120631
120731
|
import { execFile as execFile6 } from "node:child_process";
|
|
120632
|
-
import { randomUUID as
|
|
120732
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
120633
120733
|
import { existsSync as existsSync9, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3 } from "node:fs";
|
|
120634
120734
|
import { rm } from "node:fs/promises";
|
|
120635
120735
|
import { join as join20 } from "node:path";
|
|
@@ -121545,6 +121645,7 @@ setBrowserLookup((browserId) => browserService.get(browserId));
|
|
|
121545
121645
|
|
|
121546
121646
|
// src/server/services/cronjob-service.ts
|
|
121547
121647
|
init_src();
|
|
121648
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
121548
121649
|
|
|
121549
121650
|
// ../../node_modules/.pnpm/croner@10.0.1/node_modules/croner/dist/croner.js
|
|
121550
121651
|
function T(s6) {
|
|
@@ -122238,10 +122339,12 @@ var CronjobQueries = class _CronjobQueries {
|
|
|
122238
122339
|
cronExpression: job.cronExpression,
|
|
122239
122340
|
scope: job.scope,
|
|
122240
122341
|
workspaceId: job.workspaceId ?? null,
|
|
122342
|
+
via: job.via ?? "chat",
|
|
122241
122343
|
enabled: job.enabled,
|
|
122242
122344
|
createdAt: job.createdAt,
|
|
122243
122345
|
lastRunAt: job.lastRunAt ?? null,
|
|
122244
|
-
lastRunStatus: job.lastRunStatus ?? null
|
|
122346
|
+
lastRunStatus: job.lastRunStatus ?? null,
|
|
122347
|
+
lastTerminalId: job.lastTerminalId ?? null
|
|
122245
122348
|
}).run();
|
|
122246
122349
|
}
|
|
122247
122350
|
});
|
|
@@ -122273,6 +122376,19 @@ var CronjobQueries = class _CronjobQueries {
|
|
|
122273
122376
|
const db2 = getDb();
|
|
122274
122377
|
db2.update(cronjobs).set({ lastRunAt: (/* @__PURE__ */ new Date()).toISOString(), lastRunStatus: status }).where(eq2(cronjobs.id, jobId)).run();
|
|
122275
122378
|
}
|
|
122379
|
+
/**
|
|
122380
|
+
* Record (or clear) the terminal a via="terminal" fire spawned.
|
|
122381
|
+
*
|
|
122382
|
+
* Dedicated targeted UPDATE — same rationale as `updateLastRun`: the fire
|
|
122383
|
+
* path shouldn't load + diff + rewrite the whole file just to stamp one
|
|
122384
|
+
* column. The stored id is the handle the scheduler uses on the next tick to
|
|
122385
|
+
* decide whether the previous run is still in flight (skip) or finished
|
|
122386
|
+
* (spawn a fresh pane). Pass `null` to clear it.
|
|
122387
|
+
*/
|
|
122388
|
+
updateLastTerminal(jobId, terminalId) {
|
|
122389
|
+
const db2 = getDb();
|
|
122390
|
+
db2.update(cronjobs).set({ lastTerminalId: terminalId }).where(eq2(cronjobs.id, jobId)).run();
|
|
122391
|
+
}
|
|
122276
122392
|
/**
|
|
122277
122393
|
* Convert a Drizzle row into the optional-T-shaped `CronjobDefinition`.
|
|
122278
122394
|
*
|
|
@@ -122287,1097 +122403,1242 @@ var CronjobQueries = class _CronjobQueries {
|
|
|
122287
122403
|
cronExpression: row.cronExpression,
|
|
122288
122404
|
scope: row.scope,
|
|
122289
122405
|
workspaceId: row.workspaceId ?? void 0,
|
|
122406
|
+
via: row.via ?? "chat",
|
|
122290
122407
|
enabled: row.enabled,
|
|
122291
122408
|
createdAt: row.createdAt,
|
|
122292
122409
|
lastRunAt: row.lastRunAt ?? void 0,
|
|
122293
|
-
lastRunStatus: row.lastRunStatus
|
|
122410
|
+
lastRunStatus: row.lastRunStatus,
|
|
122411
|
+
lastTerminalId: row.lastTerminalId ?? void 0
|
|
122294
122412
|
};
|
|
122295
122413
|
}
|
|
122296
122414
|
};
|
|
122297
122415
|
|
|
122298
|
-
// src/server/services/
|
|
122299
|
-
|
|
122300
|
-
|
|
122301
|
-
|
|
122302
|
-
|
|
122303
|
-
|
|
122304
|
-
|
|
122305
|
-
|
|
122306
|
-
|
|
122307
|
-
}
|
|
122308
|
-
|
|
122309
|
-
|
|
122310
|
-
|
|
122311
|
-
|
|
122312
|
-
|
|
122313
|
-
|
|
122314
|
-
|
|
122315
|
-
|
|
122316
|
-
|
|
122317
|
-
workspaceId: external_exports.string().optional(),
|
|
122318
|
-
enabled: external_exports.boolean().default(true)
|
|
122319
|
-
});
|
|
122320
|
-
var cronjobUpdateInput = external_exports.object({
|
|
122321
|
-
// `.min(1)` rejects the obviously invalid empty-string `key`/`id` at the
|
|
122322
|
-
// validator instead of letting it reach the service, which would
|
|
122323
|
-
// `loadFile("")` an empty result and 404 on `findIndex`. The legacy
|
|
122324
|
-
// router accepted bare `z.string()` here; the migration is the natural
|
|
122325
|
-
// moment to tighten the boundary so the create + update shapes stay
|
|
122326
|
-
// consistent (`cronjobCreateInput.key` already has `.min(1)`).
|
|
122327
|
-
key: external_exports.string().min(1),
|
|
122328
|
-
id: external_exports.string().min(1),
|
|
122329
|
-
name: external_exports.string().min(1).optional(),
|
|
122330
|
-
prompt: external_exports.string().min(1).optional(),
|
|
122331
|
-
cronExpression: external_exports.string().min(1).optional(),
|
|
122332
|
-
enabled: external_exports.boolean().optional()
|
|
122333
|
-
});
|
|
122334
|
-
var cronjobByIdInput = external_exports.object({
|
|
122335
|
-
key: external_exports.string().min(1),
|
|
122336
|
-
id: external_exports.string().min(1)
|
|
122337
|
-
});
|
|
122338
|
-
var CronjobNotFoundError = class extends Error {
|
|
122339
|
-
constructor(message = "Cronjob not found") {
|
|
122340
|
-
super(message);
|
|
122341
|
-
this.name = "CronjobNotFoundError";
|
|
122342
|
-
}
|
|
122343
|
-
};
|
|
122344
|
-
var InvalidCronExpressionError = class extends Error {
|
|
122345
|
-
constructor(message = "Invalid cron expression") {
|
|
122346
|
-
super(message);
|
|
122347
|
-
this.name = "InvalidCronExpressionError";
|
|
122348
|
-
}
|
|
122349
|
-
};
|
|
122350
|
-
var CronjobWorkspaceMissingError = class extends Error {
|
|
122351
|
-
constructor(message = "workspaceId is required for workspace-scoped cronjobs") {
|
|
122352
|
-
super(message);
|
|
122353
|
-
this.name = "CronjobWorkspaceMissingError";
|
|
122354
|
-
}
|
|
122355
|
-
};
|
|
122356
|
-
var CronjobProjectNotFoundError = class extends Error {
|
|
122357
|
-
constructor(message = "Project not found") {
|
|
122358
|
-
super(message);
|
|
122359
|
-
this.name = "CronjobProjectNotFoundError";
|
|
122360
|
-
}
|
|
122361
|
-
};
|
|
122362
|
-
var CronjobService = class {
|
|
122363
|
-
constructor(queries = new CronjobQueries()) {
|
|
122364
|
-
this.queries = queries;
|
|
122365
|
-
}
|
|
122366
|
-
// -------------------------------------------------------------------------
|
|
122367
|
-
// Read operations
|
|
122368
|
-
// -------------------------------------------------------------------------
|
|
122416
|
+
// src/server/services/terminal-service.ts
|
|
122417
|
+
init_src();
|
|
122418
|
+
init_zod();
|
|
122419
|
+
|
|
122420
|
+
// src/server/infra/terminals/terminal-pool.ts
|
|
122421
|
+
init_src();
|
|
122422
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
122423
|
+
import { existsSync as existsSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
122424
|
+
import { tmpdir } from "node:os";
|
|
122425
|
+
import { join as join19, sep as sep3 } from "node:path";
|
|
122426
|
+
var log20 = createLogger("terminal-pool");
|
|
122427
|
+
var MAX_SCROLLBACK_SIZE = 1e5;
|
|
122428
|
+
var TerminalPool = class {
|
|
122429
|
+
/** terminalId -> session */
|
|
122430
|
+
terminals = /* @__PURE__ */ new Map();
|
|
122431
|
+
/** workspaceId -> Set<terminalId> (reverse index for workspace-level cleanup) */
|
|
122432
|
+
workspaceTerminals = /* @__PURE__ */ new Map();
|
|
122433
|
+
/** terminalId -> Set<listener> for live output streaming */
|
|
122434
|
+
outputListeners = /* @__PURE__ */ new Map();
|
|
122369
122435
|
/**
|
|
122370
|
-
*
|
|
122371
|
-
*
|
|
122436
|
+
* terminalId -> in-flight spawn promise. A single terminal is created via TWO
|
|
122437
|
+
* concurrent paths — the WebSocket handler (spawn-on-`getSession`-miss) and
|
|
122438
|
+
* the tRPC `terminal.create` mutation. Without deduplication both spawn a PTY
|
|
122439
|
+
* and the second `terminals.set` overwrites the first, so the client stays
|
|
122440
|
+
* wired to one PTY while the server's session map (and thus scrollback /
|
|
122441
|
+
* `terminals output` / reconnect-replay) points at the other. That's the
|
|
122442
|
+
* "output on screen but empty server scrollback until reload" bug
|
|
122443
|
+
* (band-app/band#617). This map makes concurrent spawns share ONE PTY.
|
|
122444
|
+
*/
|
|
122445
|
+
spawning = /* @__PURE__ */ new Map();
|
|
122446
|
+
/**
|
|
122447
|
+
* Spawn (or return the already-spawning/spawned) PTY for a terminalId.
|
|
122372
122448
|
*
|
|
122373
|
-
*
|
|
122374
|
-
*
|
|
122375
|
-
*
|
|
122376
|
-
*
|
|
122449
|
+
* Idempotent per terminalId: if a session already exists it is returned
|
|
122450
|
+
* as-is, and concurrent spawn calls for the same id share a single in-flight
|
|
122451
|
+
* promise — so the WebSocket and tRPC create paths never create competing
|
|
122452
|
+
* PTYs. `workspaceRoot` is the absolute worktree path; resolving the
|
|
122453
|
+
* workspaceId to a path is the service tier's job so the pool stays oblivious
|
|
122454
|
+
* to the workspace registry.
|
|
122377
122455
|
*/
|
|
122378
|
-
|
|
122379
|
-
|
|
122380
|
-
|
|
122381
|
-
|
|
122382
|
-
|
|
122383
|
-
|
|
122384
|
-
|
|
122385
|
-
|
|
122386
|
-
|
|
122387
|
-
|
|
122388
|
-
|
|
122389
|
-
};
|
|
122456
|
+
async spawn(workspaceId, terminalId, workspaceRoot, options2, onExit) {
|
|
122457
|
+
const existing = this.terminals.get(terminalId);
|
|
122458
|
+
if (existing) return existing;
|
|
122459
|
+
const inflight = this.spawning.get(terminalId);
|
|
122460
|
+
if (inflight) return inflight;
|
|
122461
|
+
const promise = this.spawnNew(workspaceId, terminalId, workspaceRoot, options2, onExit);
|
|
122462
|
+
this.spawning.set(terminalId, promise);
|
|
122463
|
+
try {
|
|
122464
|
+
return await promise;
|
|
122465
|
+
} finally {
|
|
122466
|
+
this.spawning.delete(terminalId);
|
|
122390
122467
|
}
|
|
122391
|
-
return { jobs: this.queries.listAll() };
|
|
122392
122468
|
}
|
|
122393
|
-
|
|
122394
|
-
|
|
122395
|
-
const
|
|
122396
|
-
|
|
122397
|
-
|
|
122469
|
+
async spawnNew(workspaceId, terminalId, workspaceRoot, options2, onExit) {
|
|
122470
|
+
const shell = defaultShell();
|
|
122471
|
+
const resolvedPath = await shellPath();
|
|
122472
|
+
const env = {};
|
|
122473
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
122474
|
+
if (value != null) {
|
|
122475
|
+
env[key] = value;
|
|
122476
|
+
}
|
|
122477
|
+
}
|
|
122478
|
+
env.PATH = resolvedPath;
|
|
122479
|
+
env.TERM = "xterm-256color";
|
|
122480
|
+
env.COLORTERM = "truecolor";
|
|
122481
|
+
env.COLORFGBG = "15;0";
|
|
122482
|
+
delete env.PORT;
|
|
122483
|
+
if (options2?.env) {
|
|
122484
|
+
Object.assign(env, options2.env);
|
|
122485
|
+
}
|
|
122486
|
+
env.BAND_DISPATCH = "terminal";
|
|
122487
|
+
let cwd = workspaceRoot;
|
|
122488
|
+
if (options2?.cwd) {
|
|
122489
|
+
const resolved = join19(workspaceRoot, options2.cwd);
|
|
122490
|
+
if (resolved !== workspaceRoot && !resolved.startsWith(workspaceRoot + sep3)) {
|
|
122491
|
+
log20.warn(
|
|
122492
|
+
"Ignoring cwd %s \u2014 resolves outside workspace root %s",
|
|
122493
|
+
options2.cwd,
|
|
122494
|
+
workspaceRoot
|
|
122495
|
+
);
|
|
122496
|
+
} else if (existsSync8(resolved)) {
|
|
122497
|
+
cwd = resolved;
|
|
122498
|
+
} else {
|
|
122499
|
+
log20.warn("Ignoring cwd %s \u2014 directory does not exist", options2.cwd);
|
|
122500
|
+
}
|
|
122501
|
+
}
|
|
122502
|
+
if (!existsSync8(cwd)) {
|
|
122503
|
+
throw new Error(`Workspace directory does not exist: ${cwd}`);
|
|
122504
|
+
}
|
|
122505
|
+
if (!existsSync8(shell)) {
|
|
122506
|
+
throw new Error(`Shell not found: ${shell}`);
|
|
122507
|
+
}
|
|
122508
|
+
log20.debug(
|
|
122509
|
+
"Spawning shell %s in %s for terminal %s (PATH=%s)",
|
|
122510
|
+
shell,
|
|
122511
|
+
cwd,
|
|
122512
|
+
terminalId,
|
|
122513
|
+
resolvedPath.slice(0, 200)
|
|
122514
|
+
);
|
|
122515
|
+
const nodePty = await import("node-pty");
|
|
122516
|
+
let ptyProcess;
|
|
122517
|
+
try {
|
|
122518
|
+
ptyProcess = nodePty.spawn(shell, [], {
|
|
122519
|
+
name: "xterm-256color",
|
|
122520
|
+
cols: 80,
|
|
122521
|
+
rows: 24,
|
|
122522
|
+
cwd,
|
|
122523
|
+
env
|
|
122524
|
+
});
|
|
122525
|
+
} catch (err) {
|
|
122526
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
122527
|
+
log20.error("pty.spawn failed: %s (shell=%s, cwd=%s)", msg, shell, cwd);
|
|
122528
|
+
throw err;
|
|
122529
|
+
}
|
|
122530
|
+
const session = { pty: ptyProcess, scrollback: "", workspaceId };
|
|
122531
|
+
this.terminals.set(terminalId, session);
|
|
122532
|
+
if (options2?.command) {
|
|
122533
|
+
this.autoRunCommand(session, options2.command);
|
|
122534
|
+
}
|
|
122535
|
+
let ids = this.workspaceTerminals.get(workspaceId);
|
|
122536
|
+
if (!ids) {
|
|
122537
|
+
ids = /* @__PURE__ */ new Set();
|
|
122538
|
+
this.workspaceTerminals.set(workspaceId, ids);
|
|
122539
|
+
}
|
|
122540
|
+
ids.add(terminalId);
|
|
122541
|
+
ptyProcess.onData((data) => {
|
|
122542
|
+
session.scrollback += data;
|
|
122543
|
+
if (session.scrollback.length > MAX_SCROLLBACK_SIZE) {
|
|
122544
|
+
session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK_SIZE);
|
|
122545
|
+
}
|
|
122546
|
+
const listeners3 = this.outputListeners.get(terminalId);
|
|
122547
|
+
if (listeners3) {
|
|
122548
|
+
for (const cb2 of listeners3) {
|
|
122549
|
+
try {
|
|
122550
|
+
cb2(data);
|
|
122551
|
+
} catch {
|
|
122552
|
+
}
|
|
122553
|
+
}
|
|
122554
|
+
}
|
|
122555
|
+
});
|
|
122556
|
+
ptyProcess.onExit(() => {
|
|
122557
|
+
log20.debug("Terminal exited: %s (workspace %s)", terminalId, workspaceId);
|
|
122558
|
+
const explicitlyKilled = !this.terminals.has(terminalId);
|
|
122559
|
+
this.terminals.delete(terminalId);
|
|
122560
|
+
this.outputListeners.delete(terminalId);
|
|
122561
|
+
const set = this.workspaceTerminals.get(workspaceId);
|
|
122562
|
+
if (set) {
|
|
122563
|
+
set.delete(terminalId);
|
|
122564
|
+
if (set.size === 0) {
|
|
122565
|
+
this.workspaceTerminals.delete(workspaceId);
|
|
122566
|
+
}
|
|
122567
|
+
}
|
|
122568
|
+
if (session.autoRunFile) {
|
|
122569
|
+
try {
|
|
122570
|
+
unlinkSync2(session.autoRunFile);
|
|
122571
|
+
} catch {
|
|
122572
|
+
}
|
|
122573
|
+
}
|
|
122574
|
+
if (onExit && !explicitlyKilled) {
|
|
122575
|
+
try {
|
|
122576
|
+
onExit();
|
|
122577
|
+
} catch (err) {
|
|
122578
|
+
log20.warn("onExit callback threw for terminal %s: %s", terminalId, err);
|
|
122579
|
+
}
|
|
122580
|
+
}
|
|
122581
|
+
});
|
|
122582
|
+
return session;
|
|
122398
122583
|
}
|
|
122399
|
-
// -------------------------------------------------------------------------
|
|
122400
|
-
// Mutations
|
|
122401
|
-
// -------------------------------------------------------------------------
|
|
122402
122584
|
/**
|
|
122403
|
-
*
|
|
122585
|
+
* Auto-run the workspace's initial command inside a freshly spawned PTY,
|
|
122586
|
+
* robustly.
|
|
122404
122587
|
*
|
|
122405
|
-
*
|
|
122406
|
-
*
|
|
122407
|
-
*
|
|
122408
|
-
*
|
|
122409
|
-
* its
|
|
122588
|
+
* Writing a long command line straight into a cold PTY is racy on three
|
|
122589
|
+
* fronts, all of which surfaced in production with `via=terminal`
|
|
122590
|
+
* prompt-as-argv launches (`'<agent-binary>' '<entire-prompt>'`):
|
|
122591
|
+
*
|
|
122592
|
+
* 1. **Dropped newline.** The shell may not have finished sourcing its
|
|
122593
|
+
* rc files, and the tty can still be in its startup canonical-mode
|
|
122594
|
+
* window. A trailing `\n` written then is swallowed — the command
|
|
122595
|
+
* is typed at the prompt but never executed.
|
|
122596
|
+
* 2. **Truncation.** A single input line longer than the tty's
|
|
122597
|
+
* canonical buffer (`MAX_CANON`, ~4 KB) is clipped. Long prompts
|
|
122598
|
+
* blow past that easily.
|
|
122599
|
+
* 3. **UTF-8 corruption.** A multi-byte sequence split across a tty
|
|
122600
|
+
* buffer boundary is mangled (the classic em-dash → mojibake).
|
|
122601
|
+
*
|
|
122602
|
+
* Fix both layers: (1) stage the command's bytes in a temp file so they
|
|
122603
|
+
* never traverse the tty's canonical input buffer — sidestepping the
|
|
122604
|
+
* truncation and UTF-8-split failures entirely — and (2) wait for the
|
|
122605
|
+
* shell to emit its first output (its prompt) before writing the
|
|
122606
|
+
* *short* `source <file>` line. The short line is well under the
|
|
122607
|
+
* canonical limit and lands after the cold-start window, so its newline
|
|
122608
|
+
* survives. `source` runs the command in the current interactive shell,
|
|
122609
|
+
* so the user keeps their session afterwards.
|
|
122410
122610
|
*/
|
|
122411
|
-
|
|
122412
|
-
|
|
122413
|
-
|
|
122414
|
-
|
|
122611
|
+
autoRunCommand(session, command) {
|
|
122612
|
+
const pty = session.pty;
|
|
122613
|
+
let cmdFile;
|
|
122614
|
+
try {
|
|
122615
|
+
cmdFile = join19(tmpdir(), `band-autorun-${randomUUID2()}.sh`);
|
|
122616
|
+
writeFileSync2(cmdFile, `${command}
|
|
122617
|
+
`, { encoding: "utf-8", mode: 384, flag: "wx" });
|
|
122618
|
+
session.autoRunFile = cmdFile;
|
|
122619
|
+
} catch (err) {
|
|
122620
|
+
log20.warn(
|
|
122621
|
+
"autoRunCommand: temp-file staging failed (%s); writing command directly",
|
|
122622
|
+
err instanceof Error ? err.message : String(err)
|
|
122623
|
+
);
|
|
122624
|
+
pty.write(`${command}
|
|
122625
|
+
`);
|
|
122626
|
+
return;
|
|
122415
122627
|
}
|
|
122416
|
-
const
|
|
122417
|
-
const
|
|
122418
|
-
|
|
122419
|
-
|
|
122420
|
-
|
|
122421
|
-
|
|
122422
|
-
|
|
122423
|
-
|
|
122424
|
-
|
|
122425
|
-
|
|
122628
|
+
const quotedPath = cmdFile.replace(/'/g, `'\\''`);
|
|
122629
|
+
const sourceLine = `. '${quotedPath}'
|
|
122630
|
+
`;
|
|
122631
|
+
let done = false;
|
|
122632
|
+
const inject = () => {
|
|
122633
|
+
if (done) return;
|
|
122634
|
+
done = true;
|
|
122635
|
+
try {
|
|
122636
|
+
pty.write(sourceLine);
|
|
122637
|
+
} catch (err) {
|
|
122638
|
+
log20.warn("autoRunCommand: failed to inject command line (%s)", err);
|
|
122639
|
+
}
|
|
122426
122640
|
};
|
|
122427
|
-
|
|
122428
|
-
|
|
122429
|
-
|
|
122430
|
-
|
|
122641
|
+
let dataDisposed = false;
|
|
122642
|
+
const disposeData = () => {
|
|
122643
|
+
if (dataDisposed) return;
|
|
122644
|
+
dataDisposed = true;
|
|
122645
|
+
dataDisposable.dispose();
|
|
122646
|
+
};
|
|
122647
|
+
const dataDisposable = pty.onData(() => {
|
|
122648
|
+
disposeData();
|
|
122649
|
+
inject();
|
|
122650
|
+
});
|
|
122651
|
+
const timer = setTimeout(inject, 750);
|
|
122652
|
+
timer.unref();
|
|
122653
|
+
let exitDisposed = false;
|
|
122654
|
+
const exitDisposable = pty.onExit(() => {
|
|
122655
|
+
done = true;
|
|
122656
|
+
clearTimeout(timer);
|
|
122657
|
+
disposeData();
|
|
122658
|
+
if (exitDisposed) return;
|
|
122659
|
+
exitDisposed = true;
|
|
122660
|
+
exitDisposable.dispose();
|
|
122661
|
+
});
|
|
122431
122662
|
}
|
|
122432
122663
|
/**
|
|
122433
|
-
*
|
|
122434
|
-
*
|
|
122435
|
-
* Each optional field is applied only when present, matching the legacy
|
|
122436
|
-
* router behavior. If `cronExpression` is supplied it's validated before
|
|
122437
|
-
* the write. Throws `CronjobNotFoundError` if no job matches `key`+`id`.
|
|
122664
|
+
* Returns an existing PTY session by terminalId, or undefined.
|
|
122438
122665
|
*/
|
|
122439
|
-
|
|
122440
|
-
|
|
122441
|
-
this.assertValidCron(input.cronExpression);
|
|
122442
|
-
}
|
|
122443
|
-
const file = this.queries.loadFile(input.key);
|
|
122444
|
-
const job = file.jobs.find((j) => j.id === input.id);
|
|
122445
|
-
if (!job) throw new CronjobNotFoundError();
|
|
122446
|
-
if (input.name !== void 0) job.name = input.name;
|
|
122447
|
-
if (input.prompt !== void 0) job.prompt = input.prompt;
|
|
122448
|
-
if (input.cronExpression !== void 0) job.cronExpression = input.cronExpression;
|
|
122449
|
-
if (input.enabled !== void 0) job.enabled = input.enabled;
|
|
122450
|
-
this.queries.saveFile(input.key, file);
|
|
122451
|
-
this.reloadSchedules();
|
|
122452
|
-
return { job };
|
|
122666
|
+
get(terminalId) {
|
|
122667
|
+
return this.terminals.get(terminalId);
|
|
122453
122668
|
}
|
|
122454
122669
|
/**
|
|
122455
|
-
*
|
|
122456
|
-
*
|
|
122457
|
-
* Throws `CronjobNotFoundError` if the job is already gone.
|
|
122670
|
+
* Resize the underlying PTY. No-op if the terminal isn't known to the pool.
|
|
122458
122671
|
*/
|
|
122459
|
-
|
|
122460
|
-
const
|
|
122461
|
-
|
|
122462
|
-
|
|
122463
|
-
|
|
122464
|
-
this.queries.saveFile(key, file);
|
|
122465
|
-
this.reloadSchedules();
|
|
122466
|
-
return { ok: true };
|
|
122672
|
+
resize(terminalId, cols, rows) {
|
|
122673
|
+
const session = this.terminals.get(terminalId);
|
|
122674
|
+
if (session) {
|
|
122675
|
+
session.pty.resize(cols, rows);
|
|
122676
|
+
}
|
|
122467
122677
|
}
|
|
122468
122678
|
/**
|
|
122469
|
-
*
|
|
122470
|
-
*
|
|
122471
|
-
* Uses the same `band:cronId`-labeled chat as the scheduled fire so the
|
|
122472
|
-
* user sees both paths converge on a single dedicated conversation
|
|
122473
|
-
* (issue #520). `submitTask` may throw `TaskConflictError`, which the API
|
|
122474
|
-
* tier translates into a 409 Conflict response.
|
|
122679
|
+
* List metadata for every PTY session bound to the given workspace.
|
|
122475
122680
|
*
|
|
122476
|
-
*
|
|
122477
|
-
*
|
|
122478
|
-
*
|
|
122479
|
-
* freshly-created chat remains in the workspace as an orphan with no
|
|
122480
|
-
* task ever attached. The next manual `trigger` (or scheduled fire)
|
|
122481
|
-
* reuses that chat via its `band:cronId` label, so the orphan
|
|
122482
|
-
* self-heals — but the user may briefly see an empty chat pane. The
|
|
122483
|
-
* legacy implementation had the same gap; preserved here to match
|
|
122484
|
-
* existing behavior. If this ever needs to be tightened, the fix is
|
|
122485
|
-
* either (a) wrap both calls in a try/catch that deletes the chat on
|
|
122486
|
-
* unexpected failure, or (b) defer `createChat` until `submitTask`
|
|
122487
|
-
* succeeds (which would require buffering the chat name/labels on the
|
|
122488
|
-
* task row).
|
|
122681
|
+
* The returned `title` is the foreground process name reported by the
|
|
122682
|
+
* PTY; reading `pty.process` can throw if the process has already exited
|
|
122683
|
+
* between the reverse-index lookup and the read, so the read is wrapped.
|
|
122489
122684
|
*/
|
|
122490
|
-
|
|
122491
|
-
const
|
|
122492
|
-
if (!
|
|
122493
|
-
const
|
|
122494
|
-
|
|
122495
|
-
|
|
122496
|
-
|
|
122497
|
-
|
|
122498
|
-
|
|
122499
|
-
|
|
122500
|
-
|
|
122501
|
-
|
|
122502
|
-
|
|
122503
|
-
|
|
122504
|
-
|
|
122505
|
-
|
|
122506
|
-
|
|
122507
|
-
|
|
122508
|
-
|
|
122509
|
-
|
|
122510
|
-
|
|
122511
|
-
|
|
122512
|
-
|
|
122513
|
-
|
|
122514
|
-
*
|
|
122515
|
-
*
|
|
122516
|
-
* lock), the timers are already stopped but the rows remain in the DB.
|
|
122517
|
-
* The next `reloadSchedules()` call — triggered by any subsequent
|
|
122518
|
-
* cronjob mutation, or by a server restart — re-reads the rows from the
|
|
122519
|
-
* DB and re-registers the timers, so the orphaned rows self-heal
|
|
122520
|
-
* without manual intervention. We do NOT roll back the `stop()` calls
|
|
122521
|
-
* because re-binding timers for a workspace that's mid-deletion is
|
|
122522
|
-
* worse than leaving them stopped: the workspace path is about to be
|
|
122523
|
-
* removed and any fire that races the removal would target a
|
|
122524
|
-
* non-existent worktree. Re-registration on the next reload is the
|
|
122525
|
-
* intentional recovery strategy; operators encountering a partial
|
|
122526
|
-
* failure on this path should expect the next reload to be a no-op
|
|
122527
|
-
* once the workspace removal completes and the rows are re-attempted.
|
|
122685
|
+
list(workspaceId) {
|
|
122686
|
+
const ids = this.workspaceTerminals.get(workspaceId);
|
|
122687
|
+
if (!ids) return [];
|
|
122688
|
+
const result = [];
|
|
122689
|
+
for (const terminalId of ids) {
|
|
122690
|
+
const session = this.terminals.get(terminalId);
|
|
122691
|
+
if (session) {
|
|
122692
|
+
let title = "";
|
|
122693
|
+
try {
|
|
122694
|
+
title = session.pty.process;
|
|
122695
|
+
} catch {
|
|
122696
|
+
}
|
|
122697
|
+
result.push({
|
|
122698
|
+
terminalId,
|
|
122699
|
+
workspaceId,
|
|
122700
|
+
pid: session.pty.pid,
|
|
122701
|
+
scrollbackLength: session.scrollback.length,
|
|
122702
|
+
title
|
|
122703
|
+
});
|
|
122704
|
+
}
|
|
122705
|
+
}
|
|
122706
|
+
return result;
|
|
122707
|
+
}
|
|
122708
|
+
/**
|
|
122709
|
+
* Returns the scrollback buffer for a terminal, optionally limited to the
|
|
122710
|
+
* last N lines. Returns `null` if the terminal is not found.
|
|
122528
122711
|
*/
|
|
122529
|
-
|
|
122530
|
-
this.
|
|
122531
|
-
|
|
122712
|
+
getScrollback(terminalId, lines) {
|
|
122713
|
+
const session = this.terminals.get(terminalId);
|
|
122714
|
+
if (!session) return null;
|
|
122715
|
+
if (lines == null) return session.scrollback;
|
|
122716
|
+
const allLines = session.scrollback.split("\n");
|
|
122717
|
+
return allLines.slice(-lines).join("\n");
|
|
122532
122718
|
}
|
|
122533
|
-
|
|
122534
|
-
|
|
122535
|
-
|
|
122536
|
-
|
|
122537
|
-
|
|
122538
|
-
const
|
|
122539
|
-
if (
|
|
122540
|
-
|
|
122541
|
-
|
|
122542
|
-
log20.info("cronjob scheduler started");
|
|
122719
|
+
/**
|
|
122720
|
+
* Writes data to a terminal's PTY stdin.
|
|
122721
|
+
* Returns false if the terminal is not found.
|
|
122722
|
+
*/
|
|
122723
|
+
write(terminalId, data) {
|
|
122724
|
+
const session = this.terminals.get(terminalId);
|
|
122725
|
+
if (!session) return false;
|
|
122726
|
+
session.pty.write(data);
|
|
122727
|
+
return true;
|
|
122543
122728
|
}
|
|
122544
|
-
/**
|
|
122545
|
-
|
|
122546
|
-
|
|
122547
|
-
|
|
122548
|
-
|
|
122729
|
+
/**
|
|
122730
|
+
* Subscribe to live output from a terminal's PTY.
|
|
122731
|
+
* Returns an unsubscribe function.
|
|
122732
|
+
*/
|
|
122733
|
+
subscribeOutput(terminalId, callback) {
|
|
122734
|
+
let listeners3 = this.outputListeners.get(terminalId);
|
|
122735
|
+
if (!listeners3) {
|
|
122736
|
+
listeners3 = /* @__PURE__ */ new Set();
|
|
122737
|
+
this.outputListeners.set(terminalId, listeners3);
|
|
122549
122738
|
}
|
|
122550
|
-
|
|
122551
|
-
|
|
122552
|
-
|
|
122739
|
+
listeners3.add(callback);
|
|
122740
|
+
return () => {
|
|
122741
|
+
listeners3.delete(callback);
|
|
122742
|
+
if (listeners3.size === 0) {
|
|
122743
|
+
this.outputListeners.delete(terminalId);
|
|
122744
|
+
}
|
|
122745
|
+
};
|
|
122553
122746
|
}
|
|
122554
|
-
/**
|
|
122555
|
-
|
|
122556
|
-
|
|
122557
|
-
|
|
122747
|
+
/**
|
|
122748
|
+
* Kill a single terminal by terminalId.
|
|
122749
|
+
*/
|
|
122750
|
+
kill(terminalId) {
|
|
122751
|
+
const session = this.terminals.get(terminalId);
|
|
122752
|
+
if (session) {
|
|
122753
|
+
session.pty.kill();
|
|
122754
|
+
this.terminals.delete(terminalId);
|
|
122755
|
+
const set = this.workspaceTerminals.get(session.workspaceId);
|
|
122756
|
+
if (set) {
|
|
122757
|
+
set.delete(terminalId);
|
|
122758
|
+
if (set.size === 0) {
|
|
122759
|
+
this.workspaceTerminals.delete(session.workspaceId);
|
|
122760
|
+
}
|
|
122761
|
+
}
|
|
122762
|
+
}
|
|
122558
122763
|
}
|
|
122559
122764
|
/**
|
|
122560
|
-
*
|
|
122561
|
-
*
|
|
122562
|
-
* Private because the only correct call site is `removeForKey` above:
|
|
122563
|
-
* stopping the timers without also dropping the rows would re-schedule
|
|
122564
|
-
* the same jobs on the next `reloadSchedules()` tick. Keeping it
|
|
122565
|
-
* accessible only through `removeForKey` prevents future callers from
|
|
122566
|
-
* accidentally invoking just one half of the pair.
|
|
122765
|
+
* Kill all terminals for a workspace.
|
|
122567
122766
|
*/
|
|
122568
|
-
|
|
122569
|
-
const
|
|
122570
|
-
|
|
122571
|
-
for (const
|
|
122572
|
-
const
|
|
122573
|
-
if (
|
|
122574
|
-
|
|
122575
|
-
|
|
122576
|
-
log20.info({ jobId: job.id, key }, "stopped cronjob");
|
|
122767
|
+
killWorkspace(workspaceId) {
|
|
122768
|
+
const ids = this.workspaceTerminals.get(workspaceId);
|
|
122769
|
+
if (!ids) return;
|
|
122770
|
+
for (const terminalId of ids) {
|
|
122771
|
+
const session = this.terminals.get(terminalId);
|
|
122772
|
+
if (session) {
|
|
122773
|
+
session.pty.kill();
|
|
122774
|
+
this.terminals.delete(terminalId);
|
|
122577
122775
|
}
|
|
122578
122776
|
}
|
|
122777
|
+
this.workspaceTerminals.delete(workspaceId);
|
|
122778
|
+
}
|
|
122779
|
+
/**
|
|
122780
|
+
* Kill every tracked PTY (server shutdown path).
|
|
122781
|
+
*/
|
|
122782
|
+
killAll() {
|
|
122783
|
+
for (const [, session] of this.terminals) {
|
|
122784
|
+
session.pty.kill();
|
|
122785
|
+
}
|
|
122786
|
+
this.terminals.clear();
|
|
122787
|
+
this.workspaceTerminals.clear();
|
|
122788
|
+
}
|
|
122789
|
+
};
|
|
122790
|
+
var terminalPool = new TerminalPool();
|
|
122791
|
+
|
|
122792
|
+
// src/server/services/_utils/terminal-layout-manager.ts
|
|
122793
|
+
var manager = new DockviewLayoutManager("terminal_layout");
|
|
122794
|
+
var getTerminalLayout = (workspaceId) => manager.get(workspaceId);
|
|
122795
|
+
var saveTerminalLayout = (workspaceId, tree) => manager.save(workspaceId, tree);
|
|
122796
|
+
var deleteTerminalLayout = (workspaceId) => manager.delete(workspaceId);
|
|
122797
|
+
function addTerminalToLayout(workspaceId, terminalId, opts) {
|
|
122798
|
+
manager.addPanel(workspaceId, {
|
|
122799
|
+
id: terminalId,
|
|
122800
|
+
contentComponent: "terminalTab",
|
|
122801
|
+
tabComponent: "terminalTab",
|
|
122802
|
+
title: opts?.title ?? "Terminal",
|
|
122803
|
+
params: {
|
|
122804
|
+
workspaceId,
|
|
122805
|
+
terminalId,
|
|
122806
|
+
...opts?.command ? { command: opts.command } : {},
|
|
122807
|
+
...opts?.cwd ? { cwd: opts.cwd } : {},
|
|
122808
|
+
...opts?.env ? { env: opts.env } : {}
|
|
122809
|
+
}
|
|
122810
|
+
});
|
|
122811
|
+
}
|
|
122812
|
+
function removeTerminalFromLayout(workspaceId, terminalId) {
|
|
122813
|
+
manager.removePanel(workspaceId, terminalId);
|
|
122814
|
+
}
|
|
122815
|
+
|
|
122816
|
+
// src/server/services/terminal-service.ts
|
|
122817
|
+
var log21 = createLogger("terminal-service");
|
|
122818
|
+
var TerminalPaneConfigSchema = external_exports.object({
|
|
122819
|
+
name: external_exports.string().optional(),
|
|
122820
|
+
command: external_exports.string().optional(),
|
|
122821
|
+
cwd: external_exports.string().optional(),
|
|
122822
|
+
env: external_exports.record(external_exports.string()).optional(),
|
|
122823
|
+
focus: external_exports.boolean().optional()
|
|
122824
|
+
});
|
|
122825
|
+
var PaneNodeSchema = external_exports.object({
|
|
122826
|
+
pane: TerminalPaneConfigSchema
|
|
122827
|
+
});
|
|
122828
|
+
var TerminalLayoutNodeSchema = external_exports.lazy(
|
|
122829
|
+
() => external_exports.union([
|
|
122830
|
+
PaneNodeSchema,
|
|
122831
|
+
external_exports.object({
|
|
122832
|
+
direction: external_exports.enum(["horizontal", "vertical"]),
|
|
122833
|
+
split: external_exports.number().min(0.1).max(0.9).optional().default(0.5),
|
|
122834
|
+
children: external_exports.tuple([TerminalLayoutNodeSchema, TerminalLayoutNodeSchema])
|
|
122835
|
+
})
|
|
122836
|
+
])
|
|
122837
|
+
);
|
|
122838
|
+
var WorkspaceTerminalConfigSchema = external_exports.object({
|
|
122839
|
+
layout: TerminalLayoutNodeSchema
|
|
122840
|
+
});
|
|
122841
|
+
var TerminalService = class {
|
|
122842
|
+
constructor(pool2 = terminalPool) {
|
|
122843
|
+
this.pool = pool2;
|
|
122579
122844
|
}
|
|
122580
122845
|
// -------------------------------------------------------------------------
|
|
122581
|
-
//
|
|
122846
|
+
// PTY lifecycle
|
|
122582
122847
|
// -------------------------------------------------------------------------
|
|
122583
122848
|
/**
|
|
122584
|
-
*
|
|
122849
|
+
* Spawn a new PTY for the given workspace + terminalId.
|
|
122585
122850
|
*
|
|
122586
|
-
*
|
|
122587
|
-
*
|
|
122588
|
-
*
|
|
122589
|
-
*
|
|
122590
|
-
*
|
|
122591
|
-
*
|
|
122592
|
-
* fire path (`executeCronjob` below) and the manual `trigger` route so
|
|
122593
|
-
* both stay aligned.
|
|
122851
|
+
* Resolves `workspaceId` to a worktree path before delegating to the
|
|
122852
|
+
* pool, and registers the new terminal in the saved dockview layout so it
|
|
122853
|
+
* survives a server restart (mirrors `chatService.create` /
|
|
122854
|
+
* `browserService.create`). Does NOT emit a `terminal-created` event —
|
|
122855
|
+
* the API entry points decide whether to broadcast (the WebSocket handler
|
|
122856
|
+
* stays silent; the tRPC `create` mutation emits explicitly).
|
|
122594
122857
|
*/
|
|
122595
|
-
|
|
122596
|
-
const
|
|
122597
|
-
|
|
122598
|
-
|
|
122599
|
-
|
|
122600
|
-
|
|
122601
|
-
|
|
122602
|
-
|
|
122603
|
-
|
|
122604
|
-
|
|
122605
|
-
|
|
122858
|
+
async spawn(workspaceId, terminalId, options2, opts) {
|
|
122859
|
+
const workspace = workspaceService.resolve(workspaceId);
|
|
122860
|
+
if (!workspace) {
|
|
122861
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
122862
|
+
}
|
|
122863
|
+
const session = await this.pool.spawn(
|
|
122864
|
+
workspaceId,
|
|
122865
|
+
terminalId,
|
|
122866
|
+
workspace.worktree.path,
|
|
122867
|
+
options2,
|
|
122868
|
+
opts?.cleanupOnExit ? () => this.emitRemoved(workspaceId, terminalId) : void 0
|
|
122869
|
+
);
|
|
122870
|
+
addTerminalToLayout(workspaceId, terminalId, {
|
|
122871
|
+
command: options2?.command,
|
|
122872
|
+
cwd: options2?.cwd,
|
|
122873
|
+
env: options2?.env
|
|
122606
122874
|
});
|
|
122875
|
+
return session;
|
|
122607
122876
|
}
|
|
122608
122877
|
/**
|
|
122609
|
-
*
|
|
122610
|
-
*
|
|
122611
|
-
*
|
|
122612
|
-
* (which the scheduled fire path treats as a soft failure — see
|
|
122613
|
-
* `executeCronjob`). The manual trigger route maps the same condition to
|
|
122614
|
-
* a `CronjobProjectNotFoundError`.
|
|
122615
|
-
*
|
|
122616
|
-
* Workspace-scoped jobs return `null` rather than falling through to the
|
|
122617
|
-
* project-resolution branch when `workspaceId` is somehow missing. The
|
|
122618
|
-
* `create` path already rejects this via `CronjobWorkspaceMissingError`,
|
|
122619
|
-
* but a workspace-scoped row written by an older server version could
|
|
122620
|
-
* theoretically reach here with a null `workspaceId`. Falling through
|
|
122621
|
-
* would silently misfire into the project's default branch; returning
|
|
122622
|
-
* `null` makes the failure explicit (logged + `lastRunStatus = "failed"`
|
|
122623
|
-
* for the scheduler; `CronjobProjectNotFoundError` for the manual
|
|
122624
|
-
* trigger path).
|
|
122878
|
+
* Kill a single terminal. Removes it from the saved dockview layout and
|
|
122879
|
+
* emits `terminal-killed` so the dashboard's status stream prunes the
|
|
122880
|
+
* panel. Safe to call with an unknown terminalId — no-op.
|
|
122625
122881
|
*/
|
|
122626
|
-
|
|
122627
|
-
|
|
122628
|
-
|
|
122882
|
+
kill(terminalId) {
|
|
122883
|
+
const session = this.pool.get(terminalId);
|
|
122884
|
+
const workspaceId = session?.workspaceId;
|
|
122885
|
+
this.pool.kill(terminalId);
|
|
122886
|
+
if (workspaceId) {
|
|
122887
|
+
this.emitRemoved(workspaceId, terminalId);
|
|
122629
122888
|
}
|
|
122630
|
-
|
|
122631
|
-
|
|
122632
|
-
|
|
122633
|
-
|
|
122889
|
+
}
|
|
122890
|
+
/**
|
|
122891
|
+
* Drop a terminal from the saved dockview layout and broadcast
|
|
122892
|
+
* `terminal-killed` so an open dashboard prunes the panel. Shared by the
|
|
122893
|
+
* explicit {@link kill} path and the `cleanupOnExit` natural-exit hook wired
|
|
122894
|
+
* up in {@link spawn}. Idempotent: `removeTerminalFromLayout` is a no-op when
|
|
122895
|
+
* the panel is already gone, and a duplicate `terminal-killed` is harmless.
|
|
122896
|
+
*/
|
|
122897
|
+
emitRemoved(workspaceId, terminalId) {
|
|
122898
|
+
removeTerminalFromLayout(workspaceId, terminalId);
|
|
122899
|
+
emit({ kind: "terminal-killed", workspaceId, terminalId });
|
|
122900
|
+
}
|
|
122901
|
+
/**
|
|
122902
|
+
* Kill every PTY associated with a workspace. Used by the workspace
|
|
122903
|
+
* deletion path — the caller is responsible for tearing down the layout
|
|
122904
|
+
* tree via {@link deleteLayout} as well.
|
|
122905
|
+
*/
|
|
122906
|
+
killWorkspace(workspaceId) {
|
|
122907
|
+
this.pool.killWorkspace(workspaceId);
|
|
122908
|
+
}
|
|
122909
|
+
/**
|
|
122910
|
+
* Kill every tracked PTY across all workspaces. Used by the server
|
|
122911
|
+
* shutdown path in `start-server.ts`.
|
|
122912
|
+
*/
|
|
122913
|
+
killAll() {
|
|
122914
|
+
this.pool.killAll();
|
|
122634
122915
|
}
|
|
122635
122916
|
// -------------------------------------------------------------------------
|
|
122636
|
-
//
|
|
122917
|
+
// Per-terminal accessors
|
|
122637
122918
|
// -------------------------------------------------------------------------
|
|
122638
|
-
|
|
122639
|
-
|
|
122640
|
-
return file.jobs.find((j) => j.id === id);
|
|
122919
|
+
list(workspaceId) {
|
|
122920
|
+
return this.pool.list(workspaceId);
|
|
122641
122921
|
}
|
|
122642
|
-
|
|
122643
|
-
|
|
122644
|
-
const options2 = { maxRuns: 0 };
|
|
122645
|
-
void new E2(expression, options2);
|
|
122646
|
-
} catch {
|
|
122647
|
-
throw new InvalidCronExpressionError();
|
|
122648
|
-
}
|
|
122922
|
+
getSession(terminalId) {
|
|
122923
|
+
return this.pool.get(terminalId);
|
|
122649
122924
|
}
|
|
122650
|
-
|
|
122651
|
-
|
|
122652
|
-
const existing = state.jobs.get(job.id);
|
|
122653
|
-
if (existing) {
|
|
122654
|
-
existing.stop();
|
|
122655
|
-
state.jobs.delete(job.id);
|
|
122656
|
-
}
|
|
122657
|
-
if (!job.enabled) return;
|
|
122658
|
-
try {
|
|
122659
|
-
const cronInstance = new E2(job.cronExpression, () => {
|
|
122660
|
-
this.executeCronjob(job, fileKey).catch((err) => {
|
|
122661
|
-
log20.error({ jobId: job.id, err }, "unhandled error in cronjob execution");
|
|
122662
|
-
});
|
|
122663
|
-
});
|
|
122664
|
-
state.jobs.set(job.id, cronInstance);
|
|
122665
|
-
log20.info(
|
|
122666
|
-
{ jobId: job.id, name: job.name, cron: job.cronExpression, scope: job.scope },
|
|
122667
|
-
"scheduled cronjob"
|
|
122668
|
-
);
|
|
122669
|
-
} catch (err) {
|
|
122670
|
-
log20.error(
|
|
122671
|
-
{ jobId: job.id, cronExpression: job.cronExpression, err },
|
|
122672
|
-
"invalid cron expression, skipping job"
|
|
122673
|
-
);
|
|
122674
|
-
}
|
|
122925
|
+
getScrollback(terminalId, lines) {
|
|
122926
|
+
return this.pool.getScrollback(terminalId, lines);
|
|
122675
122927
|
}
|
|
122676
|
-
|
|
122677
|
-
|
|
122678
|
-
|
|
122679
|
-
|
|
122680
|
-
|
|
122681
|
-
|
|
122682
|
-
|
|
122683
|
-
|
|
122684
|
-
|
|
122685
|
-
|
|
122686
|
-
|
|
122687
|
-
|
|
122688
|
-
|
|
122689
|
-
|
|
122690
|
-
|
|
122691
|
-
|
|
122692
|
-
|
|
122693
|
-
|
|
122694
|
-
|
|
122695
|
-
|
|
122696
|
-
|
|
122697
|
-
|
|
122698
|
-
|
|
122699
|
-
|
|
122700
|
-
|
|
122701
|
-
|
|
122702
|
-
|
|
122703
|
-
}
|
|
122928
|
+
write(terminalId, data) {
|
|
122929
|
+
return this.pool.write(terminalId, data);
|
|
122930
|
+
}
|
|
122931
|
+
resize(terminalId, cols, rows) {
|
|
122932
|
+
this.pool.resize(terminalId, cols, rows);
|
|
122933
|
+
}
|
|
122934
|
+
subscribeOutput(terminalId, callback) {
|
|
122935
|
+
return this.pool.subscribeOutput(terminalId, callback);
|
|
122936
|
+
}
|
|
122937
|
+
// -------------------------------------------------------------------------
|
|
122938
|
+
// Layout persistence (dockview tree)
|
|
122939
|
+
//
|
|
122940
|
+
// Thin pass-throughs to `lib/terminal-layout-manager` so the API tier
|
|
122941
|
+
// doesn't reach across into `lib/` directly. The layout helpers will be
|
|
122942
|
+
// folded into a dedicated infra adapter in a follow-up phase (when the
|
|
122943
|
+
// chats/browsers domains get their own infra adapters); leaving them in
|
|
122944
|
+
// `lib/` for now is the minimum-disruption shape during the phased
|
|
122945
|
+
// migration.
|
|
122946
|
+
// -------------------------------------------------------------------------
|
|
122947
|
+
getLayout(workspaceId) {
|
|
122948
|
+
return getTerminalLayout(workspaceId);
|
|
122949
|
+
}
|
|
122950
|
+
saveLayout(workspaceId, tree) {
|
|
122951
|
+
saveTerminalLayout(workspaceId, tree);
|
|
122952
|
+
}
|
|
122953
|
+
deleteLayout(workspaceId) {
|
|
122954
|
+
deleteTerminalLayout(workspaceId);
|
|
122704
122955
|
}
|
|
122956
|
+
// -------------------------------------------------------------------------
|
|
122957
|
+
// Workspace `.band/config.json` `workspace.terminal` block
|
|
122958
|
+
// -------------------------------------------------------------------------
|
|
122705
122959
|
/**
|
|
122706
|
-
*
|
|
122960
|
+
* Load and validate the `workspace.terminal` block from
|
|
122961
|
+
* `.band/config.json`. Returns `null` when the workspace can't be
|
|
122962
|
+
* resolved, the config file is absent, the block is missing, or the
|
|
122963
|
+
* payload fails validation.
|
|
122707
122964
|
*
|
|
122708
|
-
*
|
|
122709
|
-
*
|
|
122710
|
-
*
|
|
122711
|
-
*
|
|
122965
|
+
* Absorbed from the old `lib/terminal-config.ts:loadWorkspaceTerminalConfig`
|
|
122966
|
+
* — same parsing semantics, but the lookup now goes through the service
|
|
122967
|
+
* tier so callers (currently the workspace router's `getTerminalConfig`
|
|
122968
|
+
* query) reach it via `terminalService` instead of a stand-alone helper.
|
|
122712
122969
|
*/
|
|
122713
|
-
|
|
122714
|
-
|
|
122715
|
-
|
|
122716
|
-
|
|
122717
|
-
log20.warn({ jobId, err }, "failed to update lastRun on cronjob");
|
|
122718
|
-
}
|
|
122970
|
+
getWorkspaceConfig(workspaceId) {
|
|
122971
|
+
const workspace = workspaceService.resolve(workspaceId);
|
|
122972
|
+
if (!workspace) return null;
|
|
122973
|
+
return this.loadWorkspaceConfigFromPaths(workspace.worktree.path, workspace.project.path);
|
|
122719
122974
|
}
|
|
122720
|
-
|
|
122721
|
-
|
|
122722
|
-
|
|
122723
|
-
|
|
122724
|
-
|
|
122725
|
-
|
|
122726
|
-
|
|
122727
|
-
|
|
122975
|
+
/**
|
|
122976
|
+
* Internal: the path-driven parse so tests / future non-tRPC entry
|
|
122977
|
+
* points can plug raw paths in without going through `resolveWorkspace`.
|
|
122978
|
+
*/
|
|
122979
|
+
loadWorkspaceConfigFromPaths(worktreePath, projectPath) {
|
|
122980
|
+
const raw = loadProjectConfig(worktreePath, projectPath);
|
|
122981
|
+
if (!raw) return null;
|
|
122982
|
+
const terminalBlock = raw.workspace && typeof raw.workspace === "object" ? raw.workspace.terminal : void 0;
|
|
122983
|
+
if (!terminalBlock) return null;
|
|
122984
|
+
const result = WorkspaceTerminalConfigSchema.safeParse(terminalBlock);
|
|
122985
|
+
if (!result.success) {
|
|
122986
|
+
log21.warn(
|
|
122987
|
+
"Invalid workspace.terminal config: %s",
|
|
122988
|
+
result.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`).join("; ")
|
|
122989
|
+
);
|
|
122990
|
+
return null;
|
|
122728
122991
|
}
|
|
122729
|
-
|
|
122992
|
+
return result.data;
|
|
122730
122993
|
}
|
|
122731
122994
|
};
|
|
122732
|
-
var
|
|
122995
|
+
var terminalService = new TerminalService();
|
|
122733
122996
|
|
|
122734
|
-
// src/server/services/
|
|
122735
|
-
|
|
122736
|
-
var
|
|
122737
|
-
var
|
|
122738
|
-
|
|
122739
|
-
|
|
122997
|
+
// src/server/services/cronjob-service.ts
|
|
122998
|
+
var log22 = createLogger("cronjob-service");
|
|
122999
|
+
var SCHEDULER_KEY = Symbol.for("band.cronjob-scheduler");
|
|
123000
|
+
var g12 = globalThis;
|
|
123001
|
+
if (!g12[SCHEDULER_KEY]) {
|
|
123002
|
+
g12[SCHEDULER_KEY] = {
|
|
123003
|
+
jobs: /* @__PURE__ */ new Map(),
|
|
123004
|
+
started: false,
|
|
123005
|
+
spawningTerminals: /* @__PURE__ */ new Set()
|
|
123006
|
+
};
|
|
122740
123007
|
}
|
|
122741
|
-
|
|
122742
|
-
|
|
122743
|
-
|
|
122744
|
-
|
|
122745
|
-
|
|
122746
|
-
|
|
122747
|
-
|
|
122748
|
-
|
|
122749
|
-
|
|
122750
|
-
|
|
122751
|
-
|
|
122752
|
-
|
|
123008
|
+
function schedulerState() {
|
|
123009
|
+
return g12[SCHEDULER_KEY];
|
|
123010
|
+
}
|
|
123011
|
+
var cronjobVia = external_exports.enum(["chat", "terminal"]);
|
|
123012
|
+
var cronjobCreateInput = external_exports.object({
|
|
123013
|
+
key: external_exports.string().min(1),
|
|
123014
|
+
name: external_exports.string().min(1),
|
|
123015
|
+
prompt: external_exports.string().min(1),
|
|
123016
|
+
cronExpression: external_exports.string().min(1),
|
|
123017
|
+
scope: external_exports.enum(["project", "workspace"]),
|
|
123018
|
+
workspaceId: external_exports.string().optional(),
|
|
123019
|
+
// Dispatch target for scheduled + manual fires. Optional — absent defaults to
|
|
123020
|
+
// "chat" in `create()` so existing callers (the web UI) keep their behavior.
|
|
123021
|
+
via: cronjobVia.optional(),
|
|
123022
|
+
enabled: external_exports.boolean().default(true)
|
|
123023
|
+
});
|
|
123024
|
+
var cronjobUpdateInput = external_exports.object({
|
|
123025
|
+
// `.min(1)` rejects the obviously invalid empty-string `key`/`id` at the
|
|
123026
|
+
// validator instead of letting it reach the service, which would
|
|
123027
|
+
// `loadFile("")` an empty result and 404 on `findIndex`. The legacy
|
|
123028
|
+
// router accepted bare `z.string()` here; the migration is the natural
|
|
123029
|
+
// moment to tighten the boundary so the create + update shapes stay
|
|
123030
|
+
// consistent (`cronjobCreateInput.key` already has `.min(1)`).
|
|
123031
|
+
key: external_exports.string().min(1),
|
|
123032
|
+
id: external_exports.string().min(1),
|
|
123033
|
+
name: external_exports.string().min(1).optional(),
|
|
123034
|
+
prompt: external_exports.string().min(1).optional(),
|
|
123035
|
+
cronExpression: external_exports.string().min(1).optional(),
|
|
123036
|
+
enabled: external_exports.boolean().optional()
|
|
123037
|
+
});
|
|
123038
|
+
var cronjobByIdInput = external_exports.object({
|
|
123039
|
+
key: external_exports.string().min(1),
|
|
123040
|
+
id: external_exports.string().min(1)
|
|
123041
|
+
});
|
|
123042
|
+
var CronjobNotFoundError = class extends Error {
|
|
123043
|
+
constructor(message = "Cronjob not found") {
|
|
123044
|
+
super(message);
|
|
123045
|
+
this.name = "CronjobNotFoundError";
|
|
123046
|
+
}
|
|
123047
|
+
};
|
|
123048
|
+
var InvalidCronExpressionError = class extends Error {
|
|
123049
|
+
constructor(message = "Invalid cron expression") {
|
|
123050
|
+
super(message);
|
|
123051
|
+
this.name = "InvalidCronExpressionError";
|
|
123052
|
+
}
|
|
123053
|
+
};
|
|
123054
|
+
var CronjobWorkspaceMissingError = class extends Error {
|
|
123055
|
+
constructor(message = "workspaceId is required for workspace-scoped cronjobs") {
|
|
123056
|
+
super(message);
|
|
123057
|
+
this.name = "CronjobWorkspaceMissingError";
|
|
123058
|
+
}
|
|
123059
|
+
};
|
|
123060
|
+
var CronjobProjectNotFoundError = class extends Error {
|
|
123061
|
+
constructor(message = "Project not found") {
|
|
123062
|
+
super(message);
|
|
123063
|
+
this.name = "CronjobProjectNotFoundError";
|
|
123064
|
+
}
|
|
123065
|
+
};
|
|
123066
|
+
var CronjobService = class {
|
|
123067
|
+
constructor(queries = new CronjobQueries()) {
|
|
123068
|
+
this.queries = queries;
|
|
123069
|
+
}
|
|
123070
|
+
// -------------------------------------------------------------------------
|
|
123071
|
+
// Read operations
|
|
123072
|
+
// -------------------------------------------------------------------------
|
|
122753
123073
|
/**
|
|
122754
|
-
*
|
|
122755
|
-
*
|
|
123074
|
+
* List jobs for a key, or — if neither `project` nor `workspaceId` is
|
|
123075
|
+
* provided — every cronjob across every key.
|
|
123076
|
+
*
|
|
123077
|
+
* Mirrors the legacy tRPC `cronjobs.list` shape so the router stays a
|
|
123078
|
+
* straight delegate. The "all" branch returns `fileKey` on each row
|
|
123079
|
+
* because callers (the dashboard's cronjob settings page) need both
|
|
123080
|
+
* pieces to render the project/workspace breadcrumbs.
|
|
122756
123081
|
*/
|
|
122757
|
-
|
|
122758
|
-
if (
|
|
122759
|
-
|
|
122760
|
-
|
|
122761
|
-
|
|
122762
|
-
if (!row) return;
|
|
122763
|
-
this.persisted.add(workspaceId);
|
|
122764
|
-
try {
|
|
122765
|
-
const parsed = JSON.parse(row.state);
|
|
122766
|
-
this.focus.set(workspaceId, {
|
|
122767
|
-
chat: parsed.chat,
|
|
122768
|
-
terminal: parsed.terminal,
|
|
122769
|
-
browser: parsed.browser
|
|
122770
|
-
});
|
|
122771
|
-
} catch (err) {
|
|
122772
|
-
log21.warn({ workspaceId, err }, "failed to parse persisted panel focus; ignoring");
|
|
123082
|
+
list(filter) {
|
|
123083
|
+
if (filter?.project) {
|
|
123084
|
+
const { project } = filter;
|
|
123085
|
+
const file = this.queries.loadFile(project);
|
|
123086
|
+
return { jobs: file.jobs.map((j) => ({ ...j, fileKey: project })) };
|
|
122773
123087
|
}
|
|
123088
|
+
if (filter?.workspaceId) {
|
|
123089
|
+
const { workspaceId } = filter;
|
|
123090
|
+
const file = this.queries.loadFile(workspaceId);
|
|
123091
|
+
return {
|
|
123092
|
+
jobs: file.jobs.map((j) => ({ ...j, fileKey: workspaceId }))
|
|
123093
|
+
};
|
|
123094
|
+
}
|
|
123095
|
+
return { jobs: this.queries.listAll() };
|
|
122774
123096
|
}
|
|
122775
|
-
/** Get
|
|
122776
|
-
get(
|
|
122777
|
-
this.
|
|
122778
|
-
|
|
123097
|
+
/** Get a single job by `key` + `id`. Throws `CronjobNotFoundError` if absent. */
|
|
123098
|
+
get(key, id) {
|
|
123099
|
+
const job = this.findJob(key, id);
|
|
123100
|
+
if (!job) throw new CronjobNotFoundError();
|
|
123101
|
+
return { job };
|
|
122779
123102
|
}
|
|
123103
|
+
// -------------------------------------------------------------------------
|
|
123104
|
+
// Mutations
|
|
123105
|
+
// -------------------------------------------------------------------------
|
|
122780
123106
|
/**
|
|
122781
|
-
*
|
|
122782
|
-
*
|
|
122783
|
-
*
|
|
123107
|
+
* Create a new cronjob and re-bind the scheduler.
|
|
123108
|
+
*
|
|
123109
|
+
* Validates the cron expression by attempting to construct a `Cron` with
|
|
123110
|
+
* `maxRuns: 0` (parses without scheduling). Throws
|
|
123111
|
+
* `InvalidCronExpressionError` for a bad expression and
|
|
123112
|
+
* `CronjobWorkspaceMissingError` when a workspace-scoped job is missing
|
|
123113
|
+
* its `workspaceId`.
|
|
122784
123114
|
*/
|
|
122785
|
-
|
|
122786
|
-
this.
|
|
122787
|
-
|
|
122788
|
-
|
|
122789
|
-
const next = { ...current2, [panelType]: panelId };
|
|
122790
|
-
this.focus.set(workspaceId, next);
|
|
122791
|
-
const now = Date.now();
|
|
122792
|
-
const state = JSON.stringify(next);
|
|
122793
|
-
if (this.persisted.has(workspaceId)) {
|
|
122794
|
-
updatePanelState(focusRowId(workspaceId), { state, updatedAt: now });
|
|
122795
|
-
} else {
|
|
122796
|
-
insertPanelState({
|
|
122797
|
-
id: focusRowId(workspaceId),
|
|
122798
|
-
workspaceId,
|
|
122799
|
-
panelType: PANEL_FOCUS_TYPE,
|
|
122800
|
-
state,
|
|
122801
|
-
createdAt: now,
|
|
122802
|
-
updatedAt: now
|
|
122803
|
-
});
|
|
122804
|
-
this.persisted.add(workspaceId);
|
|
123115
|
+
create(input) {
|
|
123116
|
+
this.assertValidCron(input.cronExpression);
|
|
123117
|
+
if (input.scope === "workspace" && !input.workspaceId) {
|
|
123118
|
+
throw new CronjobWorkspaceMissingError();
|
|
122805
123119
|
}
|
|
123120
|
+
const file = this.queries.loadFile(input.key);
|
|
123121
|
+
const job = {
|
|
123122
|
+
id: generateCronjobId(),
|
|
123123
|
+
name: input.name,
|
|
123124
|
+
prompt: input.prompt,
|
|
123125
|
+
cronExpression: input.cronExpression,
|
|
123126
|
+
scope: input.scope,
|
|
123127
|
+
workspaceId: input.workspaceId,
|
|
123128
|
+
// Absent `via` means the web UI (or a pre-#581 caller) — default to chat
|
|
123129
|
+
// so existing cronjobs keep dispatching exactly as before.
|
|
123130
|
+
via: input.via ?? "chat",
|
|
123131
|
+
enabled: input.enabled,
|
|
123132
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
123133
|
+
};
|
|
123134
|
+
file.jobs.push(job);
|
|
123135
|
+
this.queries.saveFile(input.key, file);
|
|
123136
|
+
this.reloadSchedules();
|
|
123137
|
+
return { job };
|
|
122806
123138
|
}
|
|
122807
123139
|
/**
|
|
122808
|
-
*
|
|
122809
|
-
*
|
|
123140
|
+
* Partially update an existing cronjob.
|
|
123141
|
+
*
|
|
123142
|
+
* Each optional field is applied only when present, matching the legacy
|
|
123143
|
+
* router behavior. If `cronExpression` is supplied it's validated before
|
|
123144
|
+
* the write. Throws `CronjobNotFoundError` if no job matches `key`+`id`.
|
|
122810
123145
|
*/
|
|
122811
|
-
|
|
122812
|
-
|
|
122813
|
-
|
|
122814
|
-
|
|
122815
|
-
|
|
123146
|
+
update(input) {
|
|
123147
|
+
if (input.cronExpression !== void 0) {
|
|
123148
|
+
this.assertValidCron(input.cronExpression);
|
|
123149
|
+
}
|
|
123150
|
+
const file = this.queries.loadFile(input.key);
|
|
123151
|
+
const job = file.jobs.find((j) => j.id === input.id);
|
|
123152
|
+
if (!job) throw new CronjobNotFoundError();
|
|
123153
|
+
if (input.name !== void 0) job.name = input.name;
|
|
123154
|
+
if (input.prompt !== void 0) job.prompt = input.prompt;
|
|
123155
|
+
if (input.cronExpression !== void 0) job.cronExpression = input.cronExpression;
|
|
123156
|
+
if (input.enabled !== void 0) job.enabled = input.enabled;
|
|
123157
|
+
this.queries.saveFile(input.key, file);
|
|
123158
|
+
this.reloadSchedules();
|
|
123159
|
+
return { job };
|
|
122816
123160
|
}
|
|
122817
|
-
};
|
|
122818
|
-
var panelFocusService = new PanelFocusService();
|
|
122819
|
-
|
|
122820
|
-
// src/server/services/terminal-service.ts
|
|
122821
|
-
init_src();
|
|
122822
|
-
init_zod();
|
|
122823
|
-
|
|
122824
|
-
// src/server/infra/terminals/terminal-pool.ts
|
|
122825
|
-
init_src();
|
|
122826
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
122827
|
-
import { existsSync as existsSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
122828
|
-
import { tmpdir } from "node:os";
|
|
122829
|
-
import { join as join19, sep as sep3 } from "node:path";
|
|
122830
|
-
var log22 = createLogger("terminal-pool");
|
|
122831
|
-
var MAX_SCROLLBACK_SIZE = 1e5;
|
|
122832
|
-
var TerminalPool = class {
|
|
122833
|
-
/** terminalId -> session */
|
|
122834
|
-
terminals = /* @__PURE__ */ new Map();
|
|
122835
|
-
/** workspaceId -> Set<terminalId> (reverse index for workspace-level cleanup) */
|
|
122836
|
-
workspaceTerminals = /* @__PURE__ */ new Map();
|
|
122837
|
-
/** terminalId -> Set<listener> for live output streaming */
|
|
122838
|
-
outputListeners = /* @__PURE__ */ new Map();
|
|
122839
123161
|
/**
|
|
122840
|
-
*
|
|
122841
|
-
* concurrent paths — the WebSocket handler (spawn-on-`getSession`-miss) and
|
|
122842
|
-
* the tRPC `terminal.create` mutation. Without deduplication both spawn a PTY
|
|
122843
|
-
* and the second `terminals.set` overwrites the first, so the client stays
|
|
122844
|
-
* wired to one PTY while the server's session map (and thus scrollback /
|
|
122845
|
-
* `terminals output` / reconnect-replay) points at the other. That's the
|
|
122846
|
-
* "output on screen but empty server scrollback until reload" bug
|
|
122847
|
-
* (band-app/band#617). This map makes concurrent spawns share ONE PTY.
|
|
122848
|
-
*/
|
|
122849
|
-
spawning = /* @__PURE__ */ new Map();
|
|
122850
|
-
/**
|
|
122851
|
-
* Spawn (or return the already-spawning/spawned) PTY for a terminalId.
|
|
123162
|
+
* Delete a cronjob and re-bind the scheduler.
|
|
122852
123163
|
*
|
|
122853
|
-
*
|
|
122854
|
-
* as-is, and concurrent spawn calls for the same id share a single in-flight
|
|
122855
|
-
* promise — so the WebSocket and tRPC create paths never create competing
|
|
122856
|
-
* PTYs. `workspaceRoot` is the absolute worktree path; resolving the
|
|
122857
|
-
* workspaceId to a path is the service tier's job so the pool stays oblivious
|
|
122858
|
-
* to the workspace registry.
|
|
123164
|
+
* Throws `CronjobNotFoundError` if the job is already gone.
|
|
122859
123165
|
*/
|
|
122860
|
-
|
|
122861
|
-
const
|
|
122862
|
-
|
|
122863
|
-
|
|
122864
|
-
|
|
122865
|
-
|
|
122866
|
-
|
|
122867
|
-
|
|
122868
|
-
return await promise;
|
|
122869
|
-
} finally {
|
|
122870
|
-
this.spawning.delete(terminalId);
|
|
122871
|
-
}
|
|
122872
|
-
}
|
|
122873
|
-
async spawnNew(workspaceId, terminalId, workspaceRoot, options2) {
|
|
122874
|
-
const shell = defaultShell();
|
|
122875
|
-
const resolvedPath = await shellPath();
|
|
122876
|
-
const env = {};
|
|
122877
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
122878
|
-
if (value != null) {
|
|
122879
|
-
env[key] = value;
|
|
122880
|
-
}
|
|
122881
|
-
}
|
|
122882
|
-
env.PATH = resolvedPath;
|
|
122883
|
-
env.TERM = "xterm-256color";
|
|
122884
|
-
env.COLORTERM = "truecolor";
|
|
122885
|
-
env.COLORFGBG = "15;0";
|
|
122886
|
-
delete env.PORT;
|
|
122887
|
-
if (options2?.env) {
|
|
122888
|
-
Object.assign(env, options2.env);
|
|
122889
|
-
}
|
|
122890
|
-
env.BAND_DISPATCH = "terminal";
|
|
122891
|
-
let cwd = workspaceRoot;
|
|
122892
|
-
if (options2?.cwd) {
|
|
122893
|
-
const resolved = join19(workspaceRoot, options2.cwd);
|
|
122894
|
-
if (resolved !== workspaceRoot && !resolved.startsWith(workspaceRoot + sep3)) {
|
|
122895
|
-
log22.warn(
|
|
122896
|
-
"Ignoring cwd %s \u2014 resolves outside workspace root %s",
|
|
122897
|
-
options2.cwd,
|
|
122898
|
-
workspaceRoot
|
|
122899
|
-
);
|
|
122900
|
-
} else if (existsSync8(resolved)) {
|
|
122901
|
-
cwd = resolved;
|
|
122902
|
-
} else {
|
|
122903
|
-
log22.warn("Ignoring cwd %s \u2014 directory does not exist", options2.cwd);
|
|
122904
|
-
}
|
|
122905
|
-
}
|
|
122906
|
-
if (!existsSync8(cwd)) {
|
|
122907
|
-
throw new Error(`Workspace directory does not exist: ${cwd}`);
|
|
122908
|
-
}
|
|
122909
|
-
if (!existsSync8(shell)) {
|
|
122910
|
-
throw new Error(`Shell not found: ${shell}`);
|
|
122911
|
-
}
|
|
122912
|
-
log22.debug(
|
|
122913
|
-
"Spawning shell %s in %s for terminal %s (PATH=%s)",
|
|
122914
|
-
shell,
|
|
122915
|
-
cwd,
|
|
122916
|
-
terminalId,
|
|
122917
|
-
resolvedPath.slice(0, 200)
|
|
122918
|
-
);
|
|
122919
|
-
const nodePty = await import("node-pty");
|
|
122920
|
-
let ptyProcess;
|
|
122921
|
-
try {
|
|
122922
|
-
ptyProcess = nodePty.spawn(shell, [], {
|
|
122923
|
-
name: "xterm-256color",
|
|
122924
|
-
cols: 80,
|
|
122925
|
-
rows: 24,
|
|
122926
|
-
cwd,
|
|
122927
|
-
env
|
|
122928
|
-
});
|
|
122929
|
-
} catch (err) {
|
|
122930
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
122931
|
-
log22.error("pty.spawn failed: %s (shell=%s, cwd=%s)", msg, shell, cwd);
|
|
122932
|
-
throw err;
|
|
122933
|
-
}
|
|
122934
|
-
const session = { pty: ptyProcess, scrollback: "", workspaceId };
|
|
122935
|
-
this.terminals.set(terminalId, session);
|
|
122936
|
-
if (options2?.command) {
|
|
122937
|
-
this.autoRunCommand(session, options2.command);
|
|
122938
|
-
}
|
|
122939
|
-
let ids = this.workspaceTerminals.get(workspaceId);
|
|
122940
|
-
if (!ids) {
|
|
122941
|
-
ids = /* @__PURE__ */ new Set();
|
|
122942
|
-
this.workspaceTerminals.set(workspaceId, ids);
|
|
123166
|
+
delete(key, id) {
|
|
123167
|
+
const file = this.queries.loadFile(key);
|
|
123168
|
+
const index3 = file.jobs.findIndex((j) => j.id === id);
|
|
123169
|
+
if (index3 === -1) throw new CronjobNotFoundError();
|
|
123170
|
+
const [removed] = file.jobs.splice(index3, 1);
|
|
123171
|
+
this.queries.saveFile(key, file);
|
|
123172
|
+
if (removed?.lastTerminalId) {
|
|
123173
|
+
terminalService.kill(removed.lastTerminalId);
|
|
122943
123174
|
}
|
|
122944
|
-
|
|
122945
|
-
|
|
122946
|
-
session.scrollback += data;
|
|
122947
|
-
if (session.scrollback.length > MAX_SCROLLBACK_SIZE) {
|
|
122948
|
-
session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK_SIZE);
|
|
122949
|
-
}
|
|
122950
|
-
const listeners3 = this.outputListeners.get(terminalId);
|
|
122951
|
-
if (listeners3) {
|
|
122952
|
-
for (const cb2 of listeners3) {
|
|
122953
|
-
try {
|
|
122954
|
-
cb2(data);
|
|
122955
|
-
} catch {
|
|
122956
|
-
}
|
|
122957
|
-
}
|
|
122958
|
-
}
|
|
122959
|
-
});
|
|
122960
|
-
ptyProcess.onExit(() => {
|
|
122961
|
-
log22.debug("Terminal exited: %s (workspace %s)", terminalId, workspaceId);
|
|
122962
|
-
this.terminals.delete(terminalId);
|
|
122963
|
-
this.outputListeners.delete(terminalId);
|
|
122964
|
-
const set = this.workspaceTerminals.get(workspaceId);
|
|
122965
|
-
if (set) {
|
|
122966
|
-
set.delete(terminalId);
|
|
122967
|
-
if (set.size === 0) {
|
|
122968
|
-
this.workspaceTerminals.delete(workspaceId);
|
|
122969
|
-
}
|
|
122970
|
-
}
|
|
122971
|
-
if (session.autoRunFile) {
|
|
122972
|
-
try {
|
|
122973
|
-
unlinkSync2(session.autoRunFile);
|
|
122974
|
-
} catch {
|
|
122975
|
-
}
|
|
122976
|
-
}
|
|
122977
|
-
});
|
|
122978
|
-
return session;
|
|
123175
|
+
this.reloadSchedules();
|
|
123176
|
+
return { ok: true };
|
|
122979
123177
|
}
|
|
122980
123178
|
/**
|
|
122981
|
-
*
|
|
122982
|
-
* robustly.
|
|
122983
|
-
*
|
|
122984
|
-
* Writing a long command line straight into a cold PTY is racy on three
|
|
122985
|
-
* fronts, all of which surfaced in production with `via=terminal`
|
|
122986
|
-
* prompt-as-argv launches (`'<agent-binary>' '<entire-prompt>'`):
|
|
123179
|
+
* Fire a cronjob synchronously (manual "run now" path).
|
|
122987
123180
|
*
|
|
122988
|
-
*
|
|
122989
|
-
*
|
|
122990
|
-
*
|
|
122991
|
-
*
|
|
122992
|
-
* 2. **Truncation.** A single input line longer than the tty's
|
|
122993
|
-
* canonical buffer (`MAX_CANON`, ~4 KB) is clipped. Long prompts
|
|
122994
|
-
* blow past that easily.
|
|
122995
|
-
* 3. **UTF-8 corruption.** A multi-byte sequence split across a tty
|
|
122996
|
-
* buffer boundary is mangled (the classic em-dash → mojibake).
|
|
123181
|
+
* Uses the same `band:cronId`-labeled chat as the scheduled fire so the
|
|
123182
|
+
* user sees both paths converge on a single dedicated conversation
|
|
123183
|
+
* (issue #520). `submitTask` may throw `TaskConflictError`, which the API
|
|
123184
|
+
* tier translates into a 409 Conflict response.
|
|
122997
123185
|
*
|
|
122998
|
-
*
|
|
122999
|
-
*
|
|
123000
|
-
*
|
|
123001
|
-
*
|
|
123002
|
-
*
|
|
123003
|
-
*
|
|
123004
|
-
*
|
|
123005
|
-
*
|
|
123186
|
+
* Atomicity caveat: `getOrCreateCronjobChat` and `submitTask` are not run
|
|
123187
|
+
* inside a single transaction. If `createChat` succeeds but `submitTask`
|
|
123188
|
+
* later throws an unexpected (non-`TaskConflictError`) error, the
|
|
123189
|
+
* freshly-created chat remains in the workspace as an orphan with no
|
|
123190
|
+
* task ever attached. The next manual `trigger` (or scheduled fire)
|
|
123191
|
+
* reuses that chat via its `band:cronId` label, so the orphan
|
|
123192
|
+
* self-heals — but the user may briefly see an empty chat pane. The
|
|
123193
|
+
* legacy implementation had the same gap; preserved here to match
|
|
123194
|
+
* existing behavior. If this ever needs to be tightened, the fix is
|
|
123195
|
+
* either (a) wrap both calls in a try/catch that deletes the chat on
|
|
123196
|
+
* unexpected failure, or (b) defer `createChat` until `submitTask`
|
|
123197
|
+
* succeeds (which would require buffering the chat name/labels on the
|
|
123198
|
+
* task row).
|
|
123006
123199
|
*/
|
|
123007
|
-
|
|
123008
|
-
const
|
|
123009
|
-
|
|
123010
|
-
|
|
123011
|
-
|
|
123012
|
-
|
|
123013
|
-
|
|
123014
|
-
|
|
123015
|
-
|
|
123016
|
-
log22.warn(
|
|
123017
|
-
"autoRunCommand: temp-file staging failed (%s); writing command directly",
|
|
123018
|
-
err instanceof Error ? err.message : String(err)
|
|
123019
|
-
);
|
|
123020
|
-
pty.write(`${command}
|
|
123021
|
-
`);
|
|
123022
|
-
return;
|
|
123023
|
-
}
|
|
123024
|
-
const quotedPath = cmdFile.replace(/'/g, `'\\''`);
|
|
123025
|
-
const sourceLine = `. '${quotedPath}'
|
|
123026
|
-
`;
|
|
123027
|
-
let done = false;
|
|
123028
|
-
const inject = () => {
|
|
123029
|
-
if (done) return;
|
|
123030
|
-
done = true;
|
|
123031
|
-
try {
|
|
123032
|
-
pty.write(sourceLine);
|
|
123033
|
-
} catch (err) {
|
|
123034
|
-
log22.warn("autoRunCommand: failed to inject command line (%s)", err);
|
|
123200
|
+
async trigger(key, id) {
|
|
123201
|
+
const job = this.findJob(key, id);
|
|
123202
|
+
if (!job) throw new CronjobNotFoundError();
|
|
123203
|
+
const workspaceId = this.resolveWorkspaceId(job, key);
|
|
123204
|
+
if (!workspaceId) throw new CronjobProjectNotFoundError();
|
|
123205
|
+
if (job.via === "terminal") {
|
|
123206
|
+
const { terminalId } = await this.spawnCronTerminal(job, workspaceId, key);
|
|
123207
|
+
if (terminalId) {
|
|
123208
|
+
return { via: "terminal", terminalId, workspaceId };
|
|
123035
123209
|
}
|
|
123036
|
-
}
|
|
123037
|
-
|
|
123038
|
-
const
|
|
123039
|
-
|
|
123040
|
-
dataDisposed = true;
|
|
123041
|
-
dataDisposable.dispose();
|
|
123042
|
-
};
|
|
123043
|
-
const dataDisposable = pty.onData(() => {
|
|
123044
|
-
disposeData();
|
|
123045
|
-
inject();
|
|
123046
|
-
});
|
|
123047
|
-
const timer = setTimeout(inject, 750);
|
|
123048
|
-
timer.unref();
|
|
123049
|
-
let exitDisposed = false;
|
|
123050
|
-
const exitDisposable = pty.onExit(() => {
|
|
123051
|
-
done = true;
|
|
123052
|
-
clearTimeout(timer);
|
|
123053
|
-
disposeData();
|
|
123054
|
-
if (exitDisposed) return;
|
|
123055
|
-
exitDisposed = true;
|
|
123056
|
-
exitDisposable.dispose();
|
|
123057
|
-
});
|
|
123210
|
+
}
|
|
123211
|
+
const cronChat = this.getOrCreateCronjobChat(workspaceId, job);
|
|
123212
|
+
const task = taskService.submitTask({ workspaceId, chatId: cronChat.id, prompt: job.prompt });
|
|
123213
|
+
return { via: "chat", taskId: task.id, workspaceId, chatId: cronChat.id };
|
|
123058
123214
|
}
|
|
123215
|
+
// -------------------------------------------------------------------------
|
|
123216
|
+
// Lifecycle hooks for workspace/project removal
|
|
123217
|
+
// -------------------------------------------------------------------------
|
|
123059
123218
|
/**
|
|
123060
|
-
*
|
|
123219
|
+
* Stop all timers for jobs in `key` and remove the file rows.
|
|
123220
|
+
*
|
|
123221
|
+
* Called when a workspace or project is removed. Order matters: stop
|
|
123222
|
+
* timers first so no further fires queue up between the timer-stop and
|
|
123223
|
+
* the row-delete. An in-flight fire is already safe in either order
|
|
123224
|
+
* because `executeCronjob` operates on closed-over `job`/`fileKey` (it
|
|
123225
|
+
* never re-reads the row) and `safeUpdateLastRun` swallows the
|
|
123226
|
+
* row-missing case. The two steps were previously two separate calls
|
|
123227
|
+
* (`stopJobsForKey` + `deleteCronjobFile`) at every call site — the
|
|
123228
|
+
* legacy callers always invoked them in pairs, so they're collapsed
|
|
123229
|
+
* here to make the contract harder to misuse.
|
|
123230
|
+
*
|
|
123231
|
+
* Partial-failure recovery: if `deleteFile` throws (e.g. a transient DB
|
|
123232
|
+
* lock), the timers are already stopped but the rows remain in the DB.
|
|
123233
|
+
* The next `reloadSchedules()` call — triggered by any subsequent
|
|
123234
|
+
* cronjob mutation, or by a server restart — re-reads the rows from the
|
|
123235
|
+
* DB and re-registers the timers, so the orphaned rows self-heal
|
|
123236
|
+
* without manual intervention. We do NOT roll back the `stop()` calls
|
|
123237
|
+
* because re-binding timers for a workspace that's mid-deletion is
|
|
123238
|
+
* worse than leaving them stopped: the workspace path is about to be
|
|
123239
|
+
* removed and any fire that races the removal would target a
|
|
123240
|
+
* non-existent worktree. Re-registration on the next reload is the
|
|
123241
|
+
* intentional recovery strategy; operators encountering a partial
|
|
123242
|
+
* failure on this path should expect the next reload to be a no-op
|
|
123243
|
+
* once the workspace removal completes and the rows are re-attempted.
|
|
123061
123244
|
*/
|
|
123062
|
-
|
|
123063
|
-
|
|
123245
|
+
removeForKey(key) {
|
|
123246
|
+
this.stopJobsForKey(key);
|
|
123247
|
+
this.queries.deleteFile(key);
|
|
123064
123248
|
}
|
|
123065
|
-
|
|
123066
|
-
|
|
123067
|
-
|
|
123068
|
-
|
|
123069
|
-
|
|
123070
|
-
|
|
123071
|
-
|
|
123249
|
+
// -------------------------------------------------------------------------
|
|
123250
|
+
// Scheduler lifecycle
|
|
123251
|
+
// -------------------------------------------------------------------------
|
|
123252
|
+
/** Start the cronjob scheduler. Called once on server boot. */
|
|
123253
|
+
start() {
|
|
123254
|
+
const state = schedulerState();
|
|
123255
|
+
if (state.started) return;
|
|
123256
|
+
state.started = true;
|
|
123257
|
+
this.loadAndScheduleAll();
|
|
123258
|
+
log22.info("cronjob scheduler started");
|
|
123259
|
+
}
|
|
123260
|
+
/** Stop the cronjob scheduler. Called on graceful shutdown. */
|
|
123261
|
+
stop() {
|
|
123262
|
+
const state = schedulerState();
|
|
123263
|
+
for (const [, cron] of state.jobs) {
|
|
123264
|
+
cron.stop();
|
|
123072
123265
|
}
|
|
123266
|
+
state.jobs.clear();
|
|
123267
|
+
state.started = false;
|
|
123268
|
+
log22.info("cronjob scheduler stopped");
|
|
123269
|
+
}
|
|
123270
|
+
/** Reload all schedules from the database. Call after cronjob mutations. */
|
|
123271
|
+
reloadSchedules() {
|
|
123272
|
+
if (!schedulerState().started) return;
|
|
123273
|
+
this.loadAndScheduleAll();
|
|
123073
123274
|
}
|
|
123074
123275
|
/**
|
|
123075
|
-
*
|
|
123276
|
+
* Stop all scheduled jobs for a key (e.g. when a workspace is removed).
|
|
123076
123277
|
*
|
|
123077
|
-
*
|
|
123078
|
-
*
|
|
123079
|
-
*
|
|
123278
|
+
* Private because the only correct call site is `removeForKey` above:
|
|
123279
|
+
* stopping the timers without also dropping the rows would re-schedule
|
|
123280
|
+
* the same jobs on the next `reloadSchedules()` tick. Keeping it
|
|
123281
|
+
* accessible only through `removeForKey` prevents future callers from
|
|
123282
|
+
* accidentally invoking just one half of the pair.
|
|
123080
123283
|
*/
|
|
123081
|
-
|
|
123082
|
-
const
|
|
123083
|
-
|
|
123084
|
-
const
|
|
123085
|
-
|
|
123086
|
-
|
|
123087
|
-
|
|
123088
|
-
|
|
123089
|
-
|
|
123090
|
-
title = session.pty.process;
|
|
123091
|
-
} catch {
|
|
123092
|
-
}
|
|
123093
|
-
result.push({
|
|
123094
|
-
terminalId,
|
|
123095
|
-
workspaceId,
|
|
123096
|
-
pid: session.pty.pid,
|
|
123097
|
-
scrollbackLength: session.scrollback.length,
|
|
123098
|
-
title
|
|
123099
|
-
});
|
|
123284
|
+
stopJobsForKey(key) {
|
|
123285
|
+
const state = schedulerState();
|
|
123286
|
+
const file = this.queries.loadFile(key);
|
|
123287
|
+
for (const job of file.jobs) {
|
|
123288
|
+
const cron = state.jobs.get(job.id);
|
|
123289
|
+
if (cron) {
|
|
123290
|
+
cron.stop();
|
|
123291
|
+
state.jobs.delete(job.id);
|
|
123292
|
+
log22.info({ jobId: job.id, key }, "stopped cronjob");
|
|
123100
123293
|
}
|
|
123101
123294
|
}
|
|
123102
|
-
return result;
|
|
123103
|
-
}
|
|
123104
|
-
/**
|
|
123105
|
-
* Returns the scrollback buffer for a terminal, optionally limited to the
|
|
123106
|
-
* last N lines. Returns `null` if the terminal is not found.
|
|
123107
|
-
*/
|
|
123108
|
-
getScrollback(terminalId, lines) {
|
|
123109
|
-
const session = this.terminals.get(terminalId);
|
|
123110
|
-
if (!session) return null;
|
|
123111
|
-
if (lines == null) return session.scrollback;
|
|
123112
|
-
const allLines = session.scrollback.split("\n");
|
|
123113
|
-
return allLines.slice(-lines).join("\n");
|
|
123114
123295
|
}
|
|
123296
|
+
// -------------------------------------------------------------------------
|
|
123297
|
+
// Shared resolution helpers (used by both the API + the scheduled fire path)
|
|
123298
|
+
// -------------------------------------------------------------------------
|
|
123115
123299
|
/**
|
|
123116
|
-
*
|
|
123117
|
-
*
|
|
123300
|
+
* Find — or create — the dedicated chat this cronjob writes into.
|
|
123301
|
+
*
|
|
123302
|
+
* Issue #520: each cronjob owns its own chat in the workspace, looked up
|
|
123303
|
+
* by the canonical `band:cronId` label. First call creates the chat;
|
|
123304
|
+
* subsequent calls reuse it. If the user deletes the chat, the next call
|
|
123305
|
+
* recreates it (intentional soft reset). This replaces the old
|
|
123306
|
+
* `getOrCreateDefaultChat` dispatch, which silently latched onto whatever
|
|
123307
|
+
* chat the user happened to be focused on. Shared between the scheduled
|
|
123308
|
+
* fire path (`executeCronjob` below) and the manual `trigger` route so
|
|
123309
|
+
* both stay aligned.
|
|
123118
123310
|
*/
|
|
123119
|
-
|
|
123120
|
-
const
|
|
123121
|
-
|
|
123122
|
-
|
|
123123
|
-
return
|
|
123311
|
+
getOrCreateCronjobChat(workspaceId, job) {
|
|
123312
|
+
const labelMatch = { [BAND_CRON_ID_LABEL]: job.id };
|
|
123313
|
+
const existing = chatService.findByLabels(workspaceId, labelMatch);
|
|
123314
|
+
if (existing) return existing;
|
|
123315
|
+
return chatService.create(workspaceId, {
|
|
123316
|
+
name: job.name,
|
|
123317
|
+
labels: labelMatch,
|
|
123318
|
+
// Cronjob scheduler is a trusted server-side caller — it's allowed
|
|
123319
|
+
// to write the reserved `band:` prefix that user-facing surfaces
|
|
123320
|
+
// are blocked from touching.
|
|
123321
|
+
allowReservedLabels: true
|
|
123322
|
+
});
|
|
123124
123323
|
}
|
|
123125
123324
|
/**
|
|
123126
|
-
*
|
|
123127
|
-
*
|
|
123325
|
+
* Dispatch a `via="terminal"` fire: spawn the agent's *headless* vendor CLI
|
|
123326
|
+
* in a fresh, self-closing PTY pane. Mirrors the `via=terminal` branch of
|
|
123327
|
+
* `WorkspaceService.create` (#551), but adapted for a recurring cron in two
|
|
123328
|
+
* important ways:
|
|
123329
|
+
*
|
|
123330
|
+
* - **Headless, not interactive.** We use `adapter.cliHeadlessInvocation`
|
|
123331
|
+
* (`claude -p`, `codex exec`, …), NOT the interactive `cliInvocation`
|
|
123332
|
+
* that workspace-create uses. The interactive REPL never exits on its
|
|
123333
|
+
* own, so the pane would stay parked after the turn — the shell's `exit`
|
|
123334
|
+
* would never run and the overlap guard below would treat "idle at the
|
|
123335
|
+
* REPL" as "still running", blocking every subsequent tick (issue #581
|
|
123336
|
+
* follow-up). The headless form runs the prompt and exits, which is what
|
|
123337
|
+
* makes both the self-close and the overlap guard actually work.
|
|
123338
|
+
* - **Overlap guard.** If the terminal spawned by this job's previous fire
|
|
123339
|
+
* is still alive, throw `TaskConflictError` instead of launching a second
|
|
123340
|
+
* agent — the caller records "skipped" (scheduled) or surfaces 409
|
|
123341
|
+
* (manual trigger), exactly like the chat running-task conflict. Because
|
|
123342
|
+
* the headless process exits when the turn completes, a live PTY reliably
|
|
123343
|
+
* means the previous run is genuinely still working.
|
|
123344
|
+
* - **Self-closing pane.** The command is `<headless-cli>\nexit`, staged
|
|
123345
|
+
* and sourced by the pool via `.`, so the shell exits the moment the
|
|
123346
|
+
* headless CLI returns. Combined with `cleanupOnExit`, the finished pane
|
|
123347
|
+
* removes itself from the layout and emits `terminal-killed` — bounding a
|
|
123348
|
+
* frequent cron to ~1 pane and keeping the next tick's liveness check
|
|
123349
|
+
* accurate.
|
|
123350
|
+
*
|
|
123351
|
+
* Returns `{ terminalId: null }` when the agent exposes no headless CLI (or
|
|
123352
|
+
* the workspace can't be resolved), signalling the caller to fall back to
|
|
123353
|
+
* chat. Spawn failures propagate (recorded as "failed").
|
|
123354
|
+
*
|
|
123355
|
+
* Edge case: if `terminalService.spawn` throws *after* the PTY is forked but
|
|
123356
|
+
* before `updateLastTerminal` runs, `lastTerminalId` stays unset for this
|
|
123357
|
+
* fire, so the next tick's overlap guard won't see that (now likely dead)
|
|
123358
|
+
* PTY. This is acceptable — a spawn that rejects has almost certainly failed
|
|
123359
|
+
* to produce a usable pane, and the synchronous in-process lock above still
|
|
123360
|
+
* prevents a *concurrent* double-spawn; only a subsequent, well-separated
|
|
123361
|
+
* tick could add a second pane, which is the same at-most-transient
|
|
123362
|
+
* duplication the self-close bound already tolerates.
|
|
123128
123363
|
*/
|
|
123129
|
-
|
|
123130
|
-
|
|
123131
|
-
if (
|
|
123132
|
-
|
|
123133
|
-
this.outputListeners.set(terminalId, listeners3);
|
|
123364
|
+
async spawnCronTerminal(job, workspaceId, fileKey) {
|
|
123365
|
+
const state = schedulerState();
|
|
123366
|
+
if (state.spawningTerminals.has(job.id)) {
|
|
123367
|
+
throw new TaskConflictError(`Cronjob ${job.id} terminal run is already being spawned`);
|
|
123134
123368
|
}
|
|
123135
|
-
|
|
123136
|
-
|
|
123137
|
-
|
|
123138
|
-
if (
|
|
123139
|
-
|
|
123369
|
+
state.spawningTerminals.add(job.id);
|
|
123370
|
+
try {
|
|
123371
|
+
const workspace = workspaceService.resolve(workspaceId);
|
|
123372
|
+
if (!workspace) {
|
|
123373
|
+
log22.warn(
|
|
123374
|
+
{ jobId: job.id, workspaceId },
|
|
123375
|
+
"workspace not resolvable for via=terminal cronjob; falling back to chat"
|
|
123376
|
+
);
|
|
123377
|
+
return { terminalId: null };
|
|
123140
123378
|
}
|
|
123141
|
-
|
|
123142
|
-
|
|
123143
|
-
|
|
123144
|
-
* Kill a single terminal by terminalId.
|
|
123145
|
-
*/
|
|
123146
|
-
kill(terminalId) {
|
|
123147
|
-
const session = this.terminals.get(terminalId);
|
|
123148
|
-
if (session) {
|
|
123149
|
-
session.pty.kill();
|
|
123150
|
-
this.terminals.delete(terminalId);
|
|
123151
|
-
const set = this.workspaceTerminals.get(session.workspaceId);
|
|
123152
|
-
if (set) {
|
|
123153
|
-
set.delete(terminalId);
|
|
123154
|
-
if (set.size === 0) {
|
|
123155
|
-
this.workspaceTerminals.delete(session.workspaceId);
|
|
123156
|
-
}
|
|
123379
|
+
const prevTerminalId = this.findJob(fileKey, job.id)?.lastTerminalId;
|
|
123380
|
+
if (prevTerminalId && terminalService.getSession(prevTerminalId)) {
|
|
123381
|
+
throw new TaskConflictError(`Cronjob ${job.id} terminal run is still in progress`);
|
|
123157
123382
|
}
|
|
123383
|
+
const adapter = await agentService.createWorkspaceAgent(workspace.worktree.path);
|
|
123384
|
+
const invocation = adapter.cliHeadlessInvocation?.(job.prompt);
|
|
123385
|
+
adapter.abort?.();
|
|
123386
|
+
if (!invocation || invocation.unsupported) {
|
|
123387
|
+
const reason = invocation?.reason ?? "adapter does not expose cliHeadlessInvocation";
|
|
123388
|
+
log22.warn(
|
|
123389
|
+
{ jobId: job.id, workspaceId, reason },
|
|
123390
|
+
"via=terminal requested but agent has no headless CLI; falling back to chat"
|
|
123391
|
+
);
|
|
123392
|
+
return { terminalId: null };
|
|
123393
|
+
}
|
|
123394
|
+
const command = `${formatShellCommand(invocation.command, invocation.args)}
|
|
123395
|
+
exit`;
|
|
123396
|
+
const terminalId = randomUUID3();
|
|
123397
|
+
await terminalService.spawn(workspaceId, terminalId, { command }, { cleanupOnExit: true });
|
|
123398
|
+
this.queries.updateLastTerminal(job.id, terminalId);
|
|
123399
|
+
emit({ kind: "terminal-created", workspaceId, terminalId });
|
|
123400
|
+
return { terminalId };
|
|
123401
|
+
} finally {
|
|
123402
|
+
state.spawningTerminals.delete(job.id);
|
|
123158
123403
|
}
|
|
123159
123404
|
}
|
|
123160
123405
|
/**
|
|
123161
|
-
*
|
|
123406
|
+
* Resolve the workspace the cronjob fires into.
|
|
123407
|
+
*
|
|
123408
|
+
* Returns `null` for project-scoped jobs whose project no longer exists
|
|
123409
|
+
* (which the scheduled fire path treats as a soft failure — see
|
|
123410
|
+
* `executeCronjob`). The manual trigger route maps the same condition to
|
|
123411
|
+
* a `CronjobProjectNotFoundError`.
|
|
123412
|
+
*
|
|
123413
|
+
* Workspace-scoped jobs return `null` rather than falling through to the
|
|
123414
|
+
* project-resolution branch when `workspaceId` is somehow missing. The
|
|
123415
|
+
* `create` path already rejects this via `CronjobWorkspaceMissingError`,
|
|
123416
|
+
* but a workspace-scoped row written by an older server version could
|
|
123417
|
+
* theoretically reach here with a null `workspaceId`. Falling through
|
|
123418
|
+
* would silently misfire into the project's default branch; returning
|
|
123419
|
+
* `null` makes the failure explicit (logged + `lastRunStatus = "failed"`
|
|
123420
|
+
* for the scheduler; `CronjobProjectNotFoundError` for the manual
|
|
123421
|
+
* trigger path).
|
|
123162
123422
|
*/
|
|
123163
|
-
|
|
123164
|
-
|
|
123165
|
-
|
|
123166
|
-
for (const terminalId of ids) {
|
|
123167
|
-
const session = this.terminals.get(terminalId);
|
|
123168
|
-
if (session) {
|
|
123169
|
-
session.pty.kill();
|
|
123170
|
-
this.terminals.delete(terminalId);
|
|
123171
|
-
}
|
|
123423
|
+
resolveWorkspaceId(job, fileKey) {
|
|
123424
|
+
if (job.scope === "workspace") {
|
|
123425
|
+
return job.workspaceId ?? null;
|
|
123172
123426
|
}
|
|
123173
|
-
|
|
123427
|
+
const appState = loadState();
|
|
123428
|
+
const project = appState.projects.find((p6) => p6.name === fileKey);
|
|
123429
|
+
if (!project) return null;
|
|
123430
|
+
return toWorkspaceId(project.name, project.defaultBranch);
|
|
123174
123431
|
}
|
|
123175
|
-
|
|
123176
|
-
|
|
123177
|
-
|
|
123178
|
-
|
|
123179
|
-
|
|
123180
|
-
|
|
123432
|
+
// -------------------------------------------------------------------------
|
|
123433
|
+
// Internals
|
|
123434
|
+
// -------------------------------------------------------------------------
|
|
123435
|
+
findJob(key, id) {
|
|
123436
|
+
const file = this.queries.loadFile(key);
|
|
123437
|
+
return file.jobs.find((j) => j.id === id);
|
|
123438
|
+
}
|
|
123439
|
+
assertValidCron(expression) {
|
|
123440
|
+
try {
|
|
123441
|
+
const options2 = { maxRuns: 0 };
|
|
123442
|
+
void new E2(expression, options2);
|
|
123443
|
+
} catch {
|
|
123444
|
+
throw new InvalidCronExpressionError();
|
|
123181
123445
|
}
|
|
123182
|
-
this.terminals.clear();
|
|
123183
|
-
this.workspaceTerminals.clear();
|
|
123184
123446
|
}
|
|
123185
|
-
|
|
123186
|
-
|
|
123187
|
-
|
|
123188
|
-
|
|
123189
|
-
|
|
123190
|
-
|
|
123191
|
-
|
|
123192
|
-
|
|
123193
|
-
|
|
123194
|
-
|
|
123195
|
-
|
|
123196
|
-
|
|
123197
|
-
|
|
123198
|
-
|
|
123199
|
-
|
|
123200
|
-
|
|
123201
|
-
|
|
123202
|
-
|
|
123203
|
-
|
|
123204
|
-
|
|
123447
|
+
scheduleJob(job, fileKey) {
|
|
123448
|
+
const state = schedulerState();
|
|
123449
|
+
const existing = state.jobs.get(job.id);
|
|
123450
|
+
if (existing) {
|
|
123451
|
+
existing.stop();
|
|
123452
|
+
state.jobs.delete(job.id);
|
|
123453
|
+
}
|
|
123454
|
+
if (!job.enabled) return;
|
|
123455
|
+
try {
|
|
123456
|
+
const cronInstance = new E2(job.cronExpression, () => {
|
|
123457
|
+
this.executeCronjob(job, fileKey).catch((err) => {
|
|
123458
|
+
log22.error({ jobId: job.id, err }, "unhandled error in cronjob execution");
|
|
123459
|
+
});
|
|
123460
|
+
});
|
|
123461
|
+
state.jobs.set(job.id, cronInstance);
|
|
123462
|
+
log22.info(
|
|
123463
|
+
{ jobId: job.id, name: job.name, cron: job.cronExpression, scope: job.scope },
|
|
123464
|
+
"scheduled cronjob"
|
|
123465
|
+
);
|
|
123466
|
+
} catch (err) {
|
|
123467
|
+
log22.error(
|
|
123468
|
+
{ jobId: job.id, cronExpression: job.cronExpression, err },
|
|
123469
|
+
"invalid cron expression, skipping job"
|
|
123470
|
+
);
|
|
123471
|
+
}
|
|
123472
|
+
}
|
|
123473
|
+
async executeCronjob(job, fileKey) {
|
|
123474
|
+
const workspaceId = this.resolveWorkspaceId(job, fileKey);
|
|
123475
|
+
if (!workspaceId) {
|
|
123476
|
+
log22.warn({ jobId: job.id, fileKey }, "project not found for cronjob, skipping");
|
|
123477
|
+
this.safeUpdateLastRun(job.id, "failed");
|
|
123478
|
+
return;
|
|
123479
|
+
}
|
|
123480
|
+
log22.info({ jobId: job.id, name: job.name, workspaceId }, "executing cronjob");
|
|
123481
|
+
if (job.via === "terminal") {
|
|
123482
|
+
try {
|
|
123483
|
+
const { terminalId } = await this.spawnCronTerminal(job, workspaceId, fileKey);
|
|
123484
|
+
if (terminalId) {
|
|
123485
|
+
log22.info({ jobId: job.id, terminalId, workspaceId }, "cronjob terminal spawned");
|
|
123486
|
+
this.safeUpdateLastRun(job.id, "completed");
|
|
123487
|
+
return;
|
|
123488
|
+
}
|
|
123489
|
+
log22.info(
|
|
123490
|
+
{ jobId: job.id, workspaceId },
|
|
123491
|
+
"via=terminal not supported by agent; falling back to chat dispatch"
|
|
123492
|
+
);
|
|
123493
|
+
} catch (err) {
|
|
123494
|
+
if (err instanceof TaskConflictError) {
|
|
123495
|
+
log22.info(
|
|
123496
|
+
{ jobId: job.id, workspaceId },
|
|
123497
|
+
"previous terminal run still active, skipping cronjob execution"
|
|
123498
|
+
);
|
|
123499
|
+
this.safeUpdateLastRun(job.id, "skipped");
|
|
123500
|
+
return;
|
|
123501
|
+
}
|
|
123502
|
+
log22.error({ jobId: job.id, err }, "cronjob terminal dispatch failed");
|
|
123503
|
+
this.safeUpdateLastRun(job.id, "failed");
|
|
123504
|
+
return;
|
|
123505
|
+
}
|
|
123506
|
+
}
|
|
123507
|
+
try {
|
|
123508
|
+
const chat = this.getOrCreateCronjobChat(workspaceId, job);
|
|
123509
|
+
const task = taskService.submitTask({ workspaceId, chatId: chat.id, prompt: job.prompt });
|
|
123510
|
+
log22.info(
|
|
123511
|
+
{ jobId: job.id, taskId: task.id, workspaceId, chatId: chat.id },
|
|
123512
|
+
"cronjob task submitted"
|
|
123513
|
+
);
|
|
123514
|
+
this.safeUpdateLastRun(job.id, "completed");
|
|
123515
|
+
} catch (err) {
|
|
123516
|
+
if (err instanceof TaskConflictError) {
|
|
123517
|
+
log22.info(
|
|
123518
|
+
{ jobId: job.id, workspaceId },
|
|
123519
|
+
"task already running, skipping cronjob execution"
|
|
123520
|
+
);
|
|
123521
|
+
this.safeUpdateLastRun(job.id, "skipped");
|
|
123522
|
+
return;
|
|
123523
|
+
}
|
|
123524
|
+
log22.error({ jobId: job.id, err }, "cronjob execution failed");
|
|
123525
|
+
this.safeUpdateLastRun(job.id, "failed");
|
|
123205
123526
|
}
|
|
123206
|
-
});
|
|
123207
|
-
}
|
|
123208
|
-
function removeTerminalFromLayout(workspaceId, terminalId) {
|
|
123209
|
-
manager.removePanel(workspaceId, terminalId);
|
|
123210
|
-
}
|
|
123211
|
-
|
|
123212
|
-
// src/server/services/terminal-service.ts
|
|
123213
|
-
var log23 = createLogger("terminal-service");
|
|
123214
|
-
var TerminalPaneConfigSchema = external_exports.object({
|
|
123215
|
-
name: external_exports.string().optional(),
|
|
123216
|
-
command: external_exports.string().optional(),
|
|
123217
|
-
cwd: external_exports.string().optional(),
|
|
123218
|
-
env: external_exports.record(external_exports.string()).optional(),
|
|
123219
|
-
focus: external_exports.boolean().optional()
|
|
123220
|
-
});
|
|
123221
|
-
var PaneNodeSchema = external_exports.object({
|
|
123222
|
-
pane: TerminalPaneConfigSchema
|
|
123223
|
-
});
|
|
123224
|
-
var TerminalLayoutNodeSchema = external_exports.lazy(
|
|
123225
|
-
() => external_exports.union([
|
|
123226
|
-
PaneNodeSchema,
|
|
123227
|
-
external_exports.object({
|
|
123228
|
-
direction: external_exports.enum(["horizontal", "vertical"]),
|
|
123229
|
-
split: external_exports.number().min(0.1).max(0.9).optional().default(0.5),
|
|
123230
|
-
children: external_exports.tuple([TerminalLayoutNodeSchema, TerminalLayoutNodeSchema])
|
|
123231
|
-
})
|
|
123232
|
-
])
|
|
123233
|
-
);
|
|
123234
|
-
var WorkspaceTerminalConfigSchema = external_exports.object({
|
|
123235
|
-
layout: TerminalLayoutNodeSchema
|
|
123236
|
-
});
|
|
123237
|
-
var TerminalService = class {
|
|
123238
|
-
constructor(pool2 = terminalPool) {
|
|
123239
|
-
this.pool = pool2;
|
|
123240
123527
|
}
|
|
123241
|
-
// -------------------------------------------------------------------------
|
|
123242
|
-
// PTY lifecycle
|
|
123243
|
-
// -------------------------------------------------------------------------
|
|
123244
123528
|
/**
|
|
123245
|
-
*
|
|
123529
|
+
* Update `lastRunAt`/`lastRunStatus`, swallowing DB errors.
|
|
123246
123530
|
*
|
|
123247
|
-
*
|
|
123248
|
-
*
|
|
123249
|
-
*
|
|
123250
|
-
* `
|
|
123251
|
-
* the API entry points decide whether to broadcast (the WebSocket handler
|
|
123252
|
-
* stays silent; the tRPC `create` mutation emits explicitly).
|
|
123531
|
+
* Errors here are diagnostic noise during the fire path — the cronjob
|
|
123532
|
+
* already executed (or failed/skipped), and we don't want a transient
|
|
123533
|
+
* DB lock to mask the real outcome by re-raising. The legacy
|
|
123534
|
+
* implementation logged at `warn` and moved on; preserved here.
|
|
123253
123535
|
*/
|
|
123254
|
-
|
|
123255
|
-
|
|
123256
|
-
|
|
123257
|
-
|
|
123536
|
+
safeUpdateLastRun(jobId, status) {
|
|
123537
|
+
try {
|
|
123538
|
+
this.queries.updateLastRun(jobId, status);
|
|
123539
|
+
} catch (err) {
|
|
123540
|
+
log22.warn({ jobId, err }, "failed to update lastRun on cronjob");
|
|
123258
123541
|
}
|
|
123259
|
-
const session = await this.pool.spawn(
|
|
123260
|
-
workspaceId,
|
|
123261
|
-
terminalId,
|
|
123262
|
-
workspace.worktree.path,
|
|
123263
|
-
options2
|
|
123264
|
-
);
|
|
123265
|
-
addTerminalToLayout(workspaceId, terminalId, {
|
|
123266
|
-
command: options2?.command,
|
|
123267
|
-
cwd: options2?.cwd,
|
|
123268
|
-
env: options2?.env
|
|
123269
|
-
});
|
|
123270
|
-
return session;
|
|
123271
123542
|
}
|
|
123272
|
-
|
|
123273
|
-
|
|
123274
|
-
|
|
123275
|
-
|
|
123276
|
-
|
|
123277
|
-
|
|
123278
|
-
const
|
|
123279
|
-
|
|
123280
|
-
this.pool.kill(terminalId);
|
|
123281
|
-
if (workspaceId) {
|
|
123282
|
-
removeTerminalFromLayout(workspaceId, terminalId);
|
|
123283
|
-
emit({ kind: "terminal-killed", workspaceId, terminalId });
|
|
123543
|
+
loadAndScheduleAll() {
|
|
123544
|
+
const state = schedulerState();
|
|
123545
|
+
for (const [, cron] of state.jobs) {
|
|
123546
|
+
cron.stop();
|
|
123547
|
+
}
|
|
123548
|
+
state.jobs.clear();
|
|
123549
|
+
for (const job of this.queries.listAll()) {
|
|
123550
|
+
this.scheduleJob(job, job.fileKey);
|
|
123284
123551
|
}
|
|
123552
|
+
log22.info({ count: state.jobs.size }, "loaded cronjob schedules");
|
|
123285
123553
|
}
|
|
123554
|
+
};
|
|
123555
|
+
var cronjobService = new CronjobService();
|
|
123556
|
+
|
|
123557
|
+
// src/server/services/panel-focus-service.ts
|
|
123558
|
+
init_src();
|
|
123559
|
+
var log23 = createLogger("panel-focus-service");
|
|
123560
|
+
var PANEL_FOCUS_TYPE = "panel_focus";
|
|
123561
|
+
function focusRowId(workspaceId) {
|
|
123562
|
+
return `${PANEL_FOCUS_TYPE}:${workspaceId}`;
|
|
123563
|
+
}
|
|
123564
|
+
var PanelFocusService = class {
|
|
123565
|
+
/** workspaceId → last-focused panel ids. */
|
|
123566
|
+
focus = /* @__PURE__ */ new Map();
|
|
123286
123567
|
/**
|
|
123287
|
-
*
|
|
123288
|
-
*
|
|
123289
|
-
*
|
|
123568
|
+
* Workspaces whose focus row has been loaded from the DB into `focus`.
|
|
123569
|
+
* Separate from the map's own key presence because a workspace can have a
|
|
123570
|
+
* loaded-but-empty record (row absent) — we still don't want to re-hit the
|
|
123571
|
+
* DB on every read.
|
|
123290
123572
|
*/
|
|
123291
|
-
|
|
123292
|
-
|
|
123293
|
-
|
|
123573
|
+
hydrated = /* @__PURE__ */ new Set();
|
|
123574
|
+
/** Workspaces that already have a `panel_states` row (drives insert vs update). */
|
|
123575
|
+
persisted = /* @__PURE__ */ new Set();
|
|
123294
123576
|
/**
|
|
123295
|
-
*
|
|
123296
|
-
*
|
|
123577
|
+
* Load a workspace's focus row from the DB on first access. Cheap and
|
|
123578
|
+
* idempotent — subsequent calls short-circuit on the `hydrated` set.
|
|
123297
123579
|
*/
|
|
123298
|
-
|
|
123299
|
-
this.
|
|
123300
|
-
|
|
123301
|
-
|
|
123302
|
-
|
|
123303
|
-
|
|
123304
|
-
|
|
123305
|
-
|
|
123306
|
-
|
|
123307
|
-
|
|
123308
|
-
|
|
123309
|
-
|
|
123310
|
-
|
|
123311
|
-
|
|
123312
|
-
|
|
123313
|
-
|
|
123314
|
-
|
|
123315
|
-
}
|
|
123316
|
-
resize(terminalId, cols, rows) {
|
|
123317
|
-
this.pool.resize(terminalId, cols, rows);
|
|
123318
|
-
}
|
|
123319
|
-
subscribeOutput(terminalId, callback) {
|
|
123320
|
-
return this.pool.subscribeOutput(terminalId, callback);
|
|
123321
|
-
}
|
|
123322
|
-
// -------------------------------------------------------------------------
|
|
123323
|
-
// Layout persistence (dockview tree)
|
|
123324
|
-
//
|
|
123325
|
-
// Thin pass-throughs to `lib/terminal-layout-manager` so the API tier
|
|
123326
|
-
// doesn't reach across into `lib/` directly. The layout helpers will be
|
|
123327
|
-
// folded into a dedicated infra adapter in a follow-up phase (when the
|
|
123328
|
-
// chats/browsers domains get their own infra adapters); leaving them in
|
|
123329
|
-
// `lib/` for now is the minimum-disruption shape during the phased
|
|
123330
|
-
// migration.
|
|
123331
|
-
// -------------------------------------------------------------------------
|
|
123332
|
-
getLayout(workspaceId) {
|
|
123333
|
-
return getTerminalLayout(workspaceId);
|
|
123334
|
-
}
|
|
123335
|
-
saveLayout(workspaceId, tree) {
|
|
123336
|
-
saveTerminalLayout(workspaceId, tree);
|
|
123580
|
+
ensureHydrated(workspaceId) {
|
|
123581
|
+
if (this.hydrated.has(workspaceId)) return;
|
|
123582
|
+
this.hydrated.add(workspaceId);
|
|
123583
|
+
const rows = listPanelStatesForWorkspace(workspaceId, PANEL_FOCUS_TYPE);
|
|
123584
|
+
const row = rows[0];
|
|
123585
|
+
if (!row) return;
|
|
123586
|
+
this.persisted.add(workspaceId);
|
|
123587
|
+
try {
|
|
123588
|
+
const parsed = JSON.parse(row.state);
|
|
123589
|
+
this.focus.set(workspaceId, {
|
|
123590
|
+
chat: parsed.chat,
|
|
123591
|
+
terminal: parsed.terminal,
|
|
123592
|
+
browser: parsed.browser
|
|
123593
|
+
});
|
|
123594
|
+
} catch (err) {
|
|
123595
|
+
log23.warn({ workspaceId, err }, "failed to parse persisted panel focus; ignoring");
|
|
123596
|
+
}
|
|
123337
123597
|
}
|
|
123338
|
-
|
|
123339
|
-
|
|
123598
|
+
/** Get the last-focused panel ids for a workspace. */
|
|
123599
|
+
get(workspaceId) {
|
|
123600
|
+
this.ensureHydrated(workspaceId);
|
|
123601
|
+
return { ...this.focus.get(workspaceId) ?? {} };
|
|
123340
123602
|
}
|
|
123341
|
-
// -------------------------------------------------------------------------
|
|
123342
|
-
// Workspace `.band/config.json` `workspace.terminal` block
|
|
123343
|
-
// -------------------------------------------------------------------------
|
|
123344
123603
|
/**
|
|
123345
|
-
*
|
|
123346
|
-
* `.
|
|
123347
|
-
*
|
|
123348
|
-
* payload fails validation.
|
|
123349
|
-
*
|
|
123350
|
-
* Absorbed from the old `lib/terminal-config.ts:loadWorkspaceTerminalConfig`
|
|
123351
|
-
* — same parsing semantics, but the lookup now goes through the service
|
|
123352
|
-
* tier so callers (currently the workspace router's `getTerminalConfig`
|
|
123353
|
-
* query) reach it via `terminalService` instead of a stand-alone helper.
|
|
123604
|
+
* Record that `panelId` is the last-focused panel of `panelType` in
|
|
123605
|
+
* `workspaceId`. Write-through: updates the in-memory map and upserts the
|
|
123606
|
+
* backing `panel_states` row.
|
|
123354
123607
|
*/
|
|
123355
|
-
|
|
123356
|
-
|
|
123357
|
-
|
|
123358
|
-
|
|
123608
|
+
set(workspaceId, panelType, panelId) {
|
|
123609
|
+
this.ensureHydrated(workspaceId);
|
|
123610
|
+
const current2 = this.focus.get(workspaceId) ?? {};
|
|
123611
|
+
if (current2[panelType] === panelId) return;
|
|
123612
|
+
const next = { ...current2, [panelType]: panelId };
|
|
123613
|
+
this.focus.set(workspaceId, next);
|
|
123614
|
+
const now = Date.now();
|
|
123615
|
+
const state = JSON.stringify(next);
|
|
123616
|
+
if (this.persisted.has(workspaceId)) {
|
|
123617
|
+
updatePanelState(focusRowId(workspaceId), { state, updatedAt: now });
|
|
123618
|
+
} else {
|
|
123619
|
+
insertPanelState({
|
|
123620
|
+
id: focusRowId(workspaceId),
|
|
123621
|
+
workspaceId,
|
|
123622
|
+
panelType: PANEL_FOCUS_TYPE,
|
|
123623
|
+
state,
|
|
123624
|
+
createdAt: now,
|
|
123625
|
+
updatedAt: now
|
|
123626
|
+
});
|
|
123627
|
+
this.persisted.add(workspaceId);
|
|
123628
|
+
}
|
|
123359
123629
|
}
|
|
123360
123630
|
/**
|
|
123361
|
-
*
|
|
123362
|
-
*
|
|
123631
|
+
* Drop all focus tracking for a workspace. Called from the workspace delete
|
|
123632
|
+
* path so the row doesn't outlive the workspace.
|
|
123363
123633
|
*/
|
|
123364
|
-
|
|
123365
|
-
|
|
123366
|
-
|
|
123367
|
-
|
|
123368
|
-
|
|
123369
|
-
const result = WorkspaceTerminalConfigSchema.safeParse(terminalBlock);
|
|
123370
|
-
if (!result.success) {
|
|
123371
|
-
log23.warn(
|
|
123372
|
-
"Invalid workspace.terminal config: %s",
|
|
123373
|
-
result.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`).join("; ")
|
|
123374
|
-
);
|
|
123375
|
-
return null;
|
|
123376
|
-
}
|
|
123377
|
-
return result.data;
|
|
123634
|
+
remove(workspaceId) {
|
|
123635
|
+
this.focus.delete(workspaceId);
|
|
123636
|
+
this.hydrated.delete(workspaceId);
|
|
123637
|
+
this.persisted.delete(workspaceId);
|
|
123638
|
+
deletePanelState(focusRowId(workspaceId));
|
|
123378
123639
|
}
|
|
123379
123640
|
};
|
|
123380
|
-
var
|
|
123641
|
+
var panelFocusService = new PanelFocusService();
|
|
123381
123642
|
|
|
123382
123643
|
// src/server/services/workspace-service.ts
|
|
123383
123644
|
var execFileAsync2 = promisify2(execFile6);
|
|
@@ -123599,7 +123860,7 @@ var WorkspaceService = class {
|
|
|
123599
123860
|
via = "chat";
|
|
123600
123861
|
} else {
|
|
123601
123862
|
terminalCommand = formatShellCommand(invocation.command, invocation.args);
|
|
123602
|
-
terminalId =
|
|
123863
|
+
terminalId = randomUUID4();
|
|
123603
123864
|
}
|
|
123604
123865
|
} catch (err) {
|
|
123605
123866
|
log24.warn(
|
|
@@ -123688,7 +123949,7 @@ var WorkspaceService = class {
|
|
|
123688
123949
|
};
|
|
123689
123950
|
}
|
|
123690
123951
|
const command = formatShellCommand(invocation.command, invocation.args);
|
|
123691
|
-
const terminalId =
|
|
123952
|
+
const terminalId = randomUUID4();
|
|
123692
123953
|
await terminalService.spawn(chat.workspaceId, terminalId, { command });
|
|
123693
123954
|
emit({ kind: "terminal-created", workspaceId: chat.workspaceId, terminalId });
|
|
123694
123955
|
log24.info(
|
|
@@ -137681,9 +137942,9 @@ var cronjobsRouter = t2.router({
|
|
|
137681
137942
|
throwAsTRPCError(err);
|
|
137682
137943
|
}
|
|
137683
137944
|
}),
|
|
137684
|
-
trigger: publicProcedure.input(cronjobByIdInput).mutation(({ input }) => {
|
|
137945
|
+
trigger: publicProcedure.input(cronjobByIdInput).mutation(async ({ input }) => {
|
|
137685
137946
|
try {
|
|
137686
|
-
return cronjobService.trigger(input.key, input.id);
|
|
137947
|
+
return await cronjobService.trigger(input.key, input.id);
|
|
137687
137948
|
} catch (err) {
|
|
137688
137949
|
throwAsTRPCError(err);
|
|
137689
137950
|
}
|
|
@@ -137697,10 +137958,7 @@ function throwAsTRPCError(err) {
|
|
|
137697
137958
|
throw new TRPCError({ code: "BAD_REQUEST", message: err.message });
|
|
137698
137959
|
}
|
|
137699
137960
|
if (err instanceof TaskConflictError) {
|
|
137700
|
-
throw new TRPCError({
|
|
137701
|
-
code: "CONFLICT",
|
|
137702
|
-
message: "Task already running for this chat pane"
|
|
137703
|
-
});
|
|
137961
|
+
throw new TRPCError({ code: "CONFLICT", message: err.message });
|
|
137704
137962
|
}
|
|
137705
137963
|
throw err;
|
|
137706
137964
|
}
|
|
@@ -140470,7 +140728,7 @@ function throwAsTrpcError2(err) {
|
|
|
140470
140728
|
}
|
|
140471
140729
|
|
|
140472
140730
|
// src/server/api/terminals/router.ts
|
|
140473
|
-
import { randomUUID as
|
|
140731
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
140474
140732
|
init_zod();
|
|
140475
140733
|
|
|
140476
140734
|
// src/server/api/terminals/strip-queries.ts
|
|
@@ -140496,7 +140754,7 @@ var terminalRouter = t2.router({
|
|
|
140496
140754
|
env: external_exports.record(external_exports.string()).optional()
|
|
140497
140755
|
})
|
|
140498
140756
|
).mutation(async ({ input }) => {
|
|
140499
|
-
const terminalId = input.id ??
|
|
140757
|
+
const terminalId = input.id ?? randomUUID5();
|
|
140500
140758
|
const session = await terminalService.spawn(input.workspaceId, terminalId, {
|
|
140501
140759
|
command: input.command,
|
|
140502
140760
|
cwd: input.cwd,
|