mulmoterminal 2.2.0 → 2.4.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 (124) hide show
  1. package/README.md +52 -1
  2. package/bin/cli-args.d.ts +1 -0
  3. package/bin/cli-args.js +16 -0
  4. package/bin/mulmoterminal.js +2 -1
  5. package/common/dirConfigSource.ts +101 -0
  6. package/common/githubRepo.ts +22 -0
  7. package/common/terminalClipboard.ts +19 -0
  8. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-6R9yRbUC.js → abnfDiagram-VRR7QNED-RjiYivmv-CqId6L6N.js} +1 -1
  9. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-CfmmIOkB.js +1 -0
  10. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-DzS5ABJE.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-CWhhLcfM.js} +1 -1
  11. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-Cw9RkJxY.js → blockDiagram-677ZJIJ3-BPuAJQRW-89yfYblY.js} +1 -1
  12. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-CWltXMHP.js → c4Diagram-LMCZKHZV-C0LAqQso-Cq70UauP.js} +1 -1
  13. package/dist/assets/channel-7wUqSdoX-Bc9EmVWS.js +1 -0
  14. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-DrrsZ2u5.js → chunk-32BRIVSS-BRrYpgtb-Bj5FdJ3A.js} +1 -1
  15. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-WJMq_L8L.js → chunk-52WLFC77-BJ-ss3Xr-Cf6wft_5.js} +1 -1
  16. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-BafXJkh7.js → chunk-C7G6YPKG-BZEucKEL-CLA5COUu.js} +1 -1
  17. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-Q8lsrJXs.js → chunk-EX3LRPZG-DLS6FBN1-ChsLH6_y.js} +1 -1
  18. package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-Cgm5-zaI.js → chunk-FWX5IMBZ-DHLSFw1H-BIvnIbPU.js} +2 -2
  19. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-Cp5J5cOD.js → chunk-HOUHSVGY-Bhlt8hXJ-Ck-Dxtw-.js} +1 -1
  20. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-CLdnp-d2.js → chunk-ICXQ74PX-DwgHBX_g-CSlWVcWs.js} +1 -1
  21. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-BsIB0ZXR.js → chunk-MOJQB5TN-CMZRaeqt-CYfpp2V7.js} +1 -1
  22. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-DlTM-RTP.js → chunk-OGEWGWER-8Qy4a8b5-DPIlRee4.js} +1 -1
  23. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-XujP_H1v.js → chunk-PUDLZKDR-DcrWQRYh-J86vFVAN.js} +1 -1
  24. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-DhxnzVDy.js → chunk-Q4XR5HBZ-ZXVGkG8Z-D-40tbjk.js} +1 -1
  25. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-C-xPmCQA.js → chunk-V7JOEXUC-DmGdheTX-7gJmS-F9.js} +1 -1
  26. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-Cng5RPUP.js → chunk-VAUOI2AC-CS9QJ4yz-DFjCsQc6.js} +1 -1
  27. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-Bmb0moA3.js → chunk-VR4S4FIN-C6a91eNY-Ba059rhB.js} +1 -1
  28. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-DmR4SyZL.js → chunk-WYO6CB5R-BlzOfotS-CTDodsnw.js} +1 -1
  29. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-CD8nNM8_.js → chunk-ZGVPDNZ5-BYwxNFTK-CMF9FJ7K.js} +1 -1
  30. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-Dk_90vZD.js +1 -0
  31. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-Dk_90vZD.js +1 -0
  32. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-B1MlaJhj.js +1 -0
  33. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--pv1DCSfy.js → cynefinDiagram-TSTJHNR4-DHA9iPo--BUwfErNr.js} +1 -1
  34. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-BPcGEiSl.js → dagre-VKFMJZFB--oJKqXBZ-FEoLipSZ.js} +1 -1
  35. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-CCOrHV0U.js → diagram-FQU43EPY-CcJCB9bG-DwH2nER4.js} +1 -1
  36. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-Crm_IB8Y.js → diagram-G47NLZAW-C_o-WGG1-BuSqgofP.js} +1 -1
  37. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--UcH8gkTE.js → diagram-NH7WQ7WH-CXJCYvY--CoMsubYQ.js} +1 -1
  38. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-7C9Me5sS.js → diagram-OA4YK3LP-BMzeJ87A-Ds_49Q4I.js} +1 -1
  39. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-BBztQ9hK.js → diagram-WEI45ONY-D_93NKqo-DeDTEgI-.js} +1 -1
  40. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-TkJNOqEw.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-B84fKP1h.js} +1 -1
  41. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-DS2blJuc.js → erDiagram-Q63AITRT-CiABnA0s-QGRolPYH.js} +1 -1
  42. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-C0eB3veQ.js +1 -0
  43. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BAfA7eXR.js +1 -0
  44. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-ZB_lhurA.js → ganttDiagram-NO4QXBWP-DQZvdFo1-BBXcle76.js} +1 -1
  45. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-CotjI6SA.js +1 -0
  46. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-Vm6I27hg.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-BxGBV6Hj.js} +1 -1
  47. package/dist/assets/index-B7LbAi10.css +1 -0
  48. package/dist/assets/{index-BP05wTLW.js → index-_XRnaxAj.js} +202 -202
  49. package/dist/assets/info-DKCQHKI2-Dplx5kMp-DKHWzOor.js +1 -0
  50. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-Cr34YkAb.js → infoDiagram-FWYZ7A6U-7UnoB5AP-DCqG4iB9.js} +1 -1
  51. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-at4P1zMV.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-Dkzr63Uo.js} +1 -1
  52. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-D4ZV57k7.js → journeyDiagram-5HDEW3XC-c5xIah9o-3K64pBTH.js} +1 -1
  53. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-BiJ3JUjX.js → kanban-definition-HUTT4EX6-Cnt6loYD-B0U7Mkg9.js} +1 -1
  54. package/dist/assets/{lib-Du_IVDkN.js → lib-rFkGOb1O.js} +1 -1
  55. package/dist/assets/{line-D7ziSjKi-CPM0FMWw.js → line-D7ziSjKi-D25izB30.js} +1 -1
  56. package/dist/assets/{marp-BBre1w8X.js → marp-79PhoGbq.js} +1 -1
  57. package/dist/assets/{mermaid-parser.core-DxEa8E3F-CO4zEYzL.js → mermaid-parser.core-DxEa8E3F-BzSqp82H.js} +2 -2
  58. package/dist/assets/{mermaid.core-V0OYwIz3-CzPQxhwu.js → mermaid.core-V0OYwIz3-0p62b-zk.js} +3 -3
  59. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-Nez_xicT.js → mindmap-definition-LN4V7U3C-CexN3O6L-BzFX06oy.js} +1 -1
  60. package/dist/assets/packet-7NZHBO7P-CQI3flND-DyAt_QAV.js +1 -0
  61. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-0AEyhHwQ.js → pegDiagram-2B236MQR-BiM4G0cW-BMRJsJZN.js} +1 -1
  62. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-nlXXCSHg.js +1 -0
  63. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-DXBEfgPl.js → pieDiagram-ENE6RG2P-B_fBBS-2-Bl2zNUvh.js} +1 -1
  64. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-dZ16XJ8-.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-BzQ392_S.js} +1 -1
  65. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-Dnn5SKpT.js +1 -0
  66. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-DjF0R5Zr.js +1 -0
  67. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-GH7jGfhq.js +1 -0
  68. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-CH8TSRFp.js +1 -0
  69. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-QfsXYBPl.js +1 -0
  70. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-DHNapLB9.js → railroadDiagram-RFXS5EU6--lNIlG64-D-Yn0Dpg.js} +1 -1
  71. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-C-9SVxpD.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-DIYnBCWZ.js} +1 -1
  72. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-Cv-mFz76.js → sankeyDiagram-HTMAVEWB-DlutaXDv-Dsr5VJzO.js} +1 -1
  73. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-BR0sUcBe.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-mR1rIlJQ.js} +1 -1
  74. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-DjqY9a1u.js → stateDiagram-2N3HPSRC-jf_dXUEU-Cq2A7Udg.js} +1 -1
  75. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-D7ipapeO.js +1 -0
  76. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-DV8Ha8XJ.js → swimlanes-5IMT3BWC-CYjtALQH-CtgZ4USf.js} +1 -1
  77. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-D1yGYNNh.js +8 -0
  78. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-D2oq5bFn.js → timeline-definition-FHXFAJF6-DYJ4oUm8-CpuMCUFK.js} +1 -1
  79. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-nxCnsDvO.js +1 -0
  80. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-7UfnOcZW.js +1 -0
  81. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-I-wP-h9k.js → vennDiagram-L72KCM5P-CdKAHoek-B-FSFzf3.js} +1 -1
  82. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-DHZIQf1x.js +1 -0
  83. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-BkrH7vLX.js → wardleyDiagram-EHGQE667-BF6c4_CW-CB01oT0C.js} +1 -1
  84. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-DMYCP46V.js → xychartDiagram-FW5EYKEG-CGiKngj7-D1vr6bQ-.js} +1 -1
  85. package/dist/index.html +2 -2
  86. package/package.json +1 -1
  87. package/server/agents/claude-args.ts +5 -0
  88. package/server/agents/session-summary-prompt.ts +49 -0
  89. package/server/config/app-config.ts +13 -0
  90. package/server/config/config-routes.ts +6 -1
  91. package/server/config/dir-config.ts +83 -0
  92. package/server/config/env.ts +12 -0
  93. package/server/config/workspace.ts +17 -7
  94. package/server/git/github-star.ts +63 -0
  95. package/server/index.ts +10 -5
  96. package/server/infra/allowed-origin.ts +106 -25
  97. package/server/infra/hide-error-stacks.ts +15 -0
  98. package/server/infra/spa-fallback.ts +18 -0
  99. package/server/routes/app-routes.ts +2 -2
  100. package/server/routes/dir-routes.ts +14 -2
  101. package/server/routes/repo-routes.ts +25 -0
  102. package/server/skills/mulmoterminal-config/SKILL.md +28 -1
  103. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-C95zkDEU.js +0 -1
  104. package/dist/assets/channel-7wUqSdoX-BYaGw2yS.js +0 -1
  105. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-EDJcEZLp.js +0 -1
  106. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-EDJcEZLp.js +0 -1
  107. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-Co-phwWx.js +0 -1
  108. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-D-dhMOz8.js +0 -1
  109. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-KiTeh-IZ.js +0 -1
  110. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-Cd1rBWSG.js +0 -1
  111. package/dist/assets/index-CGwcisib.css +0 -1
  112. package/dist/assets/info-DKCQHKI2-Dplx5kMp-Dic4-0_Y.js +0 -1
  113. package/dist/assets/packet-7NZHBO7P-CQI3flND-BBywyu3N.js +0 -1
  114. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-CoicOL1H.js +0 -1
  115. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-BeFXvO0F.js +0 -1
  116. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-BZY5Z4oN.js +0 -1
  117. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-Dubi7PGc.js +0 -1
  118. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-wnb7FBOh.js +0 -1
  119. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-P7vJjL8M.js +0 -1
  120. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-DjKerI1O.js +0 -1
  121. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-Dm5TS_6E.js +0 -8
  122. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-H3pflPYY.js +0 -1
  123. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-CIoNkikG.js +0 -1
  124. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-JQdCXyum.js +0 -1
