mulmoterminal 2.9.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +49 -1
  2. package/common/dirPriorityOrder.ts +24 -0
  3. package/common/prPhase.ts +38 -5
  4. package/common/repoDirs.ts +57 -0
  5. package/common/terminalSize.ts +30 -0
  6. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-FRF8kLeT.js → abnfDiagram-VRR7QNED-RjiYivmv-D3Xi4MgH.js} +1 -1
  7. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-C59c2mZk.js +1 -0
  8. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-D1pPQlOf.js → architectureDiagram-ZJ3FMSHR-3nWA91tG--Hhwls1P.js} +1 -1
  9. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-d5-ljc-N.js → blockDiagram-677ZJIJ3-BPuAJQRW-jbpnOYNC.js} +1 -1
  10. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-BGlEwCQt.js → c4Diagram-LMCZKHZV-C0LAqQso-BehZz0B8.js} +1 -1
  11. package/dist/assets/channel-7wUqSdoX-BQFzwCdQ.js +1 -0
  12. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-SXiKhjnH.js → chunk-32BRIVSS-BRrYpgtb-CW1MLM9R.js} +1 -1
  13. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-DKigFAu-.js → chunk-52WLFC77-BJ-ss3Xr-zXVxTZL9.js} +1 -1
  14. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-Cb5wXOXv.js → chunk-C7G6YPKG-BZEucKEL-rXDRBOEz.js} +1 -1
  15. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-CzTNcARU.js → chunk-EX3LRPZG-DLS6FBN1-B_CwjJ-P.js} +1 -1
  16. package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-BZvgg4po.js → chunk-FWX5IMBZ-DHLSFw1H-CaXZLVSZ.js} +2 -2
  17. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-DL5Y_3yg.js → chunk-HOUHSVGY-Bhlt8hXJ-4fqsPAd7.js} +1 -1
  18. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-BDvSo2KT.js → chunk-ICXQ74PX-DwgHBX_g-Dg1-0lTm.js} +1 -1
  19. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-BPbi0qBZ.js → chunk-MOJQB5TN-CMZRaeqt-EDD8-kXb.js} +1 -1
  20. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-BmITYHk7.js → chunk-OGEWGWER-8Qy4a8b5-CDKuaxoY.js} +1 -1
  21. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-CXK8yMcj.js → chunk-PUDLZKDR-DcrWQRYh-CCXDsG2_.js} +1 -1
  22. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-SCGIXxMq.js → chunk-Q4XR5HBZ-ZXVGkG8Z-DGcK-cBH.js} +1 -1
  23. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-gZgY507i.js → chunk-V7JOEXUC-DmGdheTX-Goxxrrjk.js} +1 -1
  24. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-BQv37MCq.js → chunk-VAUOI2AC-CS9QJ4yz-Oly0QbKc.js} +1 -1
  25. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-ZNRLU9H7.js → chunk-VR4S4FIN-C6a91eNY-DujIpVG1.js} +1 -1
  26. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-Bx9N57Se.js → chunk-WYO6CB5R-BlzOfotS-BJCy4_YQ.js} +1 -1
  27. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-Bg7w-yJo.js → chunk-ZGVPDNZ5-BYwxNFTK-DFr162Oj.js} +1 -1
  28. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-DpE_W69U.js +1 -0
  29. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-DpE_W69U.js +1 -0
  30. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-CDiQ6X3-.js +1 -0
  31. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--abCAh98z.js → cynefinDiagram-TSTJHNR4-DHA9iPo--YZDZk8o5.js} +1 -1
  32. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-CA9PU3rn.js → dagre-VKFMJZFB--oJKqXBZ-BE00-MPM.js} +1 -1
  33. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-BgmurMGW.js → diagram-FQU43EPY-CcJCB9bG-DH6VIEkg.js} +1 -1
  34. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-CZ6ymH57.js → diagram-G47NLZAW-C_o-WGG1-D6LkeFBZ.js} +1 -1
  35. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--5Ej1o0wj.js → diagram-NH7WQ7WH-CXJCYvY--CLWYWMmK.js} +1 -1
  36. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-Br_Yz55A.js → diagram-OA4YK3LP-BMzeJ87A-BbfOUERw.js} +1 -1
  37. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-C2UMN3qi.js → diagram-WEI45ONY-D_93NKqo-Cc3W59e4.js} +1 -1
  38. package/dist/assets/{dist-DOaExQFB.js → dist-BGPxCEcj.js} +1 -1
  39. package/dist/assets/{dist-C8-kYY50.js → dist-BxbGvs8Z.js} +1 -1
  40. package/dist/assets/{dist-_cuIeZ2Q.js → dist-CON5WUUM.js} +1 -1
  41. package/dist/assets/{dist-nsS3Kg7p.js → dist-zL7lv5OB.js} +1 -1
  42. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-DHqre3sw.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-DU5Uuoui.js} +1 -1
  43. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-BO4koo_A.js → erDiagram-Q63AITRT-CiABnA0s-BIkgPJwz.js} +1 -1
  44. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-CZzeP2Do.js +1 -0
  45. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BcNja7As.js +1 -0
  46. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-BpNj4GGQ.js → ganttDiagram-NO4QXBWP-DQZvdFo1-i0B3574R.js} +1 -1
  47. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-Dfl3aEUI.js +1 -0
  48. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-C-DD21zp.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-DDH8BFah.js} +1 -1
  49. package/dist/assets/index-Bfg3eY1L.css +1 -0
  50. package/dist/assets/index-CqSlsYVG.js +616 -0
  51. package/dist/assets/info-DKCQHKI2-Dplx5kMp-BIS49gZw.js +1 -0
  52. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-Cjtq7aHi.js → infoDiagram-FWYZ7A6U-7UnoB5AP-DPJR6zHq.js} +1 -1
  53. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-CJ7Dm1DX.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-l1ua93u2.js} +1 -1
  54. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-DmEyfShK.js → journeyDiagram-5HDEW3XC-c5xIah9o-CBBk1xj0.js} +1 -1
  55. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-CEonRig7.js → kanban-definition-HUTT4EX6-Cnt6loYD-DXMToe3A.js} +1 -1
  56. package/dist/assets/{lib-BP6_kjsB.js → lib-CRLA4jbF.js} +1 -1
  57. package/dist/assets/{line-D7ziSjKi-ZN0lN31b.js → line-D7ziSjKi-Cg_lPLRL.js} +1 -1
  58. package/dist/assets/{marp-CUPxjfPv.js → marp-B6XQKNM-.js} +1 -1
  59. package/dist/assets/{mermaid-parser.core-DxEa8E3F-C7AoyBVj.js → mermaid-parser.core-DxEa8E3F-Dit0KGYk.js} +2 -2
  60. package/dist/assets/{mermaid.core-V0OYwIz3-CXLOt97m.js → mermaid.core-V0OYwIz3-B1A61-Tj.js} +3 -3
  61. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-DOiqhmPh.js → mindmap-definition-LN4V7U3C-CexN3O6L-CTkSZLPa.js} +1 -1
  62. package/dist/assets/packet-7NZHBO7P-CQI3flND-BLEmgsub.js +1 -0
  63. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-Dxt7poMV.js → pegDiagram-2B236MQR-BiM4G0cW-eXiW6hVB.js} +1 -1
  64. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-xUYJ0MCM.js +1 -0
  65. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-COQUiiOs.js → pieDiagram-ENE6RG2P-B_fBBS-2-BWLd7wxb.js} +1 -1
  66. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-B-eWI2XH.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-CbHyHiSQ.js} +1 -1
  67. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-DuyO7T8W.js +1 -0
  68. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-DV-zr5kg.js +1 -0
  69. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-CB_G0fUx.js +1 -0
  70. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-DbKGz_AB.js +1 -0
  71. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-C_0ZnTAv.js +1 -0
  72. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-DeaxglIg.js → railroadDiagram-RFXS5EU6--lNIlG64-DXl7SYGG.js} +1 -1
  73. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-1CMezKi3.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-YpczTMgx.js} +1 -1
  74. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-D7DwjyX1.js → sankeyDiagram-HTMAVEWB-DlutaXDv-Dg-4idXe.js} +1 -1
  75. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-CsJUqii0.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-DrTMC23z.js} +1 -1
  76. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-DzQL5WUm.js → stateDiagram-2N3HPSRC-jf_dXUEU-BLcycgBQ.js} +1 -1
  77. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-DvcSup1h.js +1 -0
  78. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-C0XGpuvl.js → swimlanes-5IMT3BWC-CYjtALQH-CQanOdSi.js} +1 -1
  79. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-f_02wuOM.js +8 -0
  80. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-CV7c2zBI.js → timeline-definition-FHXFAJF6-DYJ4oUm8-BUgGWs88.js} +1 -1
  81. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DN8bx6Us.js +1 -0
  82. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-CuiA6cSr.js +1 -0
  83. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-D3MiyDV3.js → vennDiagram-L72KCM5P-CdKAHoek-CMyJgjIe.js} +1 -1
  84. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-DvYP8cos.js +1 -0
  85. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-CJynIPoh.js → wardleyDiagram-EHGQE667-BF6c4_CW-DC2v_Fw4.js} +1 -1
  86. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-Co6wcyGy.js → xychartDiagram-FW5EYKEG-CGiKngj7-DzBp0TnN.js} +1 -1
  87. package/dist/index.html +2 -2
  88. package/package.json +1 -1
  89. package/server/agents/rate-limit-probe.spec.ts +4 -3
  90. package/server/agents/rate-limit-routes.ts +2 -2
  91. package/server/agents/rate-limit-store.spec.ts +77 -12
  92. package/server/agents/rate-limit-store.ts +78 -37
  93. package/server/agents/statusline.spec.ts +45 -1
  94. package/server/agents/statusline.ts +28 -0
  95. package/server/config/app-config.ts +24 -0
  96. package/server/config/config-body.ts +1 -1
  97. package/server/config/config-routes.ts +12 -1
  98. package/server/git/issue-work.ts +92 -0
  99. package/server/git/pr-footer.ts +21 -4
  100. package/server/git/repo-dirs.ts +112 -0
  101. package/server/git/worktree-pr.ts +31 -16
  102. package/server/git/worktree-routes.ts +11 -2
  103. package/server/git/worktrees.ts +82 -19
  104. package/server/index.ts +2 -1
  105. package/server/routes/app-routes.ts +4 -0
  106. package/server/routes/dir-routes.ts +2 -2
  107. package/server/routes/issue-work-routes.ts +62 -0
  108. package/server/routes/mcp-routes.ts +33 -8
  109. package/server/routes/repo-routes.ts +16 -1
  110. package/server/routes/ws-routes.ts +66 -41
  111. package/server/session/draft-injection.ts +39 -3
  112. package/server/session/pty-connection.ts +19 -5
  113. package/server/session/tmux-size-sync.ts +8 -2
  114. package/server/session/ws-frames.ts +6 -11
  115. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-DrblApEd.js +0 -1
  116. package/dist/assets/channel-7wUqSdoX-BgZgYLWN.js +0 -1
  117. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-0zi7Cgn2.js +0 -1
  118. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-0zi7Cgn2.js +0 -1
  119. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-Bi7e0oXc.js +0 -1
  120. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-BRn6nrMK.js +0 -1
  121. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-CnFfxuAw.js +0 -1
  122. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-65Nmo9BB.js +0 -1
  123. package/dist/assets/index-Fs9P1rOl.css +0 -1
  124. package/dist/assets/index-jKUTVgQT.js +0 -616
  125. package/dist/assets/info-DKCQHKI2-Dplx5kMp-BRtvzheB.js +0 -1
  126. package/dist/assets/packet-7NZHBO7P-CQI3flND-CmU_E5Rs.js +0 -1
  127. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-CZEE10z2.js +0 -1
  128. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-r15uJDA8.js +0 -1
  129. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-Dw_fxGlH.js +0 -1
  130. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-CKyTy_kj.js +0 -1
  131. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-DKG1VzHP.js +0 -1
  132. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DfP1SMYD.js +0 -1
  133. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CVaAK8RL.js +0 -1
  134. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-Dgs9N-of.js +0 -8
  135. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DBE7pPBH.js +0 -1
  136. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DTrYIIrW.js +0 -1
  137. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-JRYl6PcW.js +0 -1
