mulmoterminal 2.0.1 → 2.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.
Files changed (121) hide show
  1. package/README.md +28 -10
  2. package/bin/mulmoterminal.js +26 -7
  3. package/common/cockpitLines.ts +46 -0
  4. package/common/dirChrome.ts +6 -1
  5. package/common/terminalFontSize.ts +20 -0
  6. package/common/voiceInputStatus.ts +21 -0
  7. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-CMyzg2zY.js → abnfDiagram-VRR7QNED-RjiYivmv-1RrSDjxg.js} +1 -1
  8. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-CtE1YID5.js +1 -0
  9. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-CSIT7RtO.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-sTMTrNOq.js} +1 -1
  10. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-CnVQRl8Q.js → blockDiagram-677ZJIJ3-BPuAJQRW-CQmbTR0O.js} +1 -1
  11. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-CrpIWU_7.js → c4Diagram-LMCZKHZV-C0LAqQso-kjKqpuxO.js} +1 -1
  12. package/dist/assets/channel-7wUqSdoX-i8mXF_QX.js +1 -0
  13. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-D_PiqrVh.js → chunk-32BRIVSS-BRrYpgtb-YnuI_kVF.js} +1 -1
  14. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-BCpG-w5L.js → chunk-52WLFC77-BJ-ss3Xr-Dv-_qNms.js} +1 -1
  15. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-CP9wac26.js → chunk-C7G6YPKG-BZEucKEL-VvGb94ad.js} +1 -1
  16. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-BJZF7tEb.js → chunk-EX3LRPZG-DLS6FBN1-Cv9JdwrA.js} +1 -1
  17. package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-Cnk0SpZp.js → chunk-FWX5IMBZ-DHLSFw1H-CFK4Xl3Y.js} +2 -2
  18. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-CRGS9q8z.js → chunk-HOUHSVGY-Bhlt8hXJ-l9dGXTcV.js} +1 -1
  19. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-DpPUV9Tl.js → chunk-ICXQ74PX-DwgHBX_g-oBhC9ghH.js} +1 -1
  20. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-CG59XkeN.js → chunk-MOJQB5TN-CMZRaeqt-CKo27qok.js} +1 -1
  21. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-B59YGczL.js → chunk-OGEWGWER-8Qy4a8b5-Di7gzOY_.js} +1 -1
  22. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-CXGtfuoa.js → chunk-PUDLZKDR-DcrWQRYh-DuMYBLYy.js} +1 -1
  23. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-eHuKxxBN.js → chunk-Q4XR5HBZ-ZXVGkG8Z-BH8ZxP7u.js} +1 -1
  24. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-BJZa8auK.js → chunk-V7JOEXUC-DmGdheTX-e5Lem5jk.js} +1 -1
  25. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-DviHNtmN.js → chunk-VAUOI2AC-CS9QJ4yz-CfgcnwuB.js} +1 -1
  26. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-DsA9hbD5.js → chunk-VR4S4FIN-C6a91eNY-BcBE3OXA.js} +1 -1
  27. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-qckQG4MR.js → chunk-WYO6CB5R-BlzOfotS-BRwSb4rl.js} +1 -1
  28. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-Cr8j6FMw.js → chunk-ZGVPDNZ5-BYwxNFTK-yy_t_DUF.js} +1 -1
  29. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-BM_zLSMQ.js +1 -0
  30. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-BM_zLSMQ.js +1 -0
  31. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-BBb6pP0M.js +1 -0
  32. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--BuPVUHaQ.js → cynefinDiagram-TSTJHNR4-DHA9iPo--CEHib9RV.js} +1 -1
  33. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-l8CP-U85.js → dagre-VKFMJZFB--oJKqXBZ-DufbQ1ld.js} +1 -1
  34. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-CV6yO6d6.js → diagram-FQU43EPY-CcJCB9bG-Bcp75QDM.js} +1 -1
  35. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-DmUMCyqP.js → diagram-G47NLZAW-C_o-WGG1-C_2FVIX_.js} +1 -1
  36. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--D1Zcc9sH.js → diagram-NH7WQ7WH-CXJCYvY--B1kVCM4I.js} +1 -1
  37. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-CsC2S-ug.js → diagram-OA4YK3LP-BMzeJ87A-Bd1BRg0M.js} +1 -1
  38. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-C3Xq1urQ.js → diagram-WEI45ONY-D_93NKqo-VJzlhYeh.js} +1 -1
  39. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-BtjcwSW_.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-BXziXSdO.js} +1 -1
  40. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-DhQMPhj9.js → erDiagram-Q63AITRT-CiABnA0s-DzjaMys9.js} +1 -1
  41. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-C1_24lZu.js +1 -0
  42. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-DQrHefx7.js +1 -0
  43. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-Bw2aYbCJ.js → ganttDiagram-NO4QXBWP-DQZvdFo1-DqlM0R3b.js} +1 -1
  44. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DSTb91Xn.js +1 -0
  45. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-C1Dlsswx.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-Dy5WrrGR.js} +1 -1
  46. package/dist/assets/{index-Dwc5wfJg.js → index-D4LB9YoW.js} +196 -196
  47. package/dist/assets/index-u-r21U2t.css +1 -0
  48. package/dist/assets/info-DKCQHKI2-Dplx5kMp-Cdy5Xn2F.js +1 -0
  49. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-CjB00vD7.js → infoDiagram-FWYZ7A6U-7UnoB5AP-ByceYPA5.js} +1 -1
  50. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-BxVGf9t3.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-BZEvgyxt.js} +1 -1
  51. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-BPLsgkBp.js → journeyDiagram-5HDEW3XC-c5xIah9o-D3IMe-Kx.js} +1 -1
  52. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-C0UF5ruR.js → kanban-definition-HUTT4EX6-Cnt6loYD-cjfYxgHr.js} +1 -1
  53. package/dist/assets/{lib-Ca9R1hlX.js → lib-BP5ftgj4.js} +1 -1
  54. package/dist/assets/{line-D7ziSjKi-B4ejYAqw.js → line-D7ziSjKi-C07Dlbdw.js} +1 -1
  55. package/dist/assets/{marp-J4wSkl3Q.js → marp-ytjNgY2U.js} +1 -1
  56. package/dist/assets/{mermaid-parser.core-DxEa8E3F-D1ef8DoR.js → mermaid-parser.core-DxEa8E3F-BAh0ySn4.js} +2 -2
  57. package/dist/assets/{mermaid.core-V0OYwIz3-BSn9HyWz.js → mermaid.core-V0OYwIz3-CIFgdt3N.js} +3 -3
  58. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-BLTr4gMi.js → mindmap-definition-LN4V7U3C-CexN3O6L-1SEo71tq.js} +1 -1
  59. package/dist/assets/packet-7NZHBO7P-CQI3flND-BFv5gKuv.js +1 -0
  60. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-E_eG9LVV.js → pegDiagram-2B236MQR-BiM4G0cW-DkewOOUw.js} +1 -1
  61. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-9hvyyDzu.js +1 -0
  62. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-f4ZSZ6GC.js → pieDiagram-ENE6RG2P-B_fBBS-2-DdJe9KqM.js} +1 -1
  63. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-DnwDJxmc.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-C8kNdasc.js} +1 -1
  64. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-DBeFDuLF.js +1 -0
  65. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-FnTYQEAW.js +1 -0
  66. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-Bo4xQewA.js +1 -0
  67. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-YFFxMVbA.js +1 -0
  68. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DMzqIAqS.js +1 -0
  69. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-BUH_ocB5.js → railroadDiagram-RFXS5EU6--lNIlG64-DawLoSCW.js} +1 -1
  70. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-CT5zmLHk.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-BMFbGAmA.js} +1 -1
  71. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-9sorx_WA.js → sankeyDiagram-HTMAVEWB-DlutaXDv-Dwt5PZ2I.js} +1 -1
  72. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-CnjR9ja7.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-yb89czG2.js} +1 -1
  73. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-DboFu0BL.js → stateDiagram-2N3HPSRC-jf_dXUEU-kqvWM0jR.js} +1 -1
  74. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-QnrqYDdZ.js +1 -0
  75. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-Cqxsp00w.js → swimlanes-5IMT3BWC-CYjtALQH-CiVWAsob.js} +1 -1
  76. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-CE7hrF_H.js +8 -0
  77. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-Du08XOrV.js → timeline-definition-FHXFAJF6-DYJ4oUm8-DrqIflbo.js} +1 -1
  78. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DaEUFpbs.js +1 -0
  79. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-Cb7ZUpob.js +1 -0
  80. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-DtBtukcO.js → vennDiagram-L72KCM5P-CdKAHoek-COepU8y_.js} +1 -1
  81. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-BON96qJv.js +1 -0
  82. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-CW4nfoJ3.js → wardleyDiagram-EHGQE667-BF6c4_CW-Z2AgrgfV.js} +1 -1
  83. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-Ruux6STV.js → xychartDiagram-FW5EYKEG-CGiKngj7-ChC7g8sJ.js} +1 -1
  84. package/dist/index.html +2 -2
  85. package/package.json +1 -1
  86. package/server/backends/whisper.ts +3 -5
  87. package/server/config/app-config.ts +41 -15
  88. package/server/config/config-routes.ts +13 -0
  89. package/server/config/config-schema.ts +12 -0
  90. package/server/config/dir-config.ts +18 -2
  91. package/server/config/header-config.ts +4 -4
  92. package/server/git/pr-footer.ts +20 -0
  93. package/server/git/worktree-pr.ts +32 -4
  94. package/server/index.ts +1 -1
  95. package/server/infra/tmux-routes.ts +1 -1
  96. package/server/session/pty-connection.ts +1 -1
  97. package/server/session/scheduled-sessions.ts +2 -2
  98. package/server/skills/mulmoterminal-config/SKILL.md +45 -12
  99. package/server/skills/mulmoterminal-config/palettes.json +6 -6
  100. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-3kpQCFcP.js +0 -1
  101. package/dist/assets/channel-7wUqSdoX-D8_GjCXH.js +0 -1
  102. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-axLhYHqO.js +0 -1
  103. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-axLhYHqO.js +0 -1
  104. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-C9DtePCD.js +0 -1
  105. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-BCX3cvpw.js +0 -1
  106. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-DjZRfq7e.js +0 -1
  107. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-pIAqsAx0.js +0 -1
  108. package/dist/assets/index-DVTKApMs.css +0 -1
  109. package/dist/assets/info-DKCQHKI2-Dplx5kMp-S1We2Nw2.js +0 -1
  110. package/dist/assets/packet-7NZHBO7P-CQI3flND-DaLVcMu_.js +0 -1
  111. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-BKDCOtxh.js +0 -1
  112. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-Cw4aJNVl.js +0 -1
  113. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-DJZYlsdK.js +0 -1
  114. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-D9wfK70u.js +0 -1
  115. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-C2rW9Qx8.js +0 -1
  116. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CCUOq6go.js +0 -1
  117. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-FGJ3mpM7.js +0 -1
  118. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-B3qFCxjl.js +0 -8
  119. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-dIhNDmqB.js +0 -1
  120. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-C8SEy1d3.js +0 -1
  121. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-SYQlwZRo.js +0 -1