package/README.md CHANGED
@@ -93,6 +93,13 @@ than as bytes (files within the session's working directory only):
93
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
94
  | everything else — images, PDF, SVG, HTML, video | raw bytes in a new tab, which the browser renders better than an editor would |
95
95
 
96
+ **While a grid cell is enlarged, the [Files pane](#files-view-browse--edit) takes the click first** — every
97
+ row above except the last one, since the pane is the same editor plus a Markdown preview. The
98
+ file opens *beside* the terminal that printed it, and the pane opens itself if it was closed.
99
+ It declines, leaving the routing above untouched, when nothing is enlarged, when the path is
100
+ not under that cell's own directory (the pane cannot walk above its root), or for the raw-bytes
101
+ row, where it would only show an empty editor.
102
+
96
103
  Highlighting in the Files view covers the JS/TS family, JSON and Markdown (the modes
97
104
  `cmEditor.ts` bundles); other languages open as plain text.
98
105
 
@@ -223,6 +230,7 @@ The launcher detects it and prints the exact, OS-appropriate removal command; ru
223
230
  - [Session model](#session-model)
224
231
  - [Session lifecycle](#session-lifecycle)
225
232
  - [Claude hook injection](#claude-hook-injection)
233
+ - [Closing summary](#closing-summary)
226
234
  - [Session discovery & titles](#session-discovery--titles)
227
235
  - [Project structure](#project-structure)
228
236
  - [Testing](#testing)
@@ -287,7 +295,8 @@ today — **Claude Code** (the default) and **Codex**.
287
295
  - **Claude** — spawned as `claude` (override with `CLAUDE_BIN`). The server passes
288
296
  `--session-id <uuid>`, so it knows the live session's id even before its transcript
289
297
  file exists, and injects activity hooks + the GUI MCP per spawn (see
290
- [Claude hook injection](#claude-hook-injection)).
298
+ [Claude hook injection](#claude-hook-injection)) plus the
299
+ [closing summary](#closing-summary) instruction.
291
300
  - **Codex** — spawned as `codex` (override with `CODEX_BIN`; `CODEX_MODEL` sets
292
301
  `--model`). Codex runs on its own WebSocket (`/ws/codex`) and its sessions appear in the
293
302
  sidebar next to Claude's. Because Codex only mints its rollout id **after** the first
@@ -465,6 +474,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
465
474
  | `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). |
466
475
  | `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). |
467
476
  | `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). |
477
+ | `copyOnSelect` | `true` puts a **mouse selection on the clipboard the moment it settles**, with no key pressed (the PuTTY / iTerm2 behaviour). **Off by default** — it changes the clipboard when you may only have meant to highlight something. No Settings UI: edit the file and reload the tab. Composes with the `copy` keymap action rather than replacing it. Over plain `http://` the browser gives a page no clipboard access, so a fallback asks xterm to copy instead; see the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#copy-on-select). |
468
478
  | `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). |
469
479
  | `fontFamily` | The **terminal font** every session renders in — a CSS font-family stack, e.g. `"'Cica', 'MS Gothic', monospace"`. No Settings UI: edit the file, then **restart** (this config is read once at startup). Unset uses the built-in stack (JetBrains Mono / Fira Code / Menlo / Consolas, then CJK faces for Japanese, Korean and Chinese). Unlike the per-browser font **size**, this is one value for the whole host — it names fonts, and which fonts exist is a property of the machine. A directory can override it. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#font-family). |
470
480
 
@@ -617,6 +627,12 @@ as the reload signal, so colors, palette, font size and grid order update withou
617
627
  anything. There is no filesystem watcher, so an edit made **outside** a session (your own
618
628
  editor) is picked up when the terminal is next opened.
619
629
 
630
+ **Checking what took effect.** Settings → **Directory settings** lists your recent directories
631
+ and expands each one to the values in force, with a swatch per color and the path of the file
632
+ they came from. It also names the keys it **dropped** (a color that isn't `#rrggbb`, a size out
633
+ of range) and the keys it doesn't read at all (`badgeColour`, a global-only setting) — which is
634
+ what tells "I never set that" apart from "I set it and it didn't take".
635
+
620
636
  ---
621
637
 
622
638
  ## Running
@@ -768,6 +784,10 @@ shrinks the pane rather than reflowing xterm into garbage. It works in both zoom
768
784
  (cockpit roster and thumbnail filmstrip), the pane re-roots as you walk the zoom between
769
785
  terminals, and whether it's open plus how wide it is are remembered per browser.
770
786
 
787
+ The toggle is not the only way in: while a cell is enlarged, **clicking a file path the agent
788
+ printed** opens it here too, rather than in a new tab or full-screen — see
789
+ [Clicking a file path](#clicking-a-file-path).
790
+
771
791
  All reads and writes go through `GET/PUT /api/files/browse/*?cwd=&path=`, and every
772
792
  `path` is **contained within the project root** (server-side) — `..`/absolute escapes
773
793
  are rejected for reads and writes alike, so editing can't reach outside the directory
@@ -920,6 +940,11 @@ Favorited collections get their own toolbar buttons.
920
940
  session.
921
941
  - **Notifications** (🔔) — a toolbar bell with an unread badge and a dropdown of active
922
942
  notifications; click a row to jump to its session.
943
+ - **Star MulmoTerminal** — a star button in the grid toolbar that stars the project on GitHub
944
+ through your own `gh` login, in one click. It is a one-time ask: once the repo is starred the
945
+ button is gone for good and stops calling the server at all. It shows **only when `gh` can
946
+ answer** — with no `gh`, no login, or no network, one click couldn't star anything, so nothing
947
+ is shown and nothing is recorded. Set `gh` up later and the button appears by itself.
923
948
  - **Voice input** — dictate a prompt via on-device Whisper (`POST /api/transcribe`, macOS
924
949
  only; the model downloads on first use). Settings picks **the language you dictate in**
925
950
  (per browser): your browser's, whisper's own per-clip detection, or a fixed one. Worth
@@ -1102,6 +1127,7 @@ same-origin-guarded.
1102
1127
  | `GET /api/worktrees?cwd=` · `GET /api/worktrees/diff?cwd=` | List managed worktrees / diff one vs its base. |
1103
1128
  | `POST /api/worktrees/create` · `/remove` · `/push` · `/pr` | Create on `agent/<slug>`, remove (managed root only), push, open a PR (`gh`, else compare URL). |
1104
1129
  | `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` (via `gh`). |
1130
+ | `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. |
1105
1131
 
1106
1132
  **Workspace views**
1107
1133
 
@@ -1128,6 +1154,7 @@ same-origin-guarded.
1128
1154
  | -------- | ------- |
1129
1155
  | `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `soundKinds`, `sounds`, `prRepos`, `launchers`, `quickCommands`, `userMcpServers`, `providers`). |
1130
1156
  | `GET /api/sound?kind=` · `/api/dir-sound?cwd=&kind=` · `/api/sound-preset/:id` · `/api/dir-config?cwd=` | Custom / per-directory / preset attention sound + per-dir config. `kind` selects a config entry, never a path. |
1157
+ | `GET /api/dir-config-detail?cwd=` | The same per-dir config, **plus** the settings a running terminal doesn't need (`provider`, `model`, `skills`, `addDirs`, header button/chip **labels**), **plus** which keys the file set and how each fared (applied / dropped in validation / not a setting at all). Read-only; backs the Settings modal's **Directory settings** preview. Unlike the other `?cwd=` routes this one does **not** fall back to the default workspace — it reports on the directory it was asked about, so a path that no longer exists comes back as `exists:false`. Sound paths and button commands stay server-side. |
1131
1158
  | `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. |
1132
1159
  | `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
1133
1160
  | `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
@@ -1328,6 +1355,30 @@ knows the live session's id — even before the session's `.jsonl` file exists.
1328
1355
 
1329
1356
  ---
1330
1357
 
1358
+ ## Closing summary
1359
+
1360
+ Every Claude session is spawned with `claude --append-system-prompt '<text>'`, asking the
1361
+ agent to end a reply with a short summary **when it hands control back** — the work is
1362
+ finished, or it is stopping to ask a question. Coming back to a grid cell after a while, the
1363
+ standing request and what came of it are otherwise only recoverable by scrolling the whole
1364
+ session.
1365
+
1366
+ The summary states three things: the request **for the conversation as a whole** (not the
1367
+ last message — several turns of refinement do not replace what was asked first), what was
1368
+ achieved, and what was not and why. It is written in the language of the conversation, and
1369
+ placed last with nothing after it.
1370
+
1371
+ It is deliberately **not** written on every turn: mid-work replies and short factual answers
1372
+ carry no standing request, and a summary that always appears stops being read. The wording
1373
+ lives in `server/agents/session-summary-prompt.ts`; there is no setting to turn it off.
1374
+
1375
+ Passed inline rather than as `--append-system-prompt-file` for the same reason `--settings`
1376
+ is: the sandbox spawn runs in a container that cannot read a host path.
1377
+
1378
+ Codex sessions are unaffected — the CLI has no equivalent flag.
1379
+
1380
+ ---
1381
+
1331
1382
  ## Session discovery & titles
1332
1383
 
1333
1384
  Claude stores each project's sessions as JSONL files under
package/bin/cli-args.d.ts CHANGED
@@ -10,3 +10,4 @@ export declare const SECOND_INSTANCE_NOTE: string;
10
10
  export declare const MIN_NODE_LABEL: string;
11
11
  export declare function nodeMeetsMinimum(version: string): boolean;
12
12
  export declare function serverNodeArgs(serverEntry: string, launchDir: string): string[];
13
+ export declare function serverSpawnEnv(env: Record<string, string | undefined>, port: number, cwd: string): Record<string, string | undefined>;
package/bin/cli-args.js CHANGED
@@ -150,3 +150,19 @@ export function nodeMeetsMinimum(version) {
150
150
  export function serverNodeArgs(serverEntry, launchDir) {
151
151
  return ["--import", "tsx", `--env-file-if-exists=${join(launchDir, ".env")}`, serverEntry];
152
152
  }
153
+
154
+ /**
155
+ * The environment the launcher spawns the server with.
156
+ *
157
+ * Only the two values the server cannot work out for itself. In particular NOT `NODE_ENV`:
158
+ * the server hands its own environment to every PTY it spawns, so a `NODE_ENV=production`
159
+ * set here reaches every terminal in every cell — where yarn v1 reads it and installs
160
+ * WITHOUT devDependencies while still reporting success (#955). Express is pinned to
161
+ * production separately, in the server, so nothing here needs to say it.
162
+ *
163
+ * A `NODE_ENV` the user exported themselves passes through untouched: this decides what the
164
+ * launcher adds, not what it removes.
165
+ */
166
+ export function serverSpawnEnv(env, port, cwd) {
167
+ return { ...env, PORT: String(port), CLAUDE_CWD: cwd };
168
+ }
@@ -26,6 +26,7 @@ import {
26
26
  nodeMeetsMinimum,
27
27
  MIN_NODE_LABEL,
28
28
  serverNodeArgs,
29
+ serverSpawnEnv,
29
30
  } from "./cli-args.js";
30
31
 
31
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -272,7 +273,7 @@ function runServer(port, noOpen, cwd, onChild) {
272
273
  // package directory, so serverNodeArgs makes that path absolute (#795).
273
274
  const server = spawn(process.execPath, serverNodeArgs(SERVER_ENTRY, process.cwd()), {
274
275
  cwd: PKG_DIR,
275
- env: { ...process.env, NODE_ENV: "production", PORT: String(port), CLAUDE_CWD: cwd },
276
+ env: serverSpawnEnv(process.env, port, cwd),
276
277
  stdio: ["inherit", "inherit", "pipe"],
277
278
  });
278
279
  let stderrTail = "";
@@ -0,0 +1,101 @@
1
+ // Where a directory's settings actually came from, for the settings modal's preview.
2
+ //
3
+ // The rest of the app only ever needs the RESOLVED config — the values a terminal adopts.
4
+ // That is exactly what makes a mistyped or rejected setting invisible: the file is on disk,
5
+ // the app reads it, and the value silently isn't there. This says which keys the file set,
6
+ // which of them survived validation, and which the app doesn't read at all.
7
+
8
+ // Every key `loadDirConfig` reads. A key outside this list is a typo as far as the app is
9
+ // concerned, however sensible it looks — `server/config/dir-config.spec.ts` pins the two
10
+ // together so a field added to the loader can't quietly go missing here.
11
+ export const DIR_CONFIG_KEYS = [
12
+ "name",
13
+ "badgeColor",
14
+ "headerColor",
15
+ "headerTextColor",
16
+ "cellColor",
17
+ "cellBorderColor",
18
+ "dotColor",
19
+ "buttonColor",
20
+ "fontSize",
21
+ "fontFamily",
22
+ "orderPriority",
23
+ "theme",
24
+ "colors",
25
+ "sound",
26
+ "sounds",
27
+ "buttons",
28
+ "chips",
29
+ "skills",
30
+ "provider",
31
+ "model",
32
+ "addDirs",
33
+ ] as const;
34
+
35
+ export interface DirConfigSource {
36
+ // Keys the file set that the app is using.
37
+ applied: string[];
38
+ // Keys the file set that the app read and then dropped — an out-of-range number, a colour
39
+ // that isn't #rrggbb, a sound outside the directory. The value is gone; the key is the clue.
40
+ ignored: string[];
41
+ // Keys the app doesn't read at all: a misspelling (`badgeColour`), or a setting that belongs
42
+ // in the global config instead of a directory's file.
43
+ unknown: string[];
44
+ }
45
+
46
+ export const EMPTY_DIR_CONFIG_SOURCE: DirConfigSource = { applied: [], ignored: [], unknown: [] };
47
+
48
+ // The settings a directory can make that the per-cell config deliberately doesn't carry — the
49
+ // browser has no use for them while running a terminal, but the preview has to SHOW them or a
50
+ // key like `provider` can only be named as applied, never read back.
51
+ //
52
+ // Labels only for buttons and chips: their `cmd` / `text` is what the button would type into
53
+ // the session, which is not what "did my config take effect" needs, and is the sort of thing a
54
+ // screenshot of the settings screen shouldn't carry. Sound paths stay out for the same reason
55
+ // they stay out of the per-cell config — that path is the confinement boundary.
56
+ export interface DirConfigExtras {
57
+ provider: string | null;
58
+ model: string | null;
59
+ skills: string[] | null;
60
+ addDirs: string[] | null;
61
+ buttonLabels: string[];
62
+ chipLabels: string[];
63
+ }
64
+
65
+ export const EMPTY_DIR_CONFIG_EXTRAS: DirConfigExtras = {
66
+ provider: null,
67
+ model: null,
68
+ skills: null,
69
+ addDirs: null,
70
+ buttonLabels: [],
71
+ chipLabels: [],
72
+ };
73
+
74
+ // "The loader kept nothing for this key" — null/undefined, but also the empty collections the
75
+ // loader returns when every entry in a map or list was rejected.
76
+ function isEmptyValue(value: unknown): boolean {
77
+ if (value === null || value === undefined) return true;
78
+ if (Array.isArray(value)) return value.length === 0;
79
+ if (typeof value === "object") return Object.keys(value as Record<string, unknown>).length === 0;
80
+ return false;
81
+ }
82
+
83
+ // The keys a resolved config actually holds a value for. Taken as a SET by describeDirConfig
84
+ // rather than the config object itself: the loader's type is an interface with named fields,
85
+ // which no amount of `Record<string, unknown>` will accept without a cast.
86
+ export function keysWithValue(resolved: object): Set<string> {
87
+ const entries: [string, unknown][] = Object.entries(resolved);
88
+ return new Set(entries.filter(([, value]) => !isEmptyValue(value)).map(([key]) => key));
89
+ }
90
+
91
+ // `raw` is the file as parsed; `kept` is which keys survived the loader (see keysWithValue).
92
+ export function describeDirConfig(raw: Record<string, unknown>, kept: ReadonlySet<string>): DirConfigSource {
93
+ const known = new Set<string>(DIR_CONFIG_KEYS);
94
+ const source: DirConfigSource = { applied: [], ignored: [], unknown: [] };
95
+ for (const key of Object.keys(raw)) {
96
+ if (!known.has(key)) source.unknown.push(key);
97
+ else if (kept.has(key)) source.applied.push(key);
98
+ else source.ignored.push(key);
99
+ }
100
+ return source;
101
+ }
@@ -0,0 +1,22 @@
1
+ // This project's own GitHub repository, and the wire shape of the star endpoints that act on it.
2
+ // Both sides decide from these — the server builds the `gh api` path and types its response,
3
+ // the client parses that response — so they live here rather than as two copies.
4
+ import { isRecord } from "./isRecord.js";
5
+
6
+ export const GITHUB_REPO = "receptron/mulmoterminal";
7
+
8
+ // GitHub's "star a repository" endpoint for this repo: GET reports the current state, PUT stars it.
9
+ export const STAR_API_PATH = `/user/starred/${GITHUB_REPO}`;
10
+
11
+ // GET/POST /api/github/star. `null` means the server could not tell — no `gh`, not logged in,
12
+ // offline. That is a DIFFERENT answer from "not starred": one click cannot star anything in that
13
+ // state, so the client hides the button rather than offering one that would do nothing.
14
+ export interface GithubStarState {
15
+ starred: boolean | null;
16
+ }
17
+
18
+ // Read a star state off an untrusted response body. Anything unparseable reads as "cannot tell",
19
+ // which is the same degradation as a failed request, so the caller needs no separate error path.
20
+ export function parseStarState(value: unknown): boolean | null {
21
+ return isRecord(value) && typeof value.starred === "boolean" ? value.starred : null;
22
+ }
@@ -50,3 +50,22 @@ export function clipboardActionFor(keymap: Keymap, e: ClipboardKeyEvent, hasSele
50
50
  // to be reversed.
51
51
  return action === "copy" && !hasSelection ? null : action;
52
52
  }
53
+
54
+ // Copy-on-select: the text a settled selection should put on the clipboard, or null to leave the
55
+ // clipboard alone. `lastCopied` is what this terminal wrote last, so an unchanged selection is not
56
+ // written twice.
57
+ //
58
+ // Unlike the rules above, this one ends in a clipboard WRITE — no keystroke happened, so nothing in
59
+ // the browser was ever going to copy anything by itself. That is what makes the two skips here
60
+ // matter more than they look:
61
+ //
62
+ // - Whitespace only. Dragging across empty terminal space selects spaces, and silently replacing
63
+ // the user's clipboard with a run of them is this feature's worst failure. Anyone who really
64
+ // wants indentation still has the `copy` keymap action.
65
+ // - Unchanged text. A second identical write buys nothing and costs a duplicate entry in the OS
66
+ // clipboard history (Win+V), which is the same reason the caller waits for the selection to
67
+ // settle instead of writing on every onSelectionChange.
68
+ export function selectionToCopy(enabled: boolean, selection: string, lastCopied: string | null): string | null {
69
+ if (!enabled || selection.trim() === "" || selection === lastCopied) return null;
70
+ return selection;
71
+ }
@@ -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-CO4zEYzL.js";import"./chunk-WYO6CB5R-BlzOfotS-DmR4SyZL.js";import"./chunk-VAUOI2AC-CS9QJ4yz-Cng5RPUP.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-BsIB0ZXR.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-BzSqp82H.js";import"./chunk-WYO6CB5R-BlzOfotS-CTDodsnw.js";import"./chunk-VAUOI2AC-CS9QJ4yz-DFjCsQc6.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-CYfpp2V7.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-BzSqp82H.js";export{e as createArchitectureServices};