mulmoterminal 4.0.0 → 4.1.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/README.md +14 -7
- package/bin/mulmoterminal.js +9 -0
- package/common/dirConfigSource.ts +1 -1
- package/common/issueStartPlan.ts +58 -0
- package/common/keymap.ts +8 -8
- package/common/prPhase.ts +19 -2
- package/common/repoEntry.ts +77 -0
- package/common/sessionAgent.ts +1 -1
- package/common/terminalSubmit.ts +1 -1
- package/common/themeVars.ts +14 -6
- package/common/voiceInputStatus.ts +14 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-Ds795mMl.js → abnfDiagram-VRR7QNED-RjiYivmv-CuUAI5MR.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-B89nYL-T.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-C_pKLVRy.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-rRDT09Vx.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-C1XW9rk5.js → blockDiagram-677ZJIJ3-BPuAJQRW-By0PkzIO.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-B_DJUxYS.js → c4Diagram-LMCZKHZV-C0LAqQso-GgOhwf63.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-DLHsiYx_.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-muO9l3Wd.js → chunk-32BRIVSS-BRrYpgtb-C_AxGcxt.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-2G1qxJCe.js → chunk-52WLFC77-BJ-ss3Xr-DNopp04b.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-DcP1Ik_Y.js → chunk-C7G6YPKG-BZEucKEL---6fNwPJ.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-DzGx10EI.js → chunk-EX3LRPZG-DLS6FBN1-D5lN82UI.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-C_1-WiA5.js → chunk-FWX5IMBZ-DHLSFw1H-DKxptXUj.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-CNQLLKZo.js → chunk-HOUHSVGY-Bhlt8hXJ-CAh9s_YU.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-zUyYvMip.js → chunk-ICXQ74PX-DwgHBX_g-jwQc10EC.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-CuddDdck.js → chunk-MOJQB5TN-CMZRaeqt-DcPt_trW.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-OusgHU3N.js → chunk-OGEWGWER-8Qy4a8b5-BuRF4rGR.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-CbEu-YVk.js → chunk-PUDLZKDR-DcrWQRYh-BYgad2gt.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-C8R8rf8U.js → chunk-Q4XR5HBZ-ZXVGkG8Z-DhnrB6RT.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-CAvev2P-.js → chunk-V7JOEXUC-DmGdheTX-Ur1KzthC.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-6Fqcztnb.js → chunk-VAUOI2AC-CS9QJ4yz-BZe0Q9XX.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-Oo6w4umS.js → chunk-VR4S4FIN-C6a91eNY-2iUlb5mQ.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-CIUpBpsn.js → chunk-WYO6CB5R-BlzOfotS-CeZxJxL0.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-DJKhMGKs.js → chunk-ZGVPDNZ5-BYwxNFTK-Ct0mEJmO.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-vnBEnnpL.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-vnBEnnpL.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-U6pbCywv.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--D510DZwZ.js → cynefinDiagram-TSTJHNR4-DHA9iPo--BISNJm8X.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-BqyoygxX.js → dagre-VKFMJZFB--oJKqXBZ-B459NNxB.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-DfnckLd4.js → diagram-FQU43EPY-CcJCB9bG-DVyaiIsO.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-BPt0rgWP.js → diagram-G47NLZAW-C_o-WGG1-BhuGPhW4.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--CkJBzBK2.js → diagram-NH7WQ7WH-CXJCYvY--DzbRZHC1.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-COSn3f12.js → diagram-OA4YK3LP-BMzeJ87A-D8t4UJW_.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-Dp8Om4pW.js → diagram-WEI45ONY-D_93NKqo-Bbl2-CHc.js} +1 -1
- package/dist/assets/{dist-gls9ajYD.js → dist-ClO6UEBJ.js} +1 -1
- package/dist/assets/{dist-DZD6s32q.js → dist-D8AEcEHx.js} +1 -1
- package/dist/assets/{dist-JstXoCzo.js → dist-Daghl_nN.js} +1 -1
- package/dist/assets/{dist-DiEF7DGo.js → dist-epQ42VP2.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-Di2Rnx6S.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-Bbg4X-2o.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-dgCq9gZ-.js → erDiagram-Q63AITRT-CiABnA0s-CKq_tipK.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-Dz8P7VVP.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-Bqi3MvHN.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-Cia0ajA1.js → ganttDiagram-NO4QXBWP-DQZvdFo1-jmx958Wo.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-C_bvrqXL.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-Dh1y0VXZ.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-DuZaMN46.js} +1 -1
- package/dist/assets/index-CXYjAh9r.css +1 -0
- package/dist/assets/index-D9sJGCR9.js +616 -0
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-aO_hAsb7.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-BqYiXSaj.js → infoDiagram-FWYZ7A6U-7UnoB5AP-C8ikfN1o.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-CcZgH7Ew.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-ChrD-r9s.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-DdArPeNL.js → journeyDiagram-5HDEW3XC-c5xIah9o-uY7L1btR.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-DmJ152Sf.js → kanban-definition-HUTT4EX6-Cnt6loYD-CNv5ziZo.js} +1 -1
- package/dist/assets/{lib-Db3KPEnF.js → lib-dCbEkBG7.js} +1 -1
- package/dist/assets/{line-D7ziSjKi-B3NE0st9.js → line-D7ziSjKi-BEYdEqj_.js} +1 -1
- package/dist/assets/{marp-SCu9aKhI.js → marp-CU_bIZEZ.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-nhwF9Zmw.js → mermaid-parser.core-DxEa8E3F-akMN_vOP.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-COnZ7RY6.js → mermaid.core-V0OYwIz3-BMPzsgXF.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-DAFbTZnf.js → mindmap-definition-LN4V7U3C-CexN3O6L-Bed_YDhf.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-jjlc375I.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-DVdp8F2g.js → pegDiagram-2B236MQR-BiM4G0cW-CBrM86uu.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-BGKp68D7.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-dOKbqFlT.js → pieDiagram-ENE6RG2P-B_fBBS-2-D-tRTGSr.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-B0K7Bi_G.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-CH-H4qGn.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-ChTyqNRH.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-BRepafjd.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-B2E_hGfU.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-C2k_5LtZ.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DSwMdPCz.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-IX0DQMzv.js → railroadDiagram-RFXS5EU6--lNIlG64-jEmJgaFj.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-7ePp-6uY.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-DurumWyC.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-DcPeO3wM.js → sankeyDiagram-HTMAVEWB-DlutaXDv-Doed1fks.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-BPKloVvU.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-CmkZuV9D.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-Bd6-ZMXE.js → stateDiagram-2N3HPSRC-jf_dXUEU-BFXZmQ_i.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CZroC0CQ.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-UC879FAu.js → swimlanes-5IMT3BWC-CYjtALQH-e_82UQvE.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-C2aatMMX.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-ByREbTkR.js → timeline-definition-FHXFAJF6-DYJ4oUm8-CO0rLezO.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DhW5sK_F.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-oZY-xCg-.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-CznsE58R.js → vennDiagram-L72KCM5P-CdKAHoek-BO9jvKIz.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-BCWqjXDb.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-MMgJtZmB.js → wardleyDiagram-EHGQE667-BF6c4_CW-eXxyipXb.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-D9mjkddh.js → xychartDiagram-FW5EYKEG-CGiKngj7-CMZLvO1e.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +5 -5
- package/server/agents/antigravity-mcp.ts +4 -3
- package/server/agents/antigravity-session.ts +3 -3
- package/server/agents/antigravity-sessions.ts +104 -0
- package/server/agents/codex-sessions.ts +7 -32
- package/server/agents/probe-stall.spec.ts +103 -0
- package/server/agents/probe-stall.ts +70 -0
- package/server/agents/probe-transcript.ts +8 -11
- package/server/agents/rate-limit-probe.spec.ts +90 -50
- package/server/agents/rate-limit-probe.ts +55 -33
- package/server/agents/rate-limit-routes.ts +13 -4
- package/server/agents/rate-limit-store.spec.ts +58 -4
- package/server/agents/rate-limit-store.ts +42 -5
- package/server/agents/transcript-head.ts +47 -0
- package/server/backends/collectionNotifierAdapter.ts +3 -2
- package/server/backends/collections.ts +23 -13
- package/server/backends/html.ts +11 -3
- package/server/backends/mulmoscript.ts +12 -2
- package/server/backends/openPath.ts +7 -0
- package/server/backends/presentPathRoot.ts +123 -0
- package/server/backends/remoteHost/collectionPage.ts +8 -9
- package/server/backends/remoteHost/getFeed.spec.ts +1 -0
- package/server/backends/remoteHost/handlers/deps.ts +9 -0
- package/server/backends/remoteHost/handlers/getRemoteViewItems.ts +5 -6
- package/server/backends/remoteHost/handlers/index.ts +5 -0
- package/server/backends/remoteHost/handlers/issueWork.ts +116 -0
- package/server/backends/remoteHost/handlers/mutateRemoteView.ts +3 -2
- package/server/backends/remoteHost/handlers/terminalSession.ts +3 -2
- package/server/backends/remoteHost/handlers.spec.ts +3 -0
- package/server/backends/remoteHost/index.ts +1 -0
- package/server/backends/remoteHost/issueWork.spec.ts +244 -0
- package/server/backends/remoteHost/jsonPayload.ts +55 -0
- package/server/backends/remoteHost/terminalScreen.ts +52 -6
- package/server/backends/remoteView.ts +3 -3
- package/server/backends/scheduler.ts +6 -3
- package/server/backends/shortcuts.ts +13 -9
- package/server/backends/translation.ts +12 -7
- package/server/backends/viewToken.ts +14 -8
- package/server/config/app-config.ts +12 -6
- package/server/config/cwd-presets.ts +4 -2
- package/server/config/header-context.ts +2 -14
- package/server/config/keymap-check.ts +2 -1
- package/server/config/update-status.ts +6 -1
- package/server/files/scripts.ts +2 -1
- package/server/git/forge-host.ts +80 -0
- package/server/git/forge-support.ts +73 -0
- package/server/git/gitRemote.ts +31 -25
- package/server/git/glab-items.ts +177 -0
- package/server/git/glab.ts +88 -0
- package/server/git/issue-work.ts +129 -13
- package/server/git/issues.ts +17 -5
- package/server/git/pr-for-branch.ts +3 -2
- package/server/git/prPhase.ts +61 -5
- package/server/git/prs.ts +17 -6
- package/server/git/repo-dirs.ts +2 -3
- package/server/git/work-comment.ts +61 -18
- package/server/git/worktree-pr.ts +99 -19
- package/server/git/worktrees.ts +17 -1
- package/server/index.ts +45 -15
- package/server/infra/collection-tool.ts +5 -1
- package/server/infra/gui-mcp-registration.ts +4 -4
- package/server/infra/plugins-registry.ts +5 -4
- package/server/infra/server-tool-load.ts +7 -2
- package/server/infra/spawnCapture.ts +7 -3
- package/server/infra/tmux.ts +10 -6
- package/server/mcp/broker.ts +8 -3
- package/server/routes/app-routes.ts +9 -0
- package/server/routes/dir-routes.ts +9 -6
- package/server/routes/issue-work-routes.ts +20 -17
- package/server/routes/session-routes.ts +22 -0
- package/server/routes/ws-routes.ts +57 -34
- package/server/session/background-chat.ts +2 -1
- package/server/session/cleared-transcripts.ts +1 -1
- package/server/session/draft-injection.ts +7 -30
- package/server/session/early-frames.ts +12 -8
- package/server/session/headlessScreen.ts +12 -6
- package/server/session/issue-spawn-options.ts +20 -0
- package/server/session/pty-scan.ts +37 -2
- package/server/session/registry.ts +12 -0
- package/server/session/scheduled-sessions.ts +1 -1
- package/server/session/session-cwd.ts +23 -0
- package/server/session/transcript.ts +4 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-_YNv0NMb.js +0 -1
- package/dist/assets/channel-7wUqSdoX-6DJ89-Qe.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-B85sB4KG.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-B85sB4KG.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-CVLC7hXj.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-D-U4pUhx.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-CeqmKjiQ.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-Dv8G0LNT.js +0 -1
- package/dist/assets/index-BBcSnsqR.js +0 -616
- package/dist/assets/index-Dal2P4Y6.css +0 -1
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-Gtb9rU5N.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-BlSkfnzl.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-PTCmAnlr.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-sOU_Qtf_.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-CZse0pRF.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-DD141lVl.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-6h3qaEy8.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-D3yOgOUU.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v--iFusbAN.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-B7GGyqBW.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-COPupObu.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DebmT2Ye.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-CqYCfIUl.js +0 -1
package/README.md
CHANGED
|
@@ -88,9 +88,9 @@ question — an amber dot; nothing proceeds until you answer) and which are **fi
|
|
|
88
88
|
you haven't seen** (a green dot) — driven by Claude/Codex activity hooks the server injects per
|
|
89
89
|
spawn. The horizontal tab bar carries the same two dots.
|
|
90
90
|
|
|
91
|
-

|
|
92
92
|
|
|
93
|
-
*To focus on one agent, **zoom its cell**: it takes the window, and
|
|
93
|
+
*To focus on one agent, **zoom its cell**: it takes the window, and the **GUI panel** ("Canvas") opens beside it, where that agent's tool calls render as documents, forms, charts, images, and HTML rather than printed text. **The app opens on the grid** (`/`, settling on `/terminals`), which is the only view; 3.x had a separate single view at `/chat` and 4.0.0 removed it, so that URL now lands on the grid like any other.*
|
|
94
94
|
|
|
95
95
|
**Inserting a file path** — like a native terminal, you can put a file's absolute path into
|
|
96
96
|
the prompt: **drag a file** onto the terminal, or click the **file button** in the terminal
|
|
@@ -204,11 +204,17 @@ more.
|
|
|
204
204
|
|
|
205
205
|
Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
|
|
206
206
|
|
|
207
|
+
> **Never installed any of this before?** The guide walks it end to end, macOS and Windows,
|
|
208
|
+
> assuming no command-line experience:
|
|
209
|
+
> [Getting started](https://receptron.github.io/mulmoterminal/guide/en/getting-started.html) ·
|
|
210
|
+
> [はじめに — 起動するまで](https://receptron.github.io/mulmoterminal/guide/ja/getting-started.html)
|
|
211
|
+
|
|
207
212
|
| | Tool | What it gives you | Install |
|
|
208
213
|
| --- | --- | --- | --- |
|
|
209
214
|
| **Required** | [`claude`](https://claude.com/claude-code) | every Claude session — this app is a cockpit for it | `npm i -g @anthropic-ai/claude-code`, then run `claude` once to log in |
|
|
210
215
|
| **Required** | `git` | [worktree isolation](#git-worktrees--pull-requests), each cell's branch / unsaved-dot / diff readout, the PR footer | `brew install git` · `sudo apt install git` · `sudo dnf install git` · Windows: [git-scm.com](https://git-scm.com/download/win) |
|
|
211
216
|
| **Required** | `gh` | the cross-repo **PRs & Issues** view and one-click PR creation — it uses your `gh` login, so no token is stored | [cli.github.com](https://cli.github.com), then `gh auth login` |
|
|
217
|
+
| Optional | `glab` | the same for **gitlab.com** projects (#981). Same arrangement: the CLI holds the credentials, this app stores no token | `brew install glab`, then `glab auth login` |
|
|
212
218
|
| Recommended | `tmux` | [session persistence](#session-persistence-tmux) — terminals survive a server restart | `brew install tmux` · `sudo apt install tmux` · `sudo dnf install tmux` · no native Windows build (falls back to plain PTYs) |
|
|
213
219
|
| Optional | `codex` | [Codex sessions](#agents-claude--codex) in a cell, alongside Claude | `npm i -g @openai/codex` |
|
|
214
220
|
| Optional | `ffmpeg` | video rendering from the [mulmo-script panel](#wiki-collections--the-gui-panel) (its plugin ships enabled) | `brew install ffmpeg` · `sudo apt install ffmpeg` · `sudo dnf install ffmpeg` |
|
|
@@ -536,7 +542,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
536
542
|
| `soundFile` | Absolute path to a custom **attention sound**, the fallback for every kind. Empty/unset uses the built-in synthesized chime. |
|
|
537
543
|
| `soundKinds` | Which moments beep — see [Notification sounds](#notification-sounds). Defaults to `["finished","waiting"]`; the other kinds are opt-in. |
|
|
538
544
|
| `sounds` | Per-kind sound: `{ "waiting": "preset:coin" }`. A `preset:<id>` reference or an absolute path; a kind with no entry falls back to `soundFile`. |
|
|
539
|
-
| `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates
|
|
545
|
+
| `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates, using whichever CLI the host needs — your own `gh` or `glab` login, so no token is stored here. An entry may name its host — `gitlab.com/group/project` is read with `glab`, and work can be started on it, commented on and turned into a merge request. A host that is neither shows a row saying so. |
|
|
540
546
|
| `repoDirs` | `{ "owner/repo": "/abs/path" }` — which local clone work on a repo starts in, when you keep several side by side. Only the *choice* is stored; which clones exist is re-derived from `cwdPresets` on every read, and an entry that no longer names a clone of that repo is ignored. |
|
|
541
547
|
| `launchers` | `{ label, command }` entries offered in a grid cell's launcher besides the agents — any interactive command. A plain shell needs no entry: the launch form's **Shell** toggle opens `$SHELL` unconfigured. |
|
|
542
548
|
| `quickCommands` | `{ label, text, agents? }` phrases the **phone** offers as chips on a session's terminal view. Tapping one puts `text` in the input box; it is not sent until you press send. `agents` (`"claude"` / `"codex"` / `"shell"`) scopes a chip to session kinds — omit it to offer the chip everywhere. Empty by default. |
|
|
@@ -962,7 +968,7 @@ Closing a worktree cell asks whether to **keep** the worktree or **discard & rem
|
|
|
962
968
|
|
|
963
969
|
**PRs & Issues (cross-repo).** The toolbar's **Pull requests** button opens a full-screen
|
|
964
970
|
view that aggregates open PRs **and** issues across the repos listed in Settings →
|
|
965
|
-
**Pull request repos** (`prRepos`, `owner/repo` entries) via your server-side `gh` login.
|
|
971
|
+
**Pull request repos** (`prRepos`, `owner/repo` entries, or `gitlab.com/group/project`) via your server-side `gh` / `glab` login.
|
|
966
972
|
PRs show a CI-rollup / review-decision / draft badge; each repo lists its latest open
|
|
967
973
|
issues. Rows are real links, per-repo errors don't sink the view, and the two lists load
|
|
968
974
|
independently. Backed by `GET /api/prs` and `GET /api/issues`.
|
|
@@ -1167,7 +1173,7 @@ quietly answering about the **default workspace** (#1151):
|
|
|
1167
1173
|
| --- | --- |
|
|
1168
1174
|
| `/ws`, `/ws/codex`, `/ws/antigravity`, `/ws/launch`, `/ws/run` | The socket is closed with `{ type: "error", message }`, which the terminal shows as a red banner and does not retry. |
|
|
1169
1175
|
| A session that is still running (`?session=` names a live PTY or a surviving tmux session) | **Attaches anyway**, with a warning in the server log. Moving or renaming a directory must not shut you out of an agent that is still working in it — and the cwd reported back comes from the running PTY, not from the request. |
|
|
1170
|
-
| `GET /api/scripts`, `/api/skills`, `/api/dir-config`, `/api/dir-sound`, `/api/git-status`, `/api/pr-phase`, `/api/header`, `/api/sessions`, `/api/codex/sessions`, `/api/session/:id`, `/api/transcript/*`, `/api/cost` | `404 { error, cwd }` — a directory that is not there. |
|
|
1176
|
+
| `GET /api/scripts`, `/api/skills`, `/api/dir-config`, `/api/dir-sound`, `/api/git-status`, `/api/pr-phase`, `/api/header`, `/api/sessions`, `/api/codex/sessions`, `/api/antigravity/sessions`, `/api/session/:id`, `/api/transcript/*`, `/api/cost` | `404 { error, cwd }` — a directory that is not there. |
|
|
1171
1177
|
| A `?cwd=` that cannot name a directory at all (relative, or repeated as `?cwd=a&cwd=b`) | `400 { error, cwd }`. |
|
|
1172
1178
|
|
|
1173
1179
|
A request that names **no** directory is unaffected: `CLAUDE_CWD` is then the answer it
|
|
@@ -1288,6 +1294,7 @@ same-origin-guarded.
|
|
|
1288
1294
|
| -------- | ------- |
|
|
1289
1295
|
| `GET /api/session/:id?cwd=` | One session's summary — cumulative `usage` and `context` (model + last-turn context tokens). Backs the cell token & ctx% badges. |
|
|
1290
1296
|
| `GET /api/codex/sessions?cwd=` | Codex sessions for the project (from `~/.codex` rollouts), newest first. |
|
|
1297
|
+
| `GET /api/antigravity/sessions?cwd=` | Antigravity conversations for the project, newest first. agy does record a workspace, but never as a complete conversation-to-workspace map (`cache/last_conversations.json` keeps one conversation per directory and is written at exit; `history.jsonl` carries no conversation id), so the project comes from MulmoTerminal's own `~/.mulmoterminal/antigravity-conversations.jsonl`; agy's transcript supplies the title. |
|
|
1291
1298
|
| `GET /api/cost?cwd=&session=` | Estimated $ cost — session / today / month. |
|
|
1292
1299
|
| `GET /api/transcript/timeline?session=&cwd=` | Per-session activity timeline (tools run). |
|
|
1293
1300
|
| `GET /api/transcript/last-turn?session=&cwd=&agent=` | A session's last completed exchange (`prompt`, `reply`) plus the `text` to paste into another terminal. `agent=codex` reads the codex rollout instead of the Claude transcript. |
|
|
@@ -1302,7 +1309,7 @@ same-origin-guarded.
|
|
|
1302
1309
|
| `POST /api/git-remote` | The dir's GitHub repo URL (for the header GitHub menu). |
|
|
1303
1310
|
| `GET /api/worktrees?cwd=` · `GET /api/worktrees/diff?cwd=` | List managed worktrees / diff one vs its base. |
|
|
1304
1311
|
| `POST /api/worktrees/create` · `/remove` · `/push` · `/pr` | Create on `agent/<slug>` — or, with `issue: <N>`, on `issue/<N>-<slug>` forked from a freshly fetched `origin/<base>`; remove (managed root only), push, open a PR (`gh`, else compare URL). |
|
|
1305
|
-
| `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos`
|
|
1312
|
+
| `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` — `gh` for github.com entries, `glab` for `gitlab.com/...` ones. |
|
|
1306
1313
|
| `GET /api/repo-dirs` | Which saved directories clone which GitHub repo, ordered, with the recorded choice per repo. |
|
|
1307
1314
|
| `POST /api/issues/start` | Cut an issue's worktree in one of that repo's known clones and spawn a session there, seeded with the issue as a draft. |
|
|
1308
1315
|
| `GET /api/github/star` · `POST /api/github/star` | Whether you have starred MulmoTerminal, and star it (via `gh`). `starred: null` means `gh` could not answer, and hides the button. |
|
|
@@ -1651,7 +1658,7 @@ server/
|
|
|
1651
1658
|
session/ per-session transcript/activity/cost: transcript.ts,
|
|
1652
1659
|
session-resolve.ts, activity-*.ts, cost.ts,
|
|
1653
1660
|
command-summary.ts, terminal-replay.ts, file-cache.ts
|
|
1654
|
-
git/ git
|
|
1661
|
+
git/ git, GitHub (gh) and GitLab (glab) + worktrees: git-status.ts, gitRemote.ts,
|
|
1655
1662
|
gh.ts, prs.ts, issues.ts, pr-for-branch.ts, worktrees.ts, worktree-*.ts
|
|
1656
1663
|
files/ files-browse.ts (contained tree read/write), pick-file.ts,
|
|
1657
1664
|
open-dir.ts, scripts.ts (Run-menu script.json loader)
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -85,6 +85,15 @@ function claudeInstalled() {
|
|
|
85
85
|
const PATH_TOOLS = [
|
|
86
86
|
{ cmd: "git", versionArg: "--version", required: true, why: "worktrees, per-cell branch/diff, PR footer", hint: "brew install git · apt install git" },
|
|
87
87
|
{ cmd: "gh", versionArg: "--version", required: true, why: "PRs & Issues view + one-click PRs", hint: "https://cli.github.com (then: gh auth login)" },
|
|
88
|
+
// Optional rather than a second required tool: github.com is what the app is built around, and a
|
|
89
|
+
// user with no GitLab project should not be told they are missing something (#981).
|
|
90
|
+
{
|
|
91
|
+
cmd: "glab",
|
|
92
|
+
versionArg: "--version",
|
|
93
|
+
required: false,
|
|
94
|
+
why: "the PRs & Issues view for gitlab.com projects",
|
|
95
|
+
hint: "brew install glab (then: glab auth login)",
|
|
96
|
+
},
|
|
88
97
|
{ cmd: "tmux", versionArg: "-V", required: false, why: "sessions survive a restart", hint: "brew install tmux · apt install tmux" },
|
|
89
98
|
{ cmd: "codex", versionArg: "--version", required: false, why: "run OpenAI Codex as an agent", hint: "npm install -g @openai/codex" },
|
|
90
99
|
{
|
|
@@ -82,7 +82,7 @@ export const EMPTY_DIR_CONFIG_EXTRAS: DirConfigExtras = {
|
|
|
82
82
|
function isEmptyValue(value: unknown): boolean {
|
|
83
83
|
if (value === null || value === undefined) return true;
|
|
84
84
|
if (Array.isArray(value)) return value.length === 0;
|
|
85
|
-
if (typeof value === "object") return Object.keys(value
|
|
85
|
+
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
86
86
|
return false;
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Whether work on one repo can start here, decided from the repo -> clone answer alone (#1173).
|
|
2
|
+
// Pure, because the three outcomes are the whole behaviour: on the desktop they are a button, a
|
|
3
|
+
// menu, or a disabled button that says why (IssueStartButton.vue).
|
|
4
|
+
//
|
|
5
|
+
// In `common/` because BOTH hosts decide from it: the phone asks the same question over the remote
|
|
6
|
+
// command channel (#1184) and must reach the same answer — with one difference it makes itself,
|
|
7
|
+
// not one encoded here. It cannot offer the menu (the phone never picks a directory, see
|
|
8
|
+
// docs/remote-host-protocol.md), so it refuses `choose` where the desktop opens it.
|
|
9
|
+
import type { RepoDirs } from "./repoDirs";
|
|
10
|
+
import { GITHUB_HOST, GITLAB_HOST, parseRepoEntry } from "./repoEntry";
|
|
11
|
+
|
|
12
|
+
// Where work can be STARTED — the issue is read and a worktree cut. A host that is merely listable
|
|
13
|
+
// still refuses, and says which host it is rather than blaming a missing clone (#981).
|
|
14
|
+
const STARTABLE_HOSTS: ReadonlySet<string> = new Set([GITHUB_HOST, GITLAB_HOST]);
|
|
15
|
+
|
|
16
|
+
/** The hosts named in a refusal, read off the set rather than written out beside it — a hardcoded
|
|
17
|
+
* "github.com only" survived GitLab becoming startable and told users the opposite of the truth
|
|
18
|
+
* (Codex review). Exported so the phone's refusal says the same thing. */
|
|
19
|
+
export const startableHosts = (): string => [...STARTABLE_HOSTS].join(" and ");
|
|
20
|
+
|
|
21
|
+
export type IssueStartPlan =
|
|
22
|
+
/** No clone of this repo on this machine, so there is nowhere for the work to happen. */
|
|
23
|
+
| { kind: "no-clone" }
|
|
24
|
+
/** The repo is on a forge whose issues can be LISTED but not started from (#981). Distinct from
|
|
25
|
+
* `no-clone`, which would send the reader off to add a clone that would not help. */
|
|
26
|
+
| { kind: "unsupported-forge"; host: string }
|
|
27
|
+
/** Exactly one answer — either recorded, or the only candidate. One click starts. */
|
|
28
|
+
| { kind: "ready"; dir: string }
|
|
29
|
+
/** Several clones and nothing recorded yet: the user picks, and the pick is remembered. */
|
|
30
|
+
| { kind: "choose"; dirs: RepoDirs["dirs"] };
|
|
31
|
+
|
|
32
|
+
/** A plan that cannot start work as it stands. Named so a caller that has already narrowed to it
|
|
33
|
+
* can be given a sentence rather than a nullable one — see the phone's refusal (#1184). */
|
|
34
|
+
export type BlockedIssueStartPlan = Exclude<IssueStartPlan, { kind: "ready" }>;
|
|
35
|
+
|
|
36
|
+
// `repo` is REQUIRED, not optional, even though only one branch reads it: a caller that omitted it
|
|
37
|
+
// would get `no-clone` for a GitLab repo, which is the exact wrong answer this parameter was added
|
|
38
|
+
// to fix. Making the compiler ask for it is what stops that coming back.
|
|
39
|
+
export function issueStartPlan(entry: RepoDirs | undefined, repo: string): IssueStartPlan {
|
|
40
|
+
// Checked before the clone list, because a GitLab repo has no entry there and would otherwise
|
|
41
|
+
// read as "no clone" — a reason that is not true and points at the wrong fix.
|
|
42
|
+
const parsed = parseRepoEntry(repo);
|
|
43
|
+
if (parsed && !STARTABLE_HOSTS.has(parsed.host)) return { kind: "unsupported-forge", host: parsed.host };
|
|
44
|
+
const dirs = entry?.dirs ?? [];
|
|
45
|
+
if (dirs.length === 0) return { kind: "no-clone" };
|
|
46
|
+
// A recording wins even when the repo has one clone: it is still the same answer, and treating
|
|
47
|
+
// the two cases the same keeps "recorded" from being a state the UI has to distinguish.
|
|
48
|
+
const recorded = entry?.primary;
|
|
49
|
+
if (recorded) return { kind: "ready", dir: recorded };
|
|
50
|
+
if (dirs.length === 1) return { kind: "ready", dir: dirs[0].path };
|
|
51
|
+
return { kind: "choose", dirs };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Why the control is disabled, for the row's title text. Null when it is not disabled. */
|
|
55
|
+
export function issueStartBlockedReason(plan: IssueStartPlan, repo: string): string | null {
|
|
56
|
+
if (plan.kind === "unsupported-forge") return `Starting work supports ${startableHosts()} — ${plan.host} issues are listed here, not started from`;
|
|
57
|
+
return plan.kind === "no-clone" ? `No local clone of ${repo} — add one to your directory presets to start work here` : null;
|
|
58
|
+
}
|
package/common/keymap.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// "keymap": { "zoom-next": "PageDown", "zoom-prev": "Shift+PageUp" }
|
|
11
11
|
|
|
12
12
|
// Actions a key can be bound to. Adding one here is all it takes for the config to accept it.
|
|
13
|
+
import { isRecord } from "./isRecord.js";
|
|
14
|
+
|
|
13
15
|
export const KEYMAP_ACTIONS = [
|
|
14
16
|
"zoom-toggle",
|
|
15
17
|
"zoom-next",
|
|
@@ -23,7 +25,7 @@ export const KEYMAP_ACTIONS = [
|
|
|
23
25
|
] as const;
|
|
24
26
|
export type KeymapAction = (typeof KEYMAP_ACTIONS)[number];
|
|
25
27
|
|
|
26
|
-
export const isKeymapAction = (value: unknown): value is KeymapAction => typeof value === "string" && (
|
|
28
|
+
export const isKeymapAction = (value: unknown): value is KeymapAction => typeof value === "string" && KEYMAP_ACTIONS.some((action) => action === value);
|
|
27
29
|
|
|
28
30
|
// action -> binding string. Absent action = unbound = that shortcut does nothing.
|
|
29
31
|
// Actions the GRID's key handler must never claim, because they are decided inside the terminal
|
|
@@ -122,8 +124,7 @@ export function actionForKey(keymap: Keymap, e: KeymapKeyEvent): KeymapAction |
|
|
|
122
124
|
return null;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
const isSendBinding = (value: unknown): value is SendBinding =>
|
|
126
|
-
typeof value === "object" && value !== null && typeof (value as SendBinding).key === "string" && typeof (value as SendBinding).bytes === "string";
|
|
127
|
+
const isSendBinding = (value: unknown): value is SendBinding => isRecord(value) && typeof value.key === "string" && typeof value.bytes === "string";
|
|
127
128
|
|
|
128
129
|
// The bytes this keydown should put into the terminal, or null when it is not a send binding.
|
|
129
130
|
//
|
|
@@ -160,7 +161,7 @@ export function validateKeymap(input: unknown): KeymapProblem[] {
|
|
|
160
161
|
if (typeof input !== "object" || Array.isArray(input)) {
|
|
161
162
|
return [{ action: "keymap", binding: input, reason: "`keymap` must be an object of action -> key binding", fatal: true }];
|
|
162
163
|
}
|
|
163
|
-
const entries = Object.entries(input
|
|
164
|
+
const entries = Object.entries(input);
|
|
164
165
|
// A claim on one keystroke. `rank` is DISPATCH order, so the winner can be named: every action
|
|
165
166
|
// outranks every send binding, because the grid's handler runs in the capture phase and stops
|
|
166
167
|
// the event before the terminal — see sendBytesFor.
|
|
@@ -241,12 +242,11 @@ const canonicalBinding = (b: KeyBinding): string => `${b.shift ? "S" : ""}${b.al
|
|
|
241
242
|
// malformed bindings are dropped rather than rejecting the whole map, matching how the
|
|
242
243
|
// rest of the config treats one bad entry.
|
|
243
244
|
export function sanitizeKeymap(input: unknown): Keymap {
|
|
244
|
-
if (
|
|
245
|
-
const
|
|
246
|
-
const entries = Object.entries(raw).filter(
|
|
245
|
+
if (!isRecord(input)) return {};
|
|
246
|
+
const entries = Object.entries(input).filter(
|
|
247
247
|
(entry): entry is [KeymapAction, string] => isKeymapAction(entry[0]) && typeof entry[1] === "string" && parseKeyBinding(entry[1]) !== null,
|
|
248
248
|
);
|
|
249
|
-
const send = sanitizeSendBindings(
|
|
249
|
+
const send = sanitizeSendBindings(input.send);
|
|
250
250
|
return { ...Object.fromEntries(entries), ...(send.length ? { send } : {}) };
|
|
251
251
|
}
|
|
252
252
|
|
package/common/prPhase.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type PrPhase = "none" | "draft" | "ci-failing" | "changes-requested" | "c
|
|
|
10
10
|
|
|
11
11
|
export const PR_PHASES: readonly PrPhase[] = ["none", "draft", "ci-failing", "changes-requested", "ci-running", "ready", "merged", "closed"];
|
|
12
12
|
|
|
13
|
-
export const isPrPhase = (v: unknown): v is PrPhase => typeof v === "string" && (
|
|
13
|
+
export const isPrPhase = (v: unknown): v is PrPhase => typeof v === "string" && PR_PHASES.some((phase) => phase === v);
|
|
14
14
|
|
|
15
15
|
// The /api/pr-phase response. `phase` and `prUrl` predate the rest and the roster reads them;
|
|
16
16
|
// the numbers are what lets a cell say WHICH work it is on rather than just how far along it is.
|
|
@@ -24,9 +24,26 @@ export interface WorkItem {
|
|
|
24
24
|
// list above all, where "#987" alone says nothing about which request is running (#1014).
|
|
25
25
|
prTitle: string | null;
|
|
26
26
|
issueTitle: string | null;
|
|
27
|
+
// WHY a request that is open and not obviously failing still cannot merge. GitLab collapses into
|
|
28
|
+
// one `detailed_merge_status` what GitHub splits across `isDraft`, `reviewDecision` and the check
|
|
29
|
+
// rollup, and three of its values — approvals outstanding, unresolved discussions, blocked by
|
|
30
|
+
// another request — have no home in `PrPhase`. Forcing them into `ready` would call something
|
|
31
|
+
// unmergeable ready, and into `changes-requested` would invent a review nobody left.
|
|
32
|
+
//
|
|
33
|
+
// Always null on GitHub, whose phases say everything there is to say (#981).
|
|
34
|
+
blockedReason: string | null;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
export const EMPTY_WORK_ITEM: Readonly<WorkItem> = {
|
|
37
|
+
export const EMPTY_WORK_ITEM: Readonly<WorkItem> = {
|
|
38
|
+
phase: "none",
|
|
39
|
+
pr: null,
|
|
40
|
+
prUrl: null,
|
|
41
|
+
issue: null,
|
|
42
|
+
issueUrl: null,
|
|
43
|
+
prTitle: null,
|
|
44
|
+
issueTitle: null,
|
|
45
|
+
blockedReason: null,
|
|
46
|
+
};
|
|
30
47
|
|
|
31
48
|
// One line for a surface with room for one: what the work is FOR beats what was done about it, so
|
|
32
49
|
// the issue's title wins when there is one. Null when neither side has a title to show.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Reading a configured repository entry — `owner/repo`, or `host/owner/repo` for one that is not
|
|
2
|
+
// on GitHub (#981).
|
|
3
|
+
//
|
|
4
|
+
// In `common/` because BOTH sides decide from it: the server turns an entry into a forge to know
|
|
5
|
+
// which CLI to run, and the browser needs the same answer to say why a control is off. Written
|
|
6
|
+
// twice it would drift, and the two answers disagreeing is precisely the failure this abstraction
|
|
7
|
+
// is undoing.
|
|
8
|
+
|
|
9
|
+
export const GITHUB_HOST = "github.com";
|
|
10
|
+
export const GITLAB_HOST = "gitlab.com";
|
|
11
|
+
|
|
12
|
+
// A leading segment containing a DOT is the host. GitHub user and organisation names may hold only
|
|
13
|
+
// alphanumerics and hyphens, so `owner/repo` and `host/owner/repo` cannot be confused.
|
|
14
|
+
const HOST_SEGMENT = /\./;
|
|
15
|
+
|
|
16
|
+
export interface RepoEntry {
|
|
17
|
+
host: string;
|
|
18
|
+
/** The segments after the host — how many of them name a project is the HOST's rule, not this. */
|
|
19
|
+
path: string[];
|
|
20
|
+
/** True when the entry spelled the host out. `owner/repo` implies github.com without saying it. */
|
|
21
|
+
declared: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Split an entry into host and path, or null when it is not one.
|
|
25
|
+
*
|
|
26
|
+
* Null for an empty segment — a doubled, leading or trailing slash. Forgiving those would let
|
|
27
|
+
* `owner//repo` parse as `owner/repo` while the entry is STORED verbatim and handed to a CLI with
|
|
28
|
+
* the extra slash still in it, which is a parse error there.
|
|
29
|
+
*/
|
|
30
|
+
export function parseRepoEntry(entry: string): RepoEntry | null {
|
|
31
|
+
const segments = entry.trim().split("/");
|
|
32
|
+
if (segments.some((segment) => segment === "")) return null;
|
|
33
|
+
const declared = segments.length > 1 && HOST_SEGMENT.test(segments[0]);
|
|
34
|
+
return declared ? { host: segments[0].toLowerCase(), path: segments.slice(1), declared } : { host: GITHUB_HOST, path: segments, declared };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The identity a repository is known by ON ITS OWN HOST — `owner/repo` for GitHub, the group path
|
|
38
|
+
* for GitLab — with any declared host stripped.
|
|
39
|
+
*
|
|
40
|
+
* `/api/repo-dirs` keys by this, because it derives the name from a clone's remote URL where the
|
|
41
|
+
* host is not part of the path. A configured entry may spell the host out (`github.com/owner/repo`
|
|
42
|
+
* became storable in #981 step 2a), so comparing the raw entry against those keys finds nothing
|
|
43
|
+
* and reports "no local clone" for a repo that has one (Codex review).
|
|
44
|
+
*/
|
|
45
|
+
export const canonicalRepo = (entry: string): string => parseRepoEntry(entry)?.path.join("/") ?? entry.trim();
|
|
46
|
+
|
|
47
|
+
/** The identity used to MATCH a configured entry against a resolved clone: always host-qualified.
|
|
48
|
+
*
|
|
49
|
+
* Distinct from `canonicalRepo`, which strips the host because that is what a CLI's `--repo`
|
|
50
|
+
* wants. Matching needs the opposite: without the host, `github.com/a/b` and `gitlab.com/a/b`
|
|
51
|
+
* collapse to the same key, and a clone of one would answer for the other (#981).
|
|
52
|
+
*/
|
|
53
|
+
export const repoIdentity = (entry: string): string => {
|
|
54
|
+
const parsed = parseRepoEntry(entry);
|
|
55
|
+
return parsed ? `${parsed.host}/${parsed.path.join("/")}`.toLowerCase() : entry.trim().toLowerCase();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// The value reaches a CLI's `--repo`, so no spaces and nothing readable as a flag.
|
|
59
|
+
const REPO_CHARS_RE = /^[A-Za-z0-9._/-]+$/;
|
|
60
|
+
// Every forge names a project as namespace + name, so one segment is never a repository.
|
|
61
|
+
const NAMESPACED = 2;
|
|
62
|
+
|
|
63
|
+
/** Whether a string may be stored and used as a repository entry.
|
|
64
|
+
*
|
|
65
|
+
* The ONE rule. It was written out four times — the config sanitizer, both issue-start endpoints
|
|
66
|
+
* and the Settings field — and widening only the first meant an entry the user could save was
|
|
67
|
+
* rejected by everything downstream, including the form that was supposed to accept it (#981).
|
|
68
|
+
*
|
|
69
|
+
* A hostless entry must be exactly `owner/repo`: `gh --repo` reads `a/b/c` as host `a`, so
|
|
70
|
+
* accepting it would have this side and the CLI aiming at different servers. A declared host may
|
|
71
|
+
* be followed by a nested GitLab group.
|
|
72
|
+
*/
|
|
73
|
+
export function isRepoEntry(entry: string): boolean {
|
|
74
|
+
const parsed = REPO_CHARS_RE.test(entry.trim()) ? parseRepoEntry(entry) : null;
|
|
75
|
+
if (!parsed) return false;
|
|
76
|
+
return parsed.declared ? parsed.path.length >= NAMESPACED : parsed.path.length === NAMESPACED;
|
|
77
|
+
}
|
package/common/sessionAgent.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type TerminalAgent = (typeof TERMINAL_AGENTS)[number];
|
|
|
16
16
|
// A remembered agent — a persisted grid cell, a localStorage toggle, a query param — read back.
|
|
17
17
|
// Validated rather than cast: anything unrecognised is Claude, which is also what an older
|
|
18
18
|
// persisted cell (written before the field existed) means.
|
|
19
|
-
export const asTerminalAgent = (value: unknown): TerminalAgent => (
|
|
19
|
+
export const asTerminalAgent = (value: unknown): TerminalAgent => (typeof value === "string" && isTerminalAgent(value) ? value : "claude");
|
|
20
20
|
|
|
21
21
|
// Narrowing rather than coercing, for the callers that must be able to say "this is not an agent
|
|
22
22
|
// at all": a shell is a session kind but not something a cell can be relaunched AS, and reading
|
package/common/terminalSubmit.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type TerminalSubmitMode = (typeof TERMINAL_SUBMIT_MODES)[number];
|
|
|
13
13
|
export const DEFAULT_TERMINAL_SUBMIT_MODE: TerminalSubmitMode = "cr";
|
|
14
14
|
|
|
15
15
|
export const isTerminalSubmitMode = (value: unknown): value is TerminalSubmitMode =>
|
|
16
|
-
typeof value === "string" && (
|
|
16
|
+
typeof value === "string" && TERMINAL_SUBMIT_MODES.some((mode) => mode === value);
|
|
17
17
|
|
|
18
18
|
// CR on its own. ESC+CR is what a terminal emits for Alt/Meta+Enter.
|
|
19
19
|
const CR = "\r";
|
package/common/themeVars.ts
CHANGED
|
@@ -50,7 +50,7 @@ export function isThemeIdLike(value: unknown): value is string {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function isBuiltinThemeId(id: string): id is ThemeId {
|
|
53
|
-
return (
|
|
53
|
+
return THEME_IDS.some((builtin) => builtin === id);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Whether an id may be used for a CUSTOM theme. Built-in ids are refused rather than merged
|
|
@@ -69,14 +69,22 @@ export interface CustomThemeInput {
|
|
|
69
69
|
|
|
70
70
|
/** The full variable set for a theme: the base it extends, with its own colours on top.
|
|
71
71
|
* `builtins` supplies the base sets so this stays pure — the client reads them from the
|
|
72
|
-
* stylesheet's source of truth, the specs from a fixture.
|
|
73
|
-
|
|
72
|
+
* stylesheet's source of truth, the specs from a fixture. PARTIAL because a base can genuinely be
|
|
73
|
+
* missing (a stylesheet read that found no block for that id), and the `?? {}` below has always
|
|
74
|
+
* handled that — the map only ever needed one key, never all of them. */
|
|
75
|
+
// Every variable present, which is what separates a set we can paint with from one we cannot.
|
|
76
|
+
// A guard rather than a length check on the missing keys: both ask the same question, but only
|
|
77
|
+
// this one hands the answer to the compiler.
|
|
78
|
+
function isCompleteThemeVars(vars: Partial<ThemeVars>): vars is ThemeVars {
|
|
79
|
+
return THEME_VAR_KEYS.every((key) => !!vars[key]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveThemeVars(theme: CustomThemeInput, builtins: Partial<Record<ThemeId, ThemeVars>>): ThemeVars | null {
|
|
74
83
|
const base = theme.extends ? builtins[theme.extends] : null;
|
|
75
|
-
const merged = { ...(base ?? {}), ...theme.colors }
|
|
76
|
-
const missing = THEME_VAR_KEYS.filter((key) => !merged[key]);
|
|
84
|
+
const merged: Partial<ThemeVars> = { ...(base ?? {}), ...theme.colors };
|
|
77
85
|
// No base and an incomplete list is the one case we cannot paint: report it rather than
|
|
78
86
|
// leaving the gaps to whatever the previously applied theme put on the element.
|
|
79
|
-
return
|
|
87
|
+
return isCompleteThemeVars(merged) ? merged : null;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
function channel(hex: string, at: number): number {
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// package implements transcription behind it is not the UI's business. The server's
|
|
7
7
|
// `getVoiceInputStatus(): VoiceInputStatus` annotation is what keeps the two in step.
|
|
8
8
|
|
|
9
|
+
import { isRecord } from "./isRecord.js";
|
|
10
|
+
|
|
9
11
|
export type VoiceModelState = "idle" | "downloading" | "ready" | "error";
|
|
10
12
|
|
|
11
13
|
export interface VoiceInputStatus {
|
|
@@ -19,3 +21,15 @@ export interface VoiceInputStatus {
|
|
|
19
21
|
error?: string;
|
|
20
22
|
};
|
|
21
23
|
}
|
|
24
|
+
|
|
25
|
+
export const VOICE_MODEL_STATES: readonly VoiceModelState[] = ["idle", "downloading", "ready", "error"];
|
|
26
|
+
|
|
27
|
+
/** The wire response as it arrives from `GET /api/transcribe/model`. Lives beside the type so the
|
|
28
|
+
* guard and the shape it checks cannot drift; the UI treats anything else as "no status". */
|
|
29
|
+
export function isVoiceInputStatus(value: unknown): value is VoiceInputStatus {
|
|
30
|
+
if (!isRecord(value) || typeof value.capable !== "boolean" || !isRecord(value.model)) return false;
|
|
31
|
+
const { name, state, progress, error } = value.model;
|
|
32
|
+
if (typeof name !== "string" || !VOICE_MODEL_STATES.some((known) => known === state)) return false;
|
|
33
|
+
if (progress !== undefined && typeof progress !== "number") return false;
|
|
34
|
+
return error === undefined || typeof error === "string";
|
|
35
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DxEa8E3F-
|
|
1
|
+
import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DxEa8E3F-akMN_vOP.js";import"./chunk-WYO6CB5R-BlzOfotS-CeZxJxL0.js";import"./chunk-VAUOI2AC-CS9QJ4yz-BZe0Q9XX.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-DcPt_trW.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{f as e}from"./mermaid-parser.core-DxEa8E3F-akMN_vOP.js";export{e as createArchitectureServices};
|