package/README.md CHANGED
@@ -70,7 +70,7 @@ output you haven't seen) — driven by Claude/Codex activity hooks the server in
70
70
 
71
71
  ![Single view — one agent in focus, terminal on the left and a GUI panel on the right](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/single-view.png)
72
72
 
73
- *Besides the grid there's a **single view** for focusing on one agent: the conversation/terminal on the left, and a **GUI panel** ("Canvas") on the right where the agent's tool calls render as documents, forms, charts, images, and HTML — not just printed text. Switch between the two with the chat / grid icons in the toolbar.*
73
+ *Besides the grid there's a **single view** for focusing on one agent: the conversation/terminal on the left, and a **GUI panel** ("Canvas") on the right where the agent's tool calls render as documents, forms, charts, images, and HTML — not just printed text. Switch between the two with the chat / grid icons in the toolbar. **The app opens on the grid** (`/`); the single view has its own URL, `/chat`, so you can bookmark either.*
74
74
 
75
75
  **Inserting a file path** — like a native terminal, you can put a file's absolute path into
76
76
  the prompt: **drag a file** onto the terminal (works where the browser exposes the path via
@@ -111,11 +111,23 @@ dotfiles are server-only. The 45 extensions both sides agree on live in
111
111
 
112
112
  ## Install & run
113
113
 
114
- Requires the [`claude`](https://claude.com/claude-code) CLI on your `PATH` and
115
- **Node ≥ 22.9**. Optional but recommended: **`tmux`** so terminals survive a server
116
- restart (see [Session persistence (tmux)](#session-persistence-tmux)), the **`gh`** CLI
117
- logged in for the PRs/Issues view and one-click PR creation, and — for Codex sessions —
118
- the **`codex`** CLI on your `PATH`.
114
+ Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
115
+
116
+ | | Tool | What it gives you | Install |
117
+ | --- | --- | --- | --- |
118
+ | **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 |
119
+ | **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) |
120
+ | **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` |
121
+ | 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) |
122
+ | Optional | `codex` | [Codex sessions](#agents-claude--codex) in a cell, alongside Claude | `npm i -g @openai/codex` |
123
+ | Optional | `docker` | the experimental [Docker sandbox](#docker-sandbox-experimental-single-view) | [docs.docker.com](https://docs.docker.com/get-started/get-docker/) |
124
+ | 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` |
125
+ | Optional | `ollama` | [`claude-ollama`](https://receptron.github.io/mulmoterminal/guide/en/claude-ollama.html) — Claude Code against a fully local model | [ollama.com/download](https://ollama.com/download) |
126
+
127
+ The server starts without any of the non-required rows; you just lose that row's feature,
128
+ and the header/panel for it says so. `git` and `gh` are marked required because losing them
129
+ costs whole views rather than one button. `npx mulmoterminal init` (below) reports which of
130
+ these it can find.
119
131
 
120
132
  ```bash
121
133
  npx mulmoterminal # start on http://localhost:34567 and open the browser
@@ -124,8 +136,8 @@ npm install -g mulmoterminal
124
136
  mulmoterminal
125
137
  ```
126
138
 
127
- **First-run setup (optional).** `npx mulmoterminal init` checks your environment (Node ≥ 22.9,
128
- the `claude` CLI, plus optional `tmux` / `gh` / `codex`), seeds the launcher's **directory
139
+ **First-run setup (optional).** `npx mulmoterminal init` checks your environment (Node ≥ 22.9
140
+ and every CLI in the table above), seeds the launcher's **directory
129
141
  presets** from the projects in your Claude Code history, and writes `~/.mulmoterminal/config.json`.
130
142
  It's **idempotent** — re-run it any time to refresh the presets; it overwrites the managed parts
131
143
  and keeps your other settings. When `claude` is installed it can hand off to the
@@ -434,7 +446,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
434
446
 
435
447
  ![The Settings modal — theme, notification sound, PR repos, launch commands, and MCP servers](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/settings.png)
436
448
 
437
- *Open it from the ⚙ button in the toolbar. Pick a **theme**, set a custom **attention sound**, list the repos the cross-repo **PRs & Issues** view should aggregate, add **launch commands** for grid cells, and register your own **MCP servers** — no need to hand-edit the config file.*
449
+ *Open it from the ⚙ button in the toolbar. Pick a **theme**, set the **terminal font size**, set a custom **attention sound**, list the repos the cross-repo **PRs & Issues** view should aggregate, add **launch commands** for grid cells, and register your own **MCP servers** — no need to hand-edit the config file. Note that **theme and font size are stored per browser** (they're display preferences, so a phone and a desktop keep their own); the rest live in `~/.mulmoterminal/config.json` and are shared by every client.*
438
450
 
439
451
  | Field | Meaning |
440
452
  | ------------ | ------- |
@@ -451,6 +463,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
451
463
  | `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). |
452
464
  | `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). |
453
465
  | `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). |
466
+ | `prWorkdirFooter` | Ends the body of a PR **⧉ Open PR** creates with `work in <clone>` — the directory name of the clone the work happened in, so a PR says which of several side-by-side checkouts produced it. **On by default**; set `false` to opt out — read from the file per PR, so no restart is needed (there is no Settings control for it). Only applied to PRs this app creates (pressing the button again on an existing PR never re-appends). |
454
467
 
455
468
  #### Header buttons
456
469
 
@@ -527,6 +540,7 @@ malformed file is ignored.
527
540
  "buttonColor": "#c7cdf0", // header icon buttons (hex #rrggbb)
528
541
  "theme": "nord", // terminal palette: midnight | nord | daylight | solarized
529
542
  "colors": { "background": "#190a23", "cursor": "#ff2e63" }, // per-key palette overrides
543
+ "fontSize": 16, // terminal font size in px (8–32); overrides Settings
530
544
  "sound": "./.mulmoterminal/alert.mp3" // attention sound, RELATIVE to this directory
531
545
  }
532
546
  ```
@@ -547,6 +561,7 @@ malformed file is ignored.
547
561
  | `buttonColor` | Header **icon button** color (`#rrggbb`) — expand / close / attach / folder / etc., across both header rows. |
548
562
  | `theme` | xterm palette for terminals in this directory (one of the built-in theme ids). |
549
563
  | `colors` | Per-key xterm palette overrides applied on top of `theme` (or the app theme when `theme` is unset). Keys are xterm `ITheme` names (`background`, `foreground`, `cursor`, `selectionBackground`, the 16 ANSI colors, …); values are hex (`#rgb` / `#rrggbb` / `#rrggbbaa`). Unknown keys / bad values are dropped. |
564
+ | `fontSize` | Terminal font size in px for this directory (8–32), overriding the Settings value. A size outside the range is clamped; a non-number is ignored. Changing it re-fits the terminal, so the PTY learns the new width — unlike browser zoom, which leaves the two disagreeing. |
550
565
  | `sound` | Attention sound for this directory's sessions, a path **relative to the directory** (served at `GET /api/dir-sound`). |
551
566
 
552
567
  **Security.** `sound` is a directory-relative path only — absolute paths and any
@@ -835,7 +850,10 @@ Favorited collections get their own toolbar buttons.
835
850
  - **Notifications** (🔔) — a toolbar bell with an unread badge and a dropdown of active
836
851
  notifications; click a row to jump to its session.
837
852
  - **Voice input** — dictate a prompt via on-device Whisper (`POST /api/transcribe`, macOS
838
- only; the model downloads on first use).
853
+ only; the model downloads on first use). Settings picks **the language you dictate in**
854
+ (per browser): your browser's, whisper's own per-clip detection, or a fixed one. Worth
855
+ setting — speech in a language the mic is not expecting comes back *translated* into the
856
+ one it is, so an English browser silently turned Japanese dictation into English.
839
857
  - **Remote host** — link MulmoTerminal to the companion phone client (Google sign-in) to
840
858
  watch and start sessions from your phone.
841
859
  - **Themes** — four terminal palettes (midnight / nord / daylight / solarized), your pick
@@ -76,6 +76,31 @@ function claudeInstalled() {
76
76
  return hasCommand("claude");
77
77
  }
78
78
 
79
+ // PATH tools the app shells out to; mirrors the requirements table in README.md. `required`
80
+ // ones back the core grid — without them a developer loses whole views rather than one
81
+ // feature — so a miss is an ✗, not an ○.
82
+ const PATH_TOOLS = [
83
+ { cmd: "git", versionArg: "--version", required: true, why: "worktrees, per-cell branch/diff, PR footer", hint: "brew install git · apt install git" },
84
+ { cmd: "gh", versionArg: "--version", required: true, why: "PRs & Issues view + one-click PRs", hint: "https://cli.github.com (then: gh auth login)" },
85
+ { cmd: "tmux", versionArg: "-V", required: false, why: "sessions survive a restart", hint: "brew install tmux · apt install tmux" },
86
+ { cmd: "codex", versionArg: "--version", required: false, why: "run OpenAI Codex as an agent", hint: "npm install -g @openai/codex" },
87
+ { cmd: "docker", versionArg: "--version", required: false, why: "the experimental Docker sandbox", hint: "https://docs.docker.com/get-started/get-docker/" },
88
+ {
89
+ cmd: "ffmpeg",
90
+ versionArg: "-version",
91
+ required: false,
92
+ why: "video rendering in the mulmo-script panel",
93
+ hint: "brew install ffmpeg · apt install ffmpeg",
94
+ },
95
+ { cmd: "ollama", versionArg: "--version", required: false, why: "a fully local model via claude-ollama", hint: "https://ollama.com/download" },
96
+ ];
97
+
98
+ function toolCheckLine({ cmd, versionArg, required, why, hint }) {
99
+ if (hasCommand(cmd, versionArg)) return ` ✓ ${cmd} — ${why}`;
100
+ const head = required ? ` ✗ ${cmd} — not found, needed for ${why}` : ` ○ ${cmd} — optional (${why})`;
101
+ return `${head}\n → ${hint}`;
102
+ }
103
+
79
104
  function promptYesNo(question) {
80
105
  return new Promise((res) => {
81
106
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -103,13 +128,7 @@ async function runInit(initArgs) {
103
128
  console.log(" → npm install -g @anthropic-ai/claude-code (then run `claude` and log in)");
104
129
  }
105
130
 
106
- for (const [cmd, versionArg, why, hint] of [
107
- ["tmux", "-V", "sessions survive a restart", "brew install tmux · apt install tmux"],
108
- ["gh", "--version", "PRs & Issues view + one-click PRs", "https://cli.github.com (then: gh auth login)"],
109
- ["codex", "--version", "run OpenAI Codex as an agent", "npm install -g @openai/codex"],
110
- ]) {
111
- console.log(hasCommand(cmd, versionArg) ? ` ✓ ${cmd} — ${why}` : ` ○ ${cmd} — optional (${why})\n → ${hint}`);
112
- }
131
+ PATH_TOOLS.forEach((tool) => console.log(toolCheckLine(tool)));
113
132
 
114
133
  // Config half: derive working-dir presets from Claude history + write config.json.
115
134
  console.log("");
@@ -0,0 +1,46 @@
1
+ // How many lines each row of the cockpit roster (the list beside a zoomed terminal) shows before
2
+ // it clamps. The roster trades two things against each other: how many sessions fit on screen,
3
+ // and how much of each one you can read without opening it. 2/2/3 is the tight end — it fits a
4
+ // long roster, but a summary written as a full sentence gets cut mid-thought, which is exactly
5
+ // when you wanted to read it. That is a trade the user should get to make, not a bug to fix, so
6
+ // the defaults stay put and the knob is opt-in.
7
+ //
8
+ // One knob per field: the three are worth different amounts. A summary says what a session is
9
+ // doing NOW, while a prompt is usually done in two lines.
10
+ //
11
+ // Shared because both sides decide from it: the server sanitizes and serves it, the grid renders
12
+ // from it. The CSS that applies the clamp is NOT here — the server has no use for it (#877).
13
+
14
+ import { isRecord } from "./isRecord.js";
15
+
16
+ export interface CockpitLines {
17
+ summary: number;
18
+ prompt: number;
19
+ response: number;
20
+ }
21
+
22
+ export const DEFAULT_COCKPIT_LINES: CockpitLines = { summary: 2, prompt: 2, response: 3 };
23
+
24
+ // 1 is "one line, still clamped". Past ~20 a single row fills the column and the roster stops
25
+ // being a roster.
26
+ export const COCKPIT_LINES_MIN = 1;
27
+ export const COCKPIT_LINES_MAX = 20;
28
+
29
+ // Clamp an out-of-range number and round a fractional one — the contract normalizeFontSize
30
+ // states for the font size, for the same reason: on a bounded number the user asked for a
31
+ // DIRECTION, so honouring it reads as working where falling back to the default reads as
32
+ // ignored. Non-numeric is a different thing — "nothing configured here" — and falls back.
33
+ const oneField = (input: unknown, fallback: number): number => {
34
+ if (typeof input !== "number" || !Number.isFinite(input)) return fallback;
35
+ return Math.min(COCKPIT_LINES_MAX, Math.max(COCKPIT_LINES_MIN, Math.round(input)));
36
+ };
37
+
38
+ /** Per field, so one typo can't discard the two the user set correctly. */
39
+ export function sanitizeCockpitLines(input: unknown): CockpitLines {
40
+ if (!isRecord(input)) return { ...DEFAULT_COCKPIT_LINES };
41
+ return {
42
+ summary: oneField(input.summary, DEFAULT_COCKPIT_LINES.summary),
43
+ prompt: oneField(input.prompt, DEFAULT_COCKPIT_LINES.prompt),
44
+ response: oneField(input.response, DEFAULT_COCKPIT_LINES.response),
45
+ };
46
+ }
@@ -1,4 +1,4 @@
1
- // The name + fixed-color fields a `.mulmoterminal.json` may set. Shared across the
1
+ // The `.mulmoterminal.json` fields whose type is the SAME on both sides. Shared across the
2
2
  // build boundary: the server's DirConfig/PublicDirConfig and the client's DirConfig
3
3
  // all extend this, so a field added on one side can't go missing on the other.
4
4
  // `theme` and `colors` stay out of it — each side declares them with its own type
@@ -17,6 +17,10 @@ export interface DirChrome {
17
17
  cellBorderColor: string | null;
18
18
  dotColor: string | null;
19
19
  buttonColor: string | null;
20
+ // xterm font size in px for terminals opened here, or null to use the global setting.
21
+ // Unlike the colors above, this changes the cell metrics — every path that applies it has
22
+ // to re-fit and push the new cols/rows to the PTY, or the grid drifts from the canvas.
23
+ fontSize: number | null;
20
24
  }
21
25
 
22
26
  // "Nothing configured" — the base every DirConfig/PublicDirConfig empty spreads, so adding
@@ -32,4 +36,5 @@ export const EMPTY_DIR_CHROME: Readonly<DirChrome> = {
32
36
  cellBorderColor: null,
33
37
  dotColor: null,
34
38
  buttonColor: null,
39
+ fontSize: null,
35
40
  };
@@ -0,0 +1,20 @@
1
+ // The xterm font size, in px. Shared because all three sides decide from the same numbers:
2
+ // the server validates `.mulmoterminal.json`, the Settings stepper bounds its own buttons, and
3
+ // the client validates what it reads back out of localStorage.
4
+ export const TERMINAL_FONT_SIZE_DEFAULT = 14;
5
+ // Below ~8px the canvas renderer's glyphs stop being legible; above ~32px a grid cell holds so
6
+ // few columns that Claude's output wraps into noise. Neither is a hard xterm limit — they bound
7
+ // the stepper and reject a value that would leave the terminal unusable.
8
+ export const TERMINAL_FONT_SIZE_MIN = 8;
9
+ export const TERMINAL_FONT_SIZE_MAX = 32;
10
+ export const TERMINAL_FONT_SIZE_STEP = 1;
11
+
12
+ // Clamp rather than reject an out-of-range number: for a size, honouring the direction the user
13
+ // asked for reads as working, where falling back to the default reads as ignored. Non-numeric
14
+ // input is a different thing — "nothing configured here" — so it stays null and lets the caller
15
+ // fall back. Fractional sizes round: xterm accepts them, but they make cell metrics drift from
16
+ // the integer the UI shows.
17
+ export function normalizeFontSize(input: unknown): number | null {
18
+ if (typeof input !== "number" || !Number.isFinite(input)) return null;
19
+ return Math.min(TERMINAL_FONT_SIZE_MAX, Math.max(TERMINAL_FONT_SIZE_MIN, Math.round(input)));
20
+ }
@@ -0,0 +1,21 @@
1
+ // The `GET /api/transcribe/model` response. The server builds it; the UI decides the mic
2
+ // button's visibility and the settings section's from it — both sides read it, so it lives
3
+ // here rather than as one interface per side.
4
+ //
5
+ // Deliberately independent of whisper's own types: this is the API contract, and which
6
+ // package implements transcription behind it is not the UI's business. The server's
7
+ // `getVoiceInputStatus(): VoiceInputStatus` annotation is what keeps the two in step.
8
+
9
+ export type VoiceModelState = "idle" | "downloading" | "ready" | "error";
10
+
11
+ export interface VoiceInputStatus {
12
+ /** The platform and binaries are present — the mic can appear. */
13
+ capable: boolean;
14
+ model: {
15
+ name: string;
16
+ state: VoiceModelState;
17
+ /** 0..1, only while downloading and only when the size is known. */
18
+ progress?: number;
19
+ error?: string;
20
+ };
21
+ }
@@ -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-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};
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-BAh0ySn4.js";import"./chunk-WYO6CB5R-BlzOfotS-BRwSb4rl.js";import"./chunk-VAUOI2AC-CS9QJ4yz-CfgcnwuB.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-CKo27qok.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-BAh0ySn4.js";export{e as createArchitectureServices};