package/README.md CHANGED
@@ -585,6 +585,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
585
585
  | `soundKinds` | Which moments beep — see [Notification sounds](#notification-sounds). Defaults to `["finished","waiting"]`; the other kinds are opt-in. |
586
586
  | `sounds` | Per-kind sound: `{ "waiting": "preset:coin" }`. A `preset:<id>` reference or an absolute path; a kind with no entry falls back to `soundFile`. |
587
587
  | `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates (via your `gh` login). |
588
+ | `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. |
588
589
  | `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. |
589
590
  | `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. |
590
591
  | `userMcpServers` | `{ id, url }` HTTP MCP servers merged into the **single-view** Claude session's `--mcp-config` (a `localhost` URL is reached over `host.docker.internal` in the Docker sandbox). Takes effect on the next session. |
@@ -958,6 +959,18 @@ separate working tree that shares the repo's `.git`, so several agents can work
958
959
  repo without colliding. Worktrees live under `~/.mulmoterminal/worktrees/` (override with
959
960
  `MULMOTERMINAL_HOME`), and existing ones are listed for reuse.
960
961
 
962
+ A worktree started **from an issue** gets an `issue/<N>-<slug>` branch instead. The number
963
+ in the name is what later tells the app which issue the work belongs to: the ⧉ Open PR
964
+ button puts `Fixes #<N>` in the PR body, and the branch chip, the issue work comment and
965
+ the merge-time auto-close all read the same number rather than guessing at it.
966
+
967
+ That path also **fetches first and forks from `origin/<base>`**, because several clones of
968
+ one repo often run side by side and only the one being worked in gets pulled — forking from
969
+ the local branch would start the work on however old that clone happens to be. A local base
970
+ that already contains the remote wins anyway (it is a superset, so nothing is lost), and
971
+ with no remote reachable the local branch is used and the worktree is still created.
972
+ Typing a task name yourself keeps the local base it has always used, with no fetch.
973
+
961
974
  ![An empty cell's launch form — choose the agent, working directory, or a worktree](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/grid-launch-form.png)
962
975
 
963
976
  *Every empty grid cell shows this launch form: toggle **Claude / Codex / Antigravity / Shell**, type a **working directory** (frequent ones autocomplete from your presets), or — in a git repo — name a task under **OR ISOLATE IN A WORKTREE** and hit **+ New worktree** to start the agent on its own isolated branch. **Shell** runs your OS default shell there instead of an agent; **OR LAUNCH** runs one of your configured launch commands.*
@@ -980,6 +993,24 @@ PRs show a CI-rollup / review-decision / draft badge; each repo lists its latest
980
993
  issues. Rows are real links, per-repo errors don't sink the view, and the two lists load
981
994
  independently. Backed by `GET /api/prs` and `GET /api/issues`.
982
995
 
996
+ **Starting work from an issue row.** Each issue row carries a **▶** button that does the setup in
997
+ one click: read the issue, cut an `issue/<number>-<slug>` worktree in your clone of that repo, and
998
+ open Claude there as a grid cell with the issue **typed into its input box but not sent**. The
999
+ prompt is seeded server-side as a *draft* (`server/session/draft-injection.ts`), which waits for
1000
+ claude's input box to be ready — text pushed in before that lands in the scrollback instead. A repo
1001
+ with several clones asks which one the first time and remembers the answer; a repo with no clone
1002
+ here disables the button and says why. Backed by `POST /api/issues/start`.
1003
+
1004
+ **Which clone a repo's work happens in.** `GET /api/repo-dirs` answers the reverse of the
1005
+ GitHub link a cell already shows: given `owner/repo`, which of your saved directories are
1006
+ clones of it. The candidates are derived from your directory presets by reading each one's
1007
+ `origin` — there is no second list to keep in step — and are ordered by each directory's
1008
+ `orderPriority`, then by path. Several clones of one repo commonly run side by side, so the
1009
+ answer is a choice rather than a lookup; once you make it, `repoDirs` in the config records
1010
+ `owner/repo` → the chosen path and it is used from then on. A recording is dropped if the
1011
+ directory is no longer a saved clone of that repo, and a repo with no clone here is simply
1012
+ absent from the answer — which is how a caller learns work cannot start on it.
1013
+
983
1014
  ---
984
1015
 
985
1016
  ## Cost & token usage
@@ -1063,6 +1094,17 @@ Favorited collections get their own toolbar buttons.
1063
1094
 
1064
1095
  ![Zoom — one agent enlarged, the others as a filmstrip along the bottom](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/grid-zoom.png)
1065
1096
 
1097
+ - **Set a terminal aside** — the moon button in a cell's header **sinks** it: the tile, its
1098
+ filmstrip thumbnail and its cockpit-roster row all fade, and the working dot stops pulsing.
1099
+ The session stays **connected and keeps its whole history** — this is what to reach for
1100
+ instead of `/clear`-ing a cell you are done with for now, which resets the conversation just
1101
+ to change how the cell looks. The setting survives a reload. **Enlarging it keeps it faded** —
1102
+ that is how you read a set-aside session without waking it, and its roster row keeps the blue
1103
+ "you are here" edge either way — while **typing into it wakes it**, so nothing has to be undone
1104
+ by hand. Clicking or scrolling to read it does *not* wake it, even though a mouse-tracking agent
1105
+ receives those as input. A cell that **stops for a permission prompt comes back to full strength
1106
+ on its own**, so setting one aside can never hide a session that is waiting on you; a merely *finished* turn
1107
+ does not, since that is the expected outcome of setting a running agent aside.
1066
1108
  - **Timeline** (🕘) — a read-only per-session activity timeline (tools run, newest first),
1067
1109
  from `GET /api/transcript/timeline`.
1068
1110
  - **Bring another cell's turn here** (💬) — pick another terminal in the grid and its
@@ -1286,8 +1328,10 @@ same-origin-guarded.
1286
1328
  | `GET /api/git-status?cwd=` | `{ repo, branch, detached, dirty, ahead, behind, upstream }`. |
1287
1329
  | `POST /api/git-remote` | The dir's GitHub repo URL (for the header GitHub menu). |
1288
1330
  | `GET /api/worktrees?cwd=` · `GET /api/worktrees/diff?cwd=` | List managed worktrees / diff one vs its base. |
1289
- | `POST /api/worktrees/create` · `/remove` · `/push` · `/pr` | Create on `agent/<slug>`, remove (managed root only), push, open a PR (`gh`, else compare URL). |
1331
+ | `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). |
1290
1332
  | `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` (via `gh`). |
1333
+ | `GET /api/repo-dirs` | Which saved directories clone which GitHub repo, ordered, with the recorded choice per repo. |
1334
+ | `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. |
1291
1335
  | `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. |
1292
1336
 
1293
1337
  **Workspace views**
@@ -1341,6 +1385,10 @@ connection (or reattach to an existing background PTY).
1341
1385
  background PTY exists for `<id>`, the socket reattaches to it (and its recent
1342
1386
  output buffer is replayed); otherwise the server spawns
1343
1387
  `claude --resume <id> --settings <hooks>`.
1388
+ - `&cols=<n>&rows=<n>` — the terminal's geometry, on every endpoint that starts a PTY. The
1389
+ PTY is created at it instead of the 120x30 default, so nothing is ever drawn at a size the
1390
+ browser didn't ask for. Out-of-range values are ignored (same bounds as a `resize` frame),
1391
+ and a connection that sends none keeps the default until its first `resize`.
1344
1392
 
1345
1393
  **Server → client** (JSON text frames):
1346
1394
 
@@ -0,0 +1,24 @@
1
+ // Ordering BY directory, for the two places that do it: the grid's "priority" sort and the
2
+ // launcher's directory chips. Shared because those two are read side by side — a launcher that
3
+ // answered "what does an unset rank mean?" differently would put a project in one place on the
4
+ // chips and another in the grid.
5
+
6
+ // A directory that sets no `orderPriority` sorts after every directory that does, so adding the
7
+ // key to ONE project doesn't displace all the others. Infinity rather than a big sentinel: no
8
+ // integer a user could write should be able to outrank "unset".
9
+ export const UNSET_PRIORITY = Number.POSITIVE_INFINITY;
10
+
11
+ export const dirPriority = (cwd: string | null, priorityByCwd: Record<string, number>): number =>
12
+ cwd ? (priorityByCwd[cwd] ?? UNSET_PRIORITY) : UNSET_PRIORITY;
13
+
14
+ // Stable: equal ranks keep their incoming order, so the directories that declare none stay in
15
+ // whatever order the caller already holds them in (for the chips, most-recently-used).
16
+ //
17
+ // Two unset directories subtract to `Infinity - Infinity` = NaN, which is falsy — so that pair
18
+ // falls through to the index comparison and keeps its order, which is what the tie is meant to do.
19
+ export function orderByDirPriority<T>(items: readonly T[], cwdOf: (item: T) => string | null, priorityByCwd: Record<string, number>): T[] {
20
+ return items
21
+ .map((item, i) => ({ item, i }))
22
+ .sort((a, b) => dirPriority(cwdOf(a.item), priorityByCwd) - dirPriority(cwdOf(b.item), priorityByCwd) || a.i - b.i)
23
+ .map((x) => x.item);
24
+ }
package/common/prPhase.ts CHANGED
@@ -42,14 +42,23 @@ export function workItemHeadline(item: WorkItem): string | null {
42
42
  //
43
43
  // `[1-9]\d*` rather than `\d+`: there is no issue #0, and `#0123` is not issue 123 — both are
44
44
  // typos, and a typo that renders as a link to somebody else's issue is worse than no chip.
45
- const CLOSING_KEYWORD = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)[:\s]+#([1-9]\d*)/i;
45
+ const CLOSING_VERB = "(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)";
46
+ const CLOSING_KEYWORD = new RegExp(`\\b${CLOSING_VERB}[:\\s]+#([1-9]\\d*)`, "i");
47
+
48
+ // The other form GitHub accepts for the same statement: the issue's full URL. Each path segment
49
+ // is a run of non-slash characters rather than `\S+`, so there is nothing for the engine to
50
+ // backtrack over — same reason the separator above is one character class.
51
+ const CLOSING_URL = new RegExp(`\\b${CLOSING_VERB}[:\\s]+https?://[^\\s/]+(?:/[^\\s/]+){2}/issues/[1-9]\\d*`, "i");
52
+
53
+ // An issue or PR number as anything in this app will accept it. Shared because every surface that
54
+ // handles one applies the same rule and a divergence would show as a link to nothing: digits from
55
+ // a body or a branch name are unbounded, and `Number("9".repeat(20))` is 1e20 — which renders in
56
+ // the chip as "#1e+20" (found by Codex review). There is also no issue #0 and no negative one.
57
+ export const isIssueNumber = (v: unknown): v is number => typeof v === "number" && Number.isSafeInteger(v) && v > 0;
46
58
 
47
- // Digits from a body or a branch name are unbounded, and `Number("9".repeat(20))` is 1e20 — which
48
- // would render in the chip as "#1e+20" and link nowhere. Anything past the safe-integer range is
49
- // not an issue number (found by Codex review).
50
59
  function toIssueNumber(digits: string): number | null {
51
60
  const n = Number(digits);
52
- return Number.isSafeInteger(n) && n > 0 ? n : null;
61
+ return isIssueNumber(n) ? n : null;
53
62
  }
54
63
 
55
64
  // The issue a PR body says it closes, or null. Deliberately blind to the full-URL form
@@ -60,6 +69,14 @@ export function issueRefFromPrBody(body: string | null | undefined): number | nu
60
69
  return found ? toIssueNumber(found[1]) : null;
61
70
  }
62
71
 
72
+ // Whether the body ALREADY states what merging it closes, in either form GitHub honours. Broader
73
+ // than issueRefFromPrBody on purpose, and the difference matters: that one answers "which issue in
74
+ // THIS repo" and so must ignore a URL that may name another repository, while this one answers
75
+ // "has the author already declared a closure" — where the URL form counts just as much, because
76
+ // adding a second keyword on top of it closes BOTH issues on merge (found by Codex review).
77
+ export const declaresClosingReference = (body: string | null | undefined): boolean =>
78
+ typeof body === "string" && (CLOSING_KEYWORD.test(body) || CLOSING_URL.test(body));
79
+
63
80
  // A branch named after its issue the way this repo names them: `fix/966-preserve-unknown-keys`.
64
81
  // Requires a type prefix and a hyphen after the digits, so `chore/dep-updates-20260728` is not
65
82
  // read as issue #20260728.
@@ -73,3 +90,19 @@ export function issueCandidateFromBranch(branch: string | null | undefined): num
73
90
  const found = typeof branch === "string" ? BRANCH_ISSUE.exec(branch) : null;
74
91
  return found ? toIssueNumber(found[1]) : null;
75
92
  }
93
+
94
+ // The prefix this app gives a branch it creates FOR an issue (#1171). Shared because the two
95
+ // sides must agree on the same string: worktree creation writes it, and the reader below is only
96
+ // sound because nothing else in the app produces it.
97
+ export const ISSUE_BRANCH_PREFIX = "issue/";
98
+
99
+ const ANCHORED_ISSUE = new RegExp(`^${ISSUE_BRANCH_PREFIX}([1-9]\\d*)-`);
100
+
101
+ // The issue a branch was CREATED for. Unlike the candidate above this is not a guess — the prefix
102
+ // is one this app writes, so a match is a statement it made about its own branch. That is what
103
+ // makes it safe to derive a PR body's `Fixes #N` from: a wrong number there closes somebody
104
+ // else's issue the moment the PR merges, which no amount of confirming afterwards undoes.
105
+ export function issueFromAnchoredBranch(branch: string | null | undefined): number | null {
106
+ const found = typeof branch === "string" ? ANCHORED_ISSUE.exec(branch) : null;
107
+ return found ? toIssueNumber(found[1]) : null;
108
+ }
@@ -0,0 +1,57 @@
1
+ // Which local clones a GitHub repository has on this machine — what `GET /api/repo-dirs` answers.
2
+ //
3
+ // Shared because both sides decide from it: the server resolves each saved directory's `origin`
4
+ // and groups them, and the UI picks which clone to start work in (and offers the rest). Several
5
+ // clones of one repo commonly run side by side here, so "the directory for owner/repo" is a
6
+ // CHOICE rather than a lookup — which is why a candidate carries what it takes to choose.
7
+
8
+ export interface RepoDirCandidate {
9
+ /** Absolute path of the clone (the canonical spelling `cwdPresets` stores). */
10
+ path: string;
11
+ /** The preset's label, which is what the user named this clone in Settings. */
12
+ label: string;
13
+ /** The directory's own `orderPriority` from `.mulmoterminal.json`, or null when it sets none. */
14
+ orderPriority: number | null;
15
+ }
16
+
17
+ export interface RepoDirs {
18
+ /** `owner/repo`. */
19
+ repo: string;
20
+ /** Candidates, already ordered: by `orderPriority`, then by path for the ones that set none. */
21
+ dirs: RepoDirCandidate[];
22
+ /** The recorded choice for this repo, or null when there is none to honour. Always one of
23
+ * `dirs` — a recording whose directory has since gone or now points at a different repo is
24
+ * dropped rather than returned, because the caller would otherwise start work in it. */
25
+ primary: string | null;
26
+ }
27
+
28
+ export interface RepoDirsResponse {
29
+ repos: RepoDirs[];
30
+ }
31
+
32
+ // The server is the only writer of this shape, but it arrives as a network response: an older
33
+ // build, a proxy returning HTML, or a half-deployed server would otherwise put `undefined` where
34
+ // a WORKING DIRECTORY is expected — and this value becomes the cwd a session is started in.
35
+ // Anything malformed is dropped rather than repaired, so a bad entry offers no clone at all
36
+ // instead of a wrong one.
37
+ const isCandidate = (v: unknown): v is RepoDirCandidate =>
38
+ isObject(v) &&
39
+ typeof v.path === "string" &&
40
+ v.path !== "" &&
41
+ typeof v.label === "string" &&
42
+ (v.orderPriority === null || typeof v.orderPriority === "number");
43
+
44
+ const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
45
+
46
+ export function parseRepoDirsResponse(data: unknown): RepoDirs[] {
47
+ if (!isObject(data) || !Array.isArray(data.repos)) return [];
48
+ return data.repos.filter(isObject).flatMap((entry) => {
49
+ if (typeof entry.repo !== "string" || entry.repo === "" || !Array.isArray(entry.dirs)) return [];
50
+ const dirs = entry.dirs.filter(isCandidate);
51
+ // `primary` must name one of the candidates it came with. A recording the server kept but a
52
+ // candidate list that no longer holds it would otherwise start work in a directory this side
53
+ // never offered.
54
+ const primary = typeof entry.primary === "string" && dirs.some((d) => d.path === entry.primary) ? entry.primary : null;
55
+ return [{ repo: entry.repo, dirs, primary }];
56
+ });
57
+ }
@@ -0,0 +1,30 @@
1
+ // A terminal's geometry, and what counts as a usable one.
2
+ //
3
+ // Shared because BOTH sides decide from it: the browser puts the size it has already measured on
4
+ // the connect URL so the pty is born with it (#1178), and the server validates that number before
5
+ // it reaches node-pty — a crafted or buggy client must not be able to ask for a 0-column or
6
+ // absurdly large terminal. Two copies of these bounds would let the two sides disagree about which
7
+ // sizes exist at all.
8
+ export interface TerminalSize {
9
+ cols: number;
10
+ rows: number;
11
+ }
12
+
13
+ export const MIN_TERM_COLS = 2;
14
+ export const MAX_TERM_COLS = 500;
15
+ export const MIN_TERM_ROWS = 1;
16
+ export const MAX_TERM_ROWS = 200;
17
+
18
+ export function isUsableTerminalSize({ cols, rows }: TerminalSize): boolean {
19
+ if (!Number.isInteger(cols) || !Number.isInteger(rows)) return false;
20
+ return cols >= MIN_TERM_COLS && cols <= MAX_TERM_COLS && rows >= MIN_TERM_ROWS && rows <= MAX_TERM_ROWS;
21
+ }
22
+
23
+ /** The geometry a connect URL carries, or null when it carries none it can stand behind — an
24
+ * absent, unparsable or out-of-range pair. Null means "spawn at the default and wait for the
25
+ * browser's resize", which is what every client did before this was sent. */
26
+ export function parseTerminalSize(cols: string | null, rows: string | null): TerminalSize | null {
27
+ if (cols === null || rows === null) return null;
28
+ const size = { cols: Number(cols), rows: Number(rows) };
29
+ return isUsableTerminalSize(size) ? size : null;
30
+ }
@@ -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-C7AoyBVj.js";import"./chunk-WYO6CB5R-BlzOfotS-Bx9N57Se.js";import"./chunk-VAUOI2AC-CS9QJ4yz-BQv37MCq.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-BPbi0qBZ.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};
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-Dit0KGYk.js";import"./chunk-WYO6CB5R-BlzOfotS-BJCy4_YQ.js";import"./chunk-VAUOI2AC-CS9QJ4yz-Oly0QbKc.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-EDD8-kXb.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-Dit0KGYk.js";export{e as createArchitectureServices};