mulmoterminal 1.11.1 → 2.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 (195) hide show
  1. package/README.md +54 -10
  2. package/common/dirChrome.ts +15 -0
  3. package/common/firebaseConfig.ts +15 -0
  4. package/common/ghItems.ts +43 -0
  5. package/common/gitStatus.ts +11 -0
  6. package/common/isRecord.ts +13 -0
  7. package/common/keymap.ts +158 -0
  8. package/common/launchAgent.ts +15 -0
  9. package/common/launchOptions.ts +24 -0
  10. package/common/pushKinds.ts +21 -0
  11. package/common/quickCommands.ts +13 -0
  12. package/common/remoteHostHealth.ts +30 -0
  13. package/common/sessionAgent.ts +8 -0
  14. package/common/shortcuts.ts +27 -0
  15. package/common/sourceExtensions.ts +61 -0
  16. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-Yx87EMvu.js → abnfDiagram-VRR7QNED-RjiYivmv-CMyzg2zY.js} +1 -1
  17. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-3kpQCFcP.js +1 -0
  18. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-DKMwPUXB.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-CSIT7RtO.js} +1 -1
  19. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-BzxGxt1k.js → blockDiagram-677ZJIJ3-BPuAJQRW-CnVQRl8Q.js} +1 -1
  20. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-CwgYTa99.js → c4Diagram-LMCZKHZV-C0LAqQso-CrpIWU_7.js} +1 -1
  21. package/dist/assets/channel-7wUqSdoX-D8_GjCXH.js +1 -0
  22. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-Cae6XalS.js → chunk-32BRIVSS-BRrYpgtb-D_PiqrVh.js} +1 -1
  23. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-CljKt1OH.js → chunk-52WLFC77-BJ-ss3Xr-BCpG-w5L.js} +1 -1
  24. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-B7Tbwq36.js → chunk-C7G6YPKG-BZEucKEL-CP9wac26.js} +1 -1
  25. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-CrRYTA1_.js → chunk-EX3LRPZG-DLS6FBN1-BJZF7tEb.js} +1 -1
  26. package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-BaHiDfme.js → chunk-FWX5IMBZ-DHLSFw1H-Cnk0SpZp.js} +2 -2
  27. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-C-_WeZ6c.js → chunk-HOUHSVGY-Bhlt8hXJ-CRGS9q8z.js} +1 -1
  28. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-BIlo73Iv.js → chunk-ICXQ74PX-DwgHBX_g-DpPUV9Tl.js} +1 -1
  29. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-zF0QMt5P.js → chunk-MOJQB5TN-CMZRaeqt-CG59XkeN.js} +1 -1
  30. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-ByZSGfBU.js → chunk-OGEWGWER-8Qy4a8b5-B59YGczL.js} +1 -1
  31. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-BGlA1pov.js → chunk-PUDLZKDR-DcrWQRYh-CXGtfuoa.js} +1 -1
  32. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-B1sNa6lZ.js → chunk-Q4XR5HBZ-ZXVGkG8Z-eHuKxxBN.js} +1 -1
  33. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-BggIXcqi.js → chunk-V7JOEXUC-DmGdheTX-BJZa8auK.js} +1 -1
  34. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-N2n8rUqR.js → chunk-VAUOI2AC-CS9QJ4yz-DviHNtmN.js} +1 -1
  35. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-KfGzmllv.js → chunk-VR4S4FIN-C6a91eNY-DsA9hbD5.js} +1 -1
  36. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-ozBfm4Ec.js → chunk-WYO6CB5R-BlzOfotS-qckQG4MR.js} +1 -1
  37. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-Dq29ee1M.js → chunk-ZGVPDNZ5-BYwxNFTK-Cr8j6FMw.js} +1 -1
  38. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-axLhYHqO.js +1 -0
  39. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-axLhYHqO.js +1 -0
  40. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-C9DtePCD.js +1 -0
  41. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--wmvSW6d7.js → cynefinDiagram-TSTJHNR4-DHA9iPo--BuPVUHaQ.js} +1 -1
  42. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-BQ4LK8eY.js → dagre-VKFMJZFB--oJKqXBZ-l8CP-U85.js} +1 -1
  43. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-CIU6tRCi.js → diagram-FQU43EPY-CcJCB9bG-CV6yO6d6.js} +1 -1
  44. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-iddHhfww.js → diagram-G47NLZAW-C_o-WGG1-DmUMCyqP.js} +1 -1
  45. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--DNxATjit.js → diagram-NH7WQ7WH-CXJCYvY--D1Zcc9sH.js} +1 -1
  46. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-D8JpFy-q.js → diagram-OA4YK3LP-BMzeJ87A-CsC2S-ug.js} +1 -1
  47. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-XUwxZdud.js → diagram-WEI45ONY-D_93NKqo-C3Xq1urQ.js} +1 -1
  48. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-kJBvOJhp.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-BtjcwSW_.js} +1 -1
  49. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-CjybCsz0.js → erDiagram-Q63AITRT-CiABnA0s-DhQMPhj9.js} +1 -1
  50. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-BCX3cvpw.js +1 -0
  51. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-DjZRfq7e.js +1 -0
  52. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-DhyDieVz.js → ganttDiagram-NO4QXBWP-DQZvdFo1-Bw2aYbCJ.js} +1 -1
  53. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-pIAqsAx0.js +1 -0
  54. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-C-MFxNco.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-C1Dlsswx.js} +1 -1
  55. package/dist/assets/index-DVTKApMs.css +1 -0
  56. package/dist/assets/index-Dwc5wfJg.js +618 -0
  57. package/dist/assets/info-DKCQHKI2-Dplx5kMp-S1We2Nw2.js +1 -0
  58. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-Bk6ucFgi.js → infoDiagram-FWYZ7A6U-7UnoB5AP-CjB00vD7.js} +1 -1
  59. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-BKc4_UOI.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-BxVGf9t3.js} +1 -1
  60. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-KWVGLnvb.js → journeyDiagram-5HDEW3XC-c5xIah9o-BPLsgkBp.js} +1 -1
  61. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-AQkWoiB_.js → kanban-definition-HUTT4EX6-Cnt6loYD-C0UF5ruR.js} +1 -1
  62. package/dist/assets/{lib-DXVmTnVo.js → lib-Ca9R1hlX.js} +1 -1
  63. package/dist/assets/{line-D7ziSjKi-BsuQ8Z8Y.js → line-D7ziSjKi-B4ejYAqw.js} +1 -1
  64. package/dist/assets/{marp-Cu8Xj7h9.js → marp-J4wSkl3Q.js} +1 -1
  65. package/dist/assets/material-symbols-outlined-D4PiVfdc.woff2 +0 -0
  66. package/dist/assets/{mermaid-parser.core-DxEa8E3F-DfwxZrpZ.js → mermaid-parser.core-DxEa8E3F-D1ef8DoR.js} +2 -2
  67. package/dist/assets/{mermaid.core-V0OYwIz3-CrOKl_kg.js → mermaid.core-V0OYwIz3-BSn9HyWz.js} +3 -3
  68. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-DjcrgejE.js → mindmap-definition-LN4V7U3C-CexN3O6L-BLTr4gMi.js} +1 -1
  69. package/dist/assets/packet-7NZHBO7P-CQI3flND-DaLVcMu_.js +1 -0
  70. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-CbtcpYhR.js → pegDiagram-2B236MQR-BiM4G0cW-E_eG9LVV.js} +1 -1
  71. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-BKDCOtxh.js +1 -0
  72. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-k2YqiOr3.js → pieDiagram-ENE6RG2P-B_fBBS-2-f4ZSZ6GC.js} +1 -1
  73. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-CKE8aMnH.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-DnwDJxmc.js} +1 -1
  74. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-Cw4aJNVl.js +1 -0
  75. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-DJZYlsdK.js +1 -0
  76. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-D9wfK70u.js +1 -0
  77. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-C2rW9Qx8.js +1 -0
  78. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CCUOq6go.js +1 -0
  79. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-CfC5IGZ9.js → railroadDiagram-RFXS5EU6--lNIlG64-BUH_ocB5.js} +1 -1
  80. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-BJz1VLLL.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-CT5zmLHk.js} +1 -1
  81. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-MDSxFtsF.js → sankeyDiagram-HTMAVEWB-DlutaXDv-9sorx_WA.js} +1 -1
  82. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-D9_zwZfU.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-CnjR9ja7.js} +1 -1
  83. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-BXB6jerK.js → stateDiagram-2N3HPSRC-jf_dXUEU-DboFu0BL.js} +1 -1
  84. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-FGJ3mpM7.js +1 -0
  85. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-B5Di6XJZ.js → swimlanes-5IMT3BWC-CYjtALQH-Cqxsp00w.js} +1 -1
  86. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-B3qFCxjl.js +8 -0
  87. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-CQw3ieIM.js → timeline-definition-FHXFAJF6-DYJ4oUm8-Du08XOrV.js} +1 -1
  88. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-dIhNDmqB.js +1 -0
  89. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-C8SEy1d3.js +1 -0
  90. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-FQvQgKow.js → vennDiagram-L72KCM5P-CdKAHoek-DtBtukcO.js} +1 -1
  91. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-SYQlwZRo.js +1 -0
  92. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-Dz6_8pBQ.js → wardleyDiagram-EHGQE667-BF6c4_CW-CW4nfoJ3.js} +1 -1
  93. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-s3WECSVK.js → xychartDiagram-FW5EYKEG-CGiKngj7-Ruux6STV.js} +1 -1
  94. package/dist/index.html +2 -2
  95. package/package.json +14 -14
  96. package/server/agents/codex-activity.ts +3 -2
  97. package/server/agents/codex-args.ts +6 -2
  98. package/server/agents/codex-session.ts +1 -2
  99. package/server/agents/codex-sessions.ts +1 -2
  100. package/server/agents/codex-skills.ts +14 -4
  101. package/server/backends/fileChange.ts +2 -2
  102. package/server/backends/notifier.ts +2 -2
  103. package/server/backends/rawServingPlan.ts +10 -47
  104. package/server/backends/remoteHost/getFeed.spec.ts +2 -1
  105. package/server/backends/remoteHost/handlers.spec.ts +15 -5
  106. package/server/backends/remoteHost/handlers.ts +30 -1
  107. package/server/backends/remoteHost/healthNotice.ts +53 -0
  108. package/server/backends/remoteHost/index.ts +30 -7
  109. package/server/backends/remoteHost/launchTerminal.ts +38 -0
  110. package/server/backends/remoteHost/quickCommands.ts +22 -0
  111. package/server/backends/remoteHost/resilientRunner.ts +174 -0
  112. package/server/backends/remoteHost/routes.spec.ts +30 -8
  113. package/server/backends/remoteHost/routes.ts +21 -8
  114. package/server/backends/remoteHost/session.ts +1 -1
  115. package/server/backends/remoteHost/terminalInput.ts +1 -1
  116. package/server/backends/remoteHost/terminalScreen.ts +32 -7
  117. package/server/backends/scheduler.ts +3 -6
  118. package/server/backends/shortcuts.ts +4 -26
  119. package/server/backends/translation.ts +2 -1
  120. package/server/config/app-config.ts +72 -6
  121. package/server/config/config-body.ts +1 -1
  122. package/server/config/config-routes.ts +16 -1
  123. package/server/config/config-schema.ts +11 -0
  124. package/server/config/cwd-presets.ts +6 -6
  125. package/server/config/dir-config.ts +6 -13
  126. package/server/config/header-config.ts +2 -2
  127. package/server/config/keymap-check.ts +49 -0
  128. package/server/config/launch-options.ts +2 -21
  129. package/server/files/atomic-write.ts +29 -0
  130. package/server/files/dirRequest.ts +1 -1
  131. package/server/files/pathContainment.ts +39 -1
  132. package/server/files/pick-file.ts +1 -2
  133. package/server/files/scripts.ts +4 -4
  134. package/server/git/ghItem.ts +5 -12
  135. package/server/git/git-parse.ts +3 -2
  136. package/server/git/git-status.ts +1 -10
  137. package/server/git/issues.ts +2 -14
  138. package/server/git/prPhase.ts +3 -3
  139. package/server/git/prs.ts +3 -18
  140. package/server/git/worktree-diff.ts +3 -2
  141. package/server/git/worktrees.ts +2 -1
  142. package/server/index.ts +71 -7
  143. package/server/infra/fs-cleanup.ts +29 -0
  144. package/server/infra/install-bundled-skills.ts +16 -4
  145. package/server/infra/pubsub.ts +27 -0
  146. package/server/infra/read-text-file.ts +23 -0
  147. package/server/infra/sandbox.ts +5 -5
  148. package/server/infra/split-lines.ts +10 -0
  149. package/server/infra/tmux.ts +3 -2
  150. package/server/mcp/broker.ts +1 -1
  151. package/server/routes/app-routes.ts +2 -9
  152. package/server/routes/hook-routes.ts +2 -9
  153. package/server/routes/plugin-narration.ts +1 -1
  154. package/server/routes/plugin-routes.ts +1 -1
  155. package/server/routes/toolResultPlan.ts +1 -1
  156. package/server/session/activity-hook.ts +2 -1
  157. package/server/session/activity-state.ts +2 -2
  158. package/server/session/command-summary.ts +1 -2
  159. package/server/session/cost.ts +2 -1
  160. package/server/session/last-turn.ts +2 -1
  161. package/server/session/scheduled-sessions.ts +1 -2
  162. package/server/session/session-activity-deps.ts +16 -0
  163. package/server/session/session-reads.ts +1 -1
  164. package/server/session/session-settings.ts +42 -3
  165. package/server/session/task-push.ts +5 -4
  166. package/server/session/taskPushRules.ts +8 -0
  167. package/server/session/transcript.ts +1 -1
  168. package/server/session/translation-submit.ts +1 -1
  169. package/server/session/types.ts +1 -1
  170. package/server/skills/mulmoterminal-config/SKILL.md +112 -2
  171. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-B_Z46N0H.js +0 -1
  172. package/dist/assets/channel-7wUqSdoX-W8JCDz4X.js +0 -1
  173. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-CiKS2vPx.js +0 -1
  174. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-CiKS2vPx.js +0 -1
  175. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-zJRLP3cv.js +0 -1
  176. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-EJ7ccecL.js +0 -1
  177. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-sOO6IJuZ.js +0 -1
  178. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DyHU2dET.js +0 -1
  179. package/dist/assets/index-CRcpNpn1.js +0 -618
  180. package/dist/assets/index-CgkzlIm9.css +0 -1
  181. package/dist/assets/info-DKCQHKI2-Dplx5kMp-CT2Yo-zj.js +0 -1
  182. package/dist/assets/material-symbols-outlined-BmmV8Mzf.woff2 +0 -0
  183. package/dist/assets/packet-7NZHBO7P-CQI3flND-8hOWvPx5.js +0 -1
  184. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-p2c1gNk2.js +0 -1
  185. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-BfSLhb-E.js +0 -1
  186. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-BZKbmfR_.js +0 -1
  187. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-C-y_NBwh.js +0 -1
  188. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-D2XerA1N.js +0 -1
  189. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DDNxD1MC.js +0 -1
  190. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-C4j-lZNT.js +0 -1
  191. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-CJ6DFoG6.js +0 -8
  192. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-C1-ZQ4Mc.js +0 -1
  193. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-D45mYTYv.js +0 -1
  194. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-CRfqyJ0c.js +0 -1
  195. package/server/backends/remoteHost/firebase.ts +0 -23
package/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  **Run a whole team of coding agents from your browser — and actually keep up with them.**
4
4
 
5
+ ### 📖 Documentation — **[receptron.github.io/mulmoterminal](https://receptron.github.io/mulmoterminal/)**
6
+
7
+ - **User guide:** [English](https://receptron.github.io/mulmoterminal/guide/en/) — the grid
8
+ view, everyday workflows, the full feature list, configuration, and mobile push notifications.
9
+ - **ユーザーガイド:** [日本語](https://receptron.github.io/mulmoterminal/guide/ja/) —
10
+ グリッドの使い方・日々のワークフロー・機能一覧・設定・スマホ通知の設定はこちら。
11
+
5
12
  ![MulmoTerminal — a grid of live Claude Code sessions, each color-coded by state, updating in real time](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/hero.gif)
6
13
 
7
14
  MulmoTerminal turns [Claude Code](https://claude.com/claude-code) (and OpenAI's **Codex**)
@@ -14,12 +21,6 @@ ping to your phone when a task finishes. One `npx` command, no Electron, no conf
14
21
  npx mulmoterminal # starts on http://localhost:34567 and opens your browser
15
22
  ```
16
23
 
17
- - 📖 **User guide:** [English](https://receptron.github.io/mulmoterminal/guide/en/) — the
18
- grid view, everyday workflows, the full feature list, configuration, and mobile push
19
- notifications.
20
- - 📖 **ユーザーガイド:** [日本語](https://receptron.github.io/mulmoterminal/guide/ja/) —
21
- グリッドの使い方・日々のワークフロー・機能一覧・設定・スマホ通知の設定はこちら。
22
-
23
24
  ![MulmoTerminal's grid view — four live Claude sessions running side by side, each in its own color-coded project](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/grid-2x2-live.png)
24
25
 
25
26
  *The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working / done (blue), needs-you (amber — e.g. waiting on a permission), idle — with an attention chime so a stuck cell off-screen still pulls you back. Supervise many; only step in where you're called.*
@@ -78,6 +79,34 @@ asks the local server to open the OS file dialog and inserts the chosen path (wo
78
79
  browser, including Chrome). The path is inserted at the cursor — it is not submitted, so you
79
80
  can review it first.
80
81
 
82
+ <a id="clicking-a-file-path"></a>
83
+
84
+ **Clicking a file path** — the other direction. A path an agent *prints* becomes a link, and
85
+ **what it opens is chosen by its extension**, so each kind arrives as the thing it is rather
86
+ than as bytes (files within the session's working directory only):
87
+
88
+ | A clicked … | opens as |
89
+ |---|---|
90
+ | `.md` `.markdown` | **rendered** markdown in a new tab — the same sandboxed `…/md` HTML the Files preview uses. It follows your system light/dark setting, since under the sandbox CSP it can't ask the app which theme is on |
91
+ | `.json` | **indented** in a new tab (Chrome and Safari otherwise show one long line) |
92
+ | `.csv` `.tsv` | a **table** in a new tab, with a sticky header that scrolls inside its own box |
93
+ | source, config, logs, and `.txt` — 46 extensions | the app's own **Files** view (`/files?path=`), where CodeMirror highlights it, the tree is right there, and it can be edited |
94
+ | everything else — images, PDF, SVG, HTML, video | raw bytes in a new tab, which the browser renders better than an editor would |
95
+
96
+ Highlighting in the Files view covers the JS/TS family, JSON and Markdown (the modes
97
+ `cmEditor.ts` bundles); other languages open as plain text.
98
+
99
+ This set is **deliberately asymmetric** with the set the server serves as viewable text —
100
+ `.md` goes to the rendered viewer rather than the Files view, `.txt` does the opposite, and
101
+ dotfiles are server-only. The 45 extensions both sides agree on live in
102
+ `common/sourceExtensions.ts`, each side adds its own extras, and
103
+ `test/common/sourceExtensions.spec.ts` pins the asymmetry so it isn't "fixed" into symmetry.
104
+
105
+ > **Changing this?** The routing table is `ROUTE_BY_EXTENSION` / `IN_APP_EXTENSIONS` in
106
+ > `src/composables/terminalFilePathLinkProvider.ts`. Update this section, the
107
+ > `docs/guide/{en,ja}/features.md` row, and the link table in `docs/terminal-notes.md`
108
+ > together — all three went stale once already (#834).
109
+
81
110
  ---
82
111
 
83
112
  ## Install & run
@@ -413,10 +442,12 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
413
442
  | `soundFile` | Absolute path to a custom **attention sound** (played when a session needs attention). Empty/unset uses the built-in synthesized chime. |
414
443
  | `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates (via your `gh` login). |
415
444
  | `launchers` | `{ label, command }` entries offered in a grid cell's launcher besides Claude — a plain shell, `codex`, any interactive command. |
445
+ | `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. |
416
446
  | `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. |
417
447
  | `buttons` | Header action buttons — see [Header buttons](#header-buttons). Omit to keep the defaults; set to replace them. |
418
448
  | `chips` | Header info chips. Omit to keep the default set; `[]` hides all built-ins. |
419
- | `pushEnabled` | `true` to send a **Web Push** to your registered devices when a background task finishes. Off by default; only sends while the **RemoteHost** channel is connected (see below). |
449
+ | `pushEnabled` | `true` to send a **Web Push** to your registered devices. Off by default; only sends while the **RemoteHost** channel is connected (see below). The master switch — `pushKinds` picks which moments. |
450
+ | `pushKinds` | Which moments push: `"finished"` (a turn ended, ✅) and/or `"waiting"` (the agent stopped to ask — a permission prompt or a question, ❓, **once per prompt**). Omit to keep both; `[]` for none. A kind added in a later version stays off until you tick it. |
420
451
  | `worklogEnabled` | `true` to run the built-in **dev worklog** batch (see below). Off by default (each run spawns an LLM session, so it costs tokens). |
421
452
  | `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). |
422
453
  | `terminalSubmit` | Which bytes Claude reads as **submit** vs **newline**: `"cr"` (default — Enter submits, Shift+Enter makes a newline) or `"esc-cr"` (for a Claude Code rebound the other way). Applies to the keyboard **and** the phone remote-view submit, for **Claude sessions only** (shell/codex keep plain Enter). See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#terminal-submit). |
@@ -781,7 +812,12 @@ Favorited collections get their own toolbar buttons.
781
812
  pages so you notice an off-screen cell that needs you.
782
813
  - **Zoom & filmstrip** — a cell's **⤢** enlarges one agent while the rest shrink to
783
814
  thumbnails in a bottom **filmstrip**; click a thumbnail to switch, **⤡** to return to the
784
- grid — so you can flip between "see everything" and "focus on one" in a click.
815
+ grid — so you can flip between "see everything" and "focus on one" in a click. While
816
+ zoomed, keys you bind walk the enlargement along the on-screen order without reaching for
817
+ the mouse — **opt-in, nothing is bound by default**, since any bound key is taken from the
818
+ terminal underneath. Add a `keymap` to `~/.mulmoterminal/config.json`; see the
819
+ [guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#keymap)
820
+ for the syntax, the action list, and combinations a browser can never bind.
785
821
 
786
822
  ![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)
787
823
 
@@ -996,15 +1032,19 @@ same-origin-guarded.
996
1032
 
997
1033
  | Endpoint | Purpose |
998
1034
  | -------- | ------- |
999
- | `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `userMcpServers`, `providers`). |
1035
+ | `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `quickCommands`, `userMcpServers`, `providers`). |
1000
1036
  | `GET /api/sound` · `/api/dir-sound?cwd=` · `/api/dir-config?cwd=` | Custom / per-directory attention sound + per-dir config. |
1001
1037
  | `GET /api/launch-options` | The Anthropic-compatible backends this server can reach, each with its models and — when it can't — the reason. Reports the **name** of the env var a key is read from, never the key. |
1002
1038
  | `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
1003
1039
  | `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
1004
1040
  | `POST /api/translation` | Runtime UI-string translation. |
1005
- | `GET /api/remote-host/status` · `POST /api/remote-host/{connect,disconnect}` | Companion phone-client link. |
1041
+ | `GET /api/remote-host/status` · `POST /api/remote-host/{connect,disconnect}` | Companion phone-client link. Each response carries the command channel's `health` (`online` / `reconnecting` / `offline`, plus the last listener error), so the toolbar shows a dropped channel instead of the last state it happened to fetch. |
1006
1042
  | `POST /api/open-dir` · `POST /api/pick-file` | Reveal a dir in Finder/Explorer; OS file-picker → path (`{ directory: true }` opens the folder picker — used by the launcher's Working-directory 📁 button). |
1007
1043
 
1044
+ The phone itself uses **none** of these routes — it reaches the host over Firestore command
1045
+ docs, not HTTP. Every command it can send, and the shapes it gets back, are in
1046
+ [`docs/remote-host-protocol.md`](docs/remote-host-protocol.md).
1047
+
1008
1048
  ### WebSocket: `/ws` (terminal)
1009
1049
 
1010
1050
  A raw WebSocket carrying the terminal stream for one session. One PTY per
@@ -1277,6 +1317,10 @@ src/
1277
1317
  composables/ useSessions, usePubSub, useGitStatus, useCost,
1278
1318
  useChatLauncher, useFilesView, useWikiBrowse,
1279
1319
  useCollectionBrowse, useNotifications, useVoiceInput, …
1320
+ common/ Shared by server/ and src/ — both tsconfigs include it, so a value or
1321
+ wire type either side decides from belongs HERE, never mirrored in both:
1322
+ dirChrome.ts, ghItems.ts, gitStatus.ts, launchOptions.ts, shortcuts.ts,
1323
+ sourceExtensions.ts, modelPresets.ts, modelIds.ts, theme*.ts, …
1280
1324
  vite.config.ts Dev proxy for /ws (+ /ws/codex, /ws/launch, /ws/run), /ws/pubsub, /api, /artifacts
1281
1325
  vitest.config.ts jsdom test environment
1282
1326
  ```
@@ -18,3 +18,18 @@ export interface DirChrome {
18
18
  dotColor: string | null;
19
19
  buttonColor: string | null;
20
20
  }
21
+
22
+ // "Nothing configured" — the base every DirConfig/PublicDirConfig empty spreads, so adding
23
+ // a field above can't leave one side's default silently missing. Readonly because it is now
24
+ // one object behind both sides' defaults, where it used to be a literal per file: a caller
25
+ // that assigned it somewhere mutable would corrupt every later "no config here".
26
+ export const EMPTY_DIR_CHROME: Readonly<DirChrome> = {
27
+ name: null,
28
+ badgeColor: null,
29
+ headerColor: null,
30
+ headerTextColor: null,
31
+ cellColor: null,
32
+ cellBorderColor: null,
33
+ dotColor: null,
34
+ buttonColor: null,
35
+ };
@@ -0,0 +1,15 @@
1
+ // Web config for the shared `mulmoserver` Firebase project, used by the remote-host
2
+ // command channel from BOTH sides: the server's session controller seeds its app with
3
+ // it, and the browser's does the same.
4
+ //
5
+ // The values are NOT secrets — they identify the project to the client SDK, and access
6
+ // is gated by Firestore security rules. Safe to commit. Firestore must be in Native mode.
7
+ export const firebaseConfig = {
8
+ apiKey: "AIzaSyC5IrhcCtfVQ4nZeI89Owa7da_D-It0b9s",
9
+ authDomain: "mulmoserver.firebaseapp.com",
10
+ projectId: "mulmoserver",
11
+ storageBucket: "mulmoserver.firebasestorage.app",
12
+ messagingSenderId: "830257137330",
13
+ appId: "1:830257137330:web:5cb8db01ae61b5d161abab",
14
+ measurementId: "G-Y75JGK1G4T",
15
+ } as const;
@@ -0,0 +1,43 @@
1
+ // What GET /api/prs and GET /api/issues answer with — the wire shapes shared across the
2
+ // build boundary so the server's `gh` aggregation and the PrsOverlay that renders it can't
3
+ // drift. The normalizing/rollup logic stays server-side (git/ghItem.ts, git/prs.ts).
4
+
5
+ /** The fields every `gh` list row shares: identity (number, url) and display text. */
6
+ export interface GhItemBase {
7
+ number: number;
8
+ title: string;
9
+ author: string;
10
+ updatedAt: string;
11
+ url: string;
12
+ }
13
+
14
+ export type IssueItem = GhItemBase;
15
+
16
+ /** Collapsed statusCheckRollup: any failure wins, else any unfinished check → pending. */
17
+ export type CiState = "passing" | "failing" | "pending" | "none";
18
+
19
+ export interface PrItem extends GhItemBase {
20
+ isDraft: boolean;
21
+ review: string | null; // gh reviewDecision (APPROVED / CHANGES_REQUESTED / REVIEW_REQUIRED / null)
22
+ ci: CiState;
23
+ }
24
+
25
+ export interface RepoPrs {
26
+ repo: string;
27
+ prs?: PrItem[];
28
+ error?: string;
29
+ // True when the repo has at least PR_LIMIT open PRs, so the list may be incomplete —
30
+ // surfaced in the UI so a truncated view isn't mistaken for full coverage.
31
+ truncated?: boolean;
32
+ }
33
+
34
+ export interface RepoIssues {
35
+ repo: string;
36
+ issues?: IssueItem[];
37
+ error?: string;
38
+ // True when the repo has more than ISSUE_LIMIT open issues, so the list is capped —
39
+ // the UI then links to the repo's issues page for the rest.
40
+ truncated?: boolean;
41
+ // The repo's GitHub issues page, used as the "see the rest" target when truncated.
42
+ url?: string;
43
+ }
@@ -0,0 +1,11 @@
1
+ // What GET /api/git-status answers with — the wire shape shared across the build
2
+ // boundary so the server's reader and the client's poller can't drift.
3
+ export interface GitStatus {
4
+ repo: boolean;
5
+ branch: string | null; // null when detached or non-repo
6
+ detached: boolean;
7
+ dirty: number; // uncommitted entries (incl. untracked)
8
+ ahead: number; // commits on HEAD not on the upstream
9
+ behind: number; // commits on the upstream not on HEAD
10
+ upstream: boolean; // HEAD has a tracking branch (ahead/behind are meaningful)
11
+ }
@@ -0,0 +1,13 @@
1
+ import { isObject } from "graphai";
2
+
3
+ // The guard both sides use before reading named fields off parsed JSON (config files, API
4
+ // responses, JSONL transcripts, request bodies). It was hand-copied into 29 files before this.
5
+ //
6
+ // Arrays are excluded deliberately, and that is not a nicety: TypeScript itself refuses
7
+ // `const r: Record<string, unknown> = []` ("index signature for type 'string' is missing"), so a
8
+ // guard that narrows an array to Record is telling the compiler something untrue — and callers
9
+ // that go on to `Object.entries()` the value would silently walk array indices as field names.
10
+ //
11
+ // graphai's own `isPlainObject` is exactly this (it also rejects Map/Date/class instances), but
12
+ // it is not exported from the package root, so the array check stays here.
13
+ export const isRecord = (value: unknown): value is Record<string, unknown> => isObject(value) && !Array.isArray(value);
@@ -0,0 +1,158 @@
1
+ // The user-defined keyboard shortcuts, in `~/.mulmoterminal/config.json` under `keymap`.
2
+ //
3
+ // There are NO defaults: an absent or empty `keymap` means the shortcuts are OFF. Every
4
+ // binding is something the user opted into, because any key this claims is a key the
5
+ // terminal underneath stops receiving — that trade is the user's to make, not ours.
6
+ //
7
+ // The server sanitizes and persists it; the browser matches keydowns against it. One
8
+ // definition here so the accepted syntax can't drift between the two.
9
+ //
10
+ // "keymap": { "zoom-next": "PageDown", "zoom-prev": "Shift+PageUp" }
11
+
12
+ // Actions a key can be bound to. Adding one here is all it takes for the config to accept it.
13
+ export const KEYMAP_ACTIONS = ["zoom-toggle", "zoom-next", "zoom-prev", "next-attention", "terminal-new", "terminal-new-adjacent", "terminal-close"] as const;
14
+ export type KeymapAction = (typeof KEYMAP_ACTIONS)[number];
15
+
16
+ export const isKeymapAction = (value: unknown): value is KeymapAction => typeof value === "string" && (KEYMAP_ACTIONS as readonly string[]).includes(value);
17
+
18
+ // action -> binding string. Absent action = unbound = that shortcut does nothing.
19
+ export type Keymap = Partial<Record<KeymapAction, string>>;
20
+
21
+ // A parsed binding. `key` is matched against `KeyboardEvent.key` exactly as the browser
22
+ // reports it, so it is case-sensitive for printable characters ("a" and "A" differ, the
23
+ // latter implying Shift).
24
+ export interface KeyBinding {
25
+ key: string;
26
+ shift: boolean;
27
+ alt: boolean;
28
+ ctrl: boolean;
29
+ meta: boolean;
30
+ }
31
+
32
+ // Modifier spellings accepted in a binding string, mapped to the flag they set. "Cmd",
33
+ // "Command" and "Meta" are the same modifier; "Option" is macOS's name for Alt.
34
+ const MODIFIERS: Record<string, keyof Omit<KeyBinding, "key">> = {
35
+ shift: "shift",
36
+ alt: "alt",
37
+ option: "alt",
38
+ ctrl: "ctrl",
39
+ control: "ctrl",
40
+ meta: "meta",
41
+ cmd: "meta",
42
+ command: "meta",
43
+ };
44
+
45
+ // Parse "Shift+PageUp" into its parts, or null when the string is malformed (empty, no
46
+ // key left after the modifiers, an unknown modifier, or a duplicate one). Callers treat
47
+ // null as "unbound" rather than throwing: a typo in a hand-edited config must cost the
48
+ // user that one shortcut, never the app.
49
+ export function parseKeyBinding(input: string): KeyBinding | null {
50
+ const parts = input.split("+").map((part) => part.trim());
51
+ if (parts.length === 0 || parts.some((part) => part === "")) return null;
52
+ const key = parts[parts.length - 1];
53
+ const binding: KeyBinding = { key, shift: false, alt: false, ctrl: false, meta: false };
54
+ for (const part of parts.slice(0, -1)) {
55
+ const flag = MODIFIERS[part.toLowerCase()];
56
+ if (!flag || binding[flag]) return null; // unknown, or named twice
57
+ binding[flag] = true;
58
+ }
59
+ // A lone modifier ("Shift") binds nothing usable.
60
+ return MODIFIERS[key.toLowerCase()] ? null : binding;
61
+ }
62
+
63
+ // The structural shape of a keydown a binding is matched against. A real KeyboardEvent
64
+ // satisfies it, and so does a plain test object — no DOM dependency.
65
+ export interface KeymapKeyEvent {
66
+ key: string;
67
+ shiftKey: boolean;
68
+ altKey: boolean;
69
+ ctrlKey: boolean;
70
+ metaKey: boolean;
71
+ }
72
+
73
+ // Every modifier must match exactly, so a binding on "PageDown" does NOT fire for
74
+ // Shift+PageDown — that keystroke stays with the terminal (xterm's scrollback) unless the
75
+ // user binds it too.
76
+ export const matchesBinding = (binding: KeyBinding, e: KeymapKeyEvent): boolean =>
77
+ e.key === binding.key && e.shiftKey === binding.shift && e.altKey === binding.alt && e.ctrlKey === binding.ctrl && e.metaKey === binding.meta;
78
+
79
+ // The action this keydown is bound to, or null. Bindings that fail to parse are skipped.
80
+ export function actionForKey(keymap: Keymap, e: KeymapKeyEvent): KeymapAction | null {
81
+ for (const action of KEYMAP_ACTIONS) {
82
+ const raw = keymap[action];
83
+ if (raw === undefined) continue;
84
+ const binding = parseKeyBinding(raw);
85
+ if (binding && matchesBinding(binding, e)) return action;
86
+ }
87
+ return null;
88
+ }
89
+
90
+ // What is wrong with one `keymap` entry. `fatal` separates a typo the user clearly meant to
91
+ // work (a binding we cannot parse — the shortcut would silently never fire) from an action
92
+ // name we simply do not know, which is what a config written for a NEWER version looks like
93
+ // and must stay loadable.
94
+ export interface KeymapProblem {
95
+ action: string;
96
+ binding: unknown;
97
+ reason: string;
98
+ fatal: boolean;
99
+ }
100
+
101
+ // Report everything wrong with a `keymap`, for a caller that wants to tell the user instead
102
+ // of quietly dropping entries. Pure: the caller decides whether to warn, throw, or exit.
103
+ export function validateKeymap(input: unknown): KeymapProblem[] {
104
+ if (input === undefined || input === null) return [];
105
+ if (typeof input !== "object" || Array.isArray(input)) {
106
+ return [{ action: "keymap", binding: input, reason: "`keymap` must be an object of action -> key binding", fatal: true }];
107
+ }
108
+ const entries = Object.entries(input as Record<string, unknown>);
109
+ const bound = new Map<string, { action: KeymapAction; binding: string }[]>();
110
+ const problems = entries.flatMap(([action, binding]): KeymapProblem[] => {
111
+ if (!isKeymapAction(action)) {
112
+ return [{ action, binding, reason: `unknown action (known: ${KEYMAP_ACTIONS.join(", ")})`, fatal: false }];
113
+ }
114
+ if (typeof binding !== "string") return [{ action, binding, reason: "binding must be a string", fatal: true }];
115
+ const parsed = parseKeyBinding(binding);
116
+ if (parsed === null) {
117
+ return [{ action, binding, reason: 'unparseable key binding — expected e.g. "PageDown" or "Shift+PageUp"', fatal: true }];
118
+ }
119
+ // Grouped as PARSED, since "Shift+PageUp" and "shift+pageup" are one keystroke.
120
+ const key = canonicalBinding(parsed);
121
+ bound.set(key, [...(bound.get(key) ?? []), { action, binding }]);
122
+ return [];
123
+ });
124
+ return [...problems, ...duplicateWarnings(bound)];
125
+ }
126
+
127
+ // Two actions on one keystroke: only one can fire, so the others silently never work.
128
+ //
129
+ // The winner is decided the way `actionForKey` decides it — first in KEYMAP_ACTIONS order —
130
+ // NOT by which came first in the config file. Reporting the config-order winner would name
131
+ // the wrong action whenever the two orders disagree, which is worse than not naming one.
132
+ function duplicateWarnings(bound: Map<string, { action: KeymapAction; binding: string }[]>): KeymapProblem[] {
133
+ return [...bound.values()].flatMap((claims) => {
134
+ if (claims.length < 2) return [];
135
+ const byDispatch = [...claims].sort((a, b) => KEYMAP_ACTIONS.indexOf(a.action) - KEYMAP_ACTIONS.indexOf(b.action));
136
+ const [winner, ...losers] = byDispatch;
137
+ return losers.map(({ action, binding }) => ({
138
+ action,
139
+ binding,
140
+ reason: `same keystroke as \`${winner.action}\` — only \`${winner.action}\` will fire`,
141
+ fatal: false,
142
+ }));
143
+ });
144
+ }
145
+
146
+ // A binding's identity as a keystroke, for spotting two actions that claim the same one.
147
+ const canonicalBinding = (b: KeyBinding): string => `${b.shift ? "S" : ""}${b.alt ? "A" : ""}${b.ctrl ? "C" : ""}${b.meta ? "M" : ""}|${b.key}`;
148
+
149
+ // Keep only known actions bound to a parseable, non-empty string. Unknown keys and
150
+ // malformed bindings are dropped rather than rejecting the whole map, matching how the
151
+ // rest of the config treats one bad entry.
152
+ export function sanitizeKeymap(input: unknown): Keymap {
153
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return {};
154
+ const entries = Object.entries(input as Record<string, unknown>).filter(
155
+ (entry): entry is [KeymapAction, string] => isKeymapAction(entry[0]) && typeof entry[1] === "string" && parseKeyBinding(entry[1]) !== null,
156
+ );
157
+ return Object.fromEntries(entries);
158
+ }
@@ -0,0 +1,15 @@
1
+ // What the phone may ask the grid to start in a session's directory (#831). The host
2
+ // validates against this list and the grid turns each into a cell, so both sides read it.
3
+ //
4
+ // Deliberately NOT the configured `launchers` list: those are arbitrary user commands, and
5
+ // the phone picks from a fixed set the host can name. "shell" is the OS default shell, the
6
+ // same thing the header's new-terminal button opens.
7
+ export const LAUNCH_AGENTS = ["shell", "claude", "codex"] as const;
8
+
9
+ export type LaunchAgent = (typeof LAUNCH_AGENTS)[number];
10
+
11
+ export const isLaunchAgent = (value: unknown): value is LaunchAgent => LAUNCH_AGENTS.some((agent) => agent === value);
12
+
13
+ // The pub/sub channel the grid listens on for these requests. The grid is browser state, so
14
+ // the host cannot open a cell itself — it asks whichever tab is connected.
15
+ export const LAUNCH_TERMINAL_CHANNEL = "launch-terminal";
@@ -0,0 +1,24 @@
1
+ // What GET /api/launch-options answers with: the backends this server can actually reach
2
+ // right now, each with the models it can run (#584). The wire shape lives here so the
3
+ // server's decision (config/launch-options.ts) and the client's picker can't drift.
4
+ import type { ModelPreset } from "./modelPresets.js";
5
+
6
+ export interface LaunchProviderOption {
7
+ id: string;
8
+ label: string;
9
+ // False when this server cannot start a session on it as configured.
10
+ ready: boolean;
11
+ // Why not, in resolveProvider's own words. Absent when ready.
12
+ reason?: string;
13
+ // The env var this provider's key is read from — the help needs to name it, and it is
14
+ // the NAME, never the value.
15
+ tokenEnv: string;
16
+ models: ModelPreset[];
17
+ }
18
+
19
+ export interface LaunchOptions {
20
+ providers: LaunchProviderOption[];
21
+ // True when at least one provider can be launched on. The picker hides itself otherwise:
22
+ // with no reachable backend there is nothing to choose between, only setup to explain.
23
+ anyReady: boolean;
24
+ }
@@ -0,0 +1,21 @@
1
+ // The kinds of Web Push a session can raise. The server decides which one a hook warrants and
2
+ // the settings UI offers them as checkboxes, so the list is a value both sides read.
3
+
4
+ // Every kind that exists.
5
+ // finished — the turn ended, output is waiting to be reviewed.
6
+ // waiting — the agent is blocked on input (a permission prompt or a question), so answering
7
+ // from the phone unblocks real work. Fires once per prompt, which on a long task
8
+ // that asks repeatedly is what makes pushes feel frequent (#850).
9
+ export const PUSH_KINDS = ["finished", "waiting"] as const;
10
+
11
+ export type PushKind = (typeof PUSH_KINDS)[number];
12
+
13
+ // What a config with no `pushKinds` gets. Deliberately a SEPARATE list from PUSH_KINDS, not a
14
+ // copy of it: a kind added later must NOT start notifying people who never asked for it.
15
+ //
16
+ // That is exactly how this setting came to be needed — `waiting` was added in 36e9e72 and
17
+ // every user with push on began receiving it, with no way to decline. Add a new kind to
18
+ // PUSH_KINDS so it can be switched on, and leave it out of here so it stays opt-in.
19
+ export const DEFAULT_PUSH_KINDS: PushKind[] = ["finished", "waiting"];
20
+
21
+ export const isPushKind = (value: unknown): value is PushKind => PUSH_KINDS.some((kind) => kind === value);
@@ -0,0 +1,13 @@
1
+ import type { SessionAgent } from "./sessionAgent.js";
2
+
3
+ // A phrase the user sends often, offered as a chip on the phone's terminal view (#830).
4
+ // The server validates and stores it; the settings UI edits it — both decide from this
5
+ // shape, so it is defined once here rather than mirrored on each side.
6
+ //
7
+ // `label` is the chip's face and `text` what it inserts, so a short chip can carry a long
8
+ // sentence. `agents` scopes it to session kinds; omitted means every kind.
9
+ export interface QuickCommand {
10
+ label: string;
11
+ text: string;
12
+ agents?: SessionAgent[];
13
+ }
@@ -0,0 +1,30 @@
1
+ // Health of the remote-host command channel, as reported by the server's resilient
2
+ // runner and rendered by the toolbar control. Shared across the build boundary so the
3
+ // two sides cannot drift on the state names.
4
+ //
5
+ // online — the Firestore subscription is up; the phone can reach this host
6
+ // reconnecting — it died and is being re-subscribed with backoff (self-healing)
7
+ // offline — re-subscribing stopped helping, or nothing is connected at all;
8
+ // recovering needs a re-auth from the browser's parked session
9
+
10
+ import { isRecord } from "./isRecord.js";
11
+ export const RUNNER_HEALTH_STATES = ["online", "reconnecting", "offline"] as const;
12
+ export type RunnerHealthState = (typeof RUNNER_HEALTH_STATES)[number];
13
+
14
+ export interface RunnerHealth {
15
+ state: RunnerHealthState;
16
+ /** Last listener error seen, for the popover and the log. Null before the first one. */
17
+ lastError: string | null;
18
+ /** ms epoch of the last state change, so the UI can say how long it has been down. */
19
+ changedAt: number;
20
+ }
21
+
22
+ export const isRunnerHealthState = (value: unknown): value is RunnerHealthState => RUNNER_HEALTH_STATES.some((state) => state === value);
23
+
24
+ /** Narrows a parsed HTTP payload. The client renders whatever this accepts, so a
25
+ * half-shaped health has to read as "no health reported" rather than as a state. */
26
+ export const isRunnerHealth = (value: unknown): value is RunnerHealth =>
27
+ isRecord(value) &&
28
+ isRunnerHealthState(value.state) &&
29
+ (value.lastError === null || typeof value.lastError === "string") &&
30
+ typeof value.changedAt === "number";
@@ -0,0 +1,8 @@
1
+ // What a remote-host session is running. The server decides it (from the PtyEntry, or from
2
+ // what tmux says is in the pane) and the settings UI offers it as the scope for a quick
3
+ // command, so the list of kinds is a value both sides read.
4
+ export const SESSION_AGENTS = ["claude", "codex", "shell"] as const;
5
+
6
+ // Null anywhere this appears means the host cannot tell — a session that outlived a restart
7
+ // exists only in tmux, and nothing recorded what launched it. That is distinct from "shell".
8
+ export type SessionAgent = (typeof SESSION_AGENTS)[number];
@@ -0,0 +1,27 @@
1
+ // Launcher shortcut (a pinned collection / feed), shared across the build boundary: the
2
+ // server reads and writes `<workspace>/config/shortcuts.json`, the browser's store,
3
+ // PinToggle and the toolbar launcher render it.
4
+ //
5
+ // MulmoClaude and MulmoTerminal SHARE that file — favoriting a collection in one app must
6
+ // show up in the other — so the on-disk format is the contract: an OBJECT WRAPPER
7
+ // `{ shortcuts: Shortcut[] }`, not a bare array, matching mulmoclaude/src/types/shortcuts.ts.
8
+ // Keep this type in sync with MulmoClaude's.
9
+
10
+ export const SHORTCUT_KINDS = ["collection", "feed"] as const;
11
+ export type ShortcutKind = (typeof SHORTCUT_KINDS)[number];
12
+
13
+ export interface Shortcut {
14
+ /** Which route family — drives the launcher's navigation target. */
15
+ kind: ShortcutKind;
16
+ /** The target collection / feed slug. */
17
+ slug: string;
18
+ /** Cached display label (user-named) — refreshed on reconcile. */
19
+ title: string;
20
+ /** Cached material-symbols glyph — refreshed on reconcile. */
21
+ icon: string;
22
+ }
23
+
24
+ /** True when two shortcuts target the same thing (the dedupe key). */
25
+ export function sameShortcut(left: Pick<Shortcut, "kind" | "slug">, right: Pick<Shortcut, "kind" | "slug">): boolean {
26
+ return left.kind === right.kind && left.slug === right.slug;
27
+ }
@@ -0,0 +1,61 @@
1
+ // Extensions a browser can only ever show as SOURCE — the code, style, config and log
2
+ // files a terminal actually prints paths to.
3
+ //
4
+ // Both sides of the file-path link decide something from this set, and the two decisions
5
+ // are different, so this is the shared CORE rather than either side's whole answer:
6
+ // - the server serves these as text/plain so they VIEW instead of downloading
7
+ // (backends/rawServingPlan.ts, which also covers docs, markup and dotfiles), and
8
+ // - the client opens these in the app's own Files view instead of a new tab
9
+ // (composables/terminalFilePathLinkProvider.ts, which also covers .txt).
10
+ // Each side adds its own extras next to its import; anything that belongs to only one of
11
+ // them does NOT belong here.
12
+ //
13
+ // Adding an extension changes what a clicked path DOES, which is documented — see the DOCS
14
+ // block in composables/terminalFilePathLinkProvider.ts for the three files to update (#834).
15
+ export const SOURCE_CODE_EXTENSIONS = [
16
+ ".ts",
17
+ ".tsx",
18
+ ".js",
19
+ ".jsx",
20
+ ".mjs",
21
+ ".cjs",
22
+ ".vue",
23
+ ".svelte",
24
+ ".astro",
25
+ ".py",
26
+ ".rb",
27
+ ".go",
28
+ ".rs",
29
+ ".java",
30
+ ".kt",
31
+ ".c",
32
+ ".h",
33
+ ".cpp",
34
+ ".cc",
35
+ ".hpp",
36
+ ".cs",
37
+ ".php",
38
+ ".swift",
39
+ ".scala",
40
+ ".lua",
41
+ ".sql",
42
+ ".sh",
43
+ ".bash",
44
+ ".zsh",
45
+ ".fish",
46
+ ".yml",
47
+ ".yaml",
48
+ ".toml",
49
+ ".ini",
50
+ ".cfg",
51
+ ".conf",
52
+ ".css",
53
+ ".scss",
54
+ ".sass",
55
+ ".less",
56
+ ".xml",
57
+ ".jsonc",
58
+ ".log",
59
+ ".diff",
60
+ ".patch",
61
+ ] as const;
@@ -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-DfwxZrpZ.js";import"./chunk-WYO6CB5R-BlzOfotS-ozBfm4Ec.js";import"./chunk-VAUOI2AC-CS9QJ4yz-N2n8rUqR.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-zF0QMt5P.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-D1ef8DoR.js";import"./chunk-WYO6CB5R-BlzOfotS-qckQG4MR.js";import"./chunk-VAUOI2AC-CS9QJ4yz-DviHNtmN.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-CG59XkeN.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-D1ef8DoR.js";export{e as createArchitectureServices};