mulmoterminal 2.5.0 → 2.5.2

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 (120) hide show
  1. package/README.md +11 -1
  2. package/common/codeBlocks.ts +88 -0
  3. package/common/decisionLog.ts +49 -0
  4. package/common/themeVars.ts +134 -0
  5. package/common/workComment.ts +44 -0
  6. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-ZKZeabOo.js → abnfDiagram-VRR7QNED-RjiYivmv-CFwKVB03.js} +1 -1
  7. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-BEV01Wk-.js +1 -0
  8. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-CzywLoPM.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-mE1s94XZ.js} +1 -1
  9. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-aOYmvIDN.js → blockDiagram-677ZJIJ3-BPuAJQRW-DBDXdKIt.js} +1 -1
  10. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-DrRq0Tz1.js → c4Diagram-LMCZKHZV-C0LAqQso-RJl7QI1d.js} +1 -1
  11. package/dist/assets/channel-7wUqSdoX-Czd43Gvk.js +1 -0
  12. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-D-VyP-2M.js → chunk-32BRIVSS-BRrYpgtb-C3OsDfXW.js} +1 -1
  13. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-xsQKbX6Y.js → chunk-52WLFC77-BJ-ss3Xr-tZqd3bLR.js} +1 -1
  14. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-LmuOyVM4.js → chunk-C7G6YPKG-BZEucKEL-B8NVLAkB.js} +1 -1
  15. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-D_ZX8lXD.js → chunk-EX3LRPZG-DLS6FBN1-BdMYjq_3.js} +1 -1
  16. package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-DABXVNFD.js → chunk-FWX5IMBZ-DHLSFw1H-DbaszCXG.js} +2 -2
  17. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-Clcclm_e.js → chunk-HOUHSVGY-Bhlt8hXJ-DZ4w7WP6.js} +1 -1
  18. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-DVrDEih_.js → chunk-ICXQ74PX-DwgHBX_g-DwfVM3-4.js} +1 -1
  19. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-B6_lajzj.js → chunk-MOJQB5TN-CMZRaeqt-DDn-QIzL.js} +1 -1
  20. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-BK8v7oHr.js → chunk-OGEWGWER-8Qy4a8b5-DsrblYpm.js} +1 -1
  21. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-BpZVxE3I.js → chunk-PUDLZKDR-DcrWQRYh-Dz5x4gGU.js} +1 -1
  22. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-BJub83F1.js → chunk-Q4XR5HBZ-ZXVGkG8Z-Dg8P4fgN.js} +1 -1
  23. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-BwD2Cu1l.js → chunk-V7JOEXUC-DmGdheTX-CSESRbzZ.js} +1 -1
  24. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-DYmUgyge.js → chunk-VAUOI2AC-CS9QJ4yz-r_1UiH65.js} +1 -1
  25. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-CZWE7I5r.js → chunk-VR4S4FIN-C6a91eNY-BiFSHZig.js} +1 -1
  26. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-CoeWhGPT.js → chunk-WYO6CB5R-BlzOfotS-DenoOpP3.js} +1 -1
  27. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-DHPaQ6Oa.js → chunk-ZGVPDNZ5-BYwxNFTK-DIJf6S9Q.js} +1 -1
  28. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-CNqTeJKX.js +1 -0
  29. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-CNqTeJKX.js +1 -0
  30. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-HnBHTRUw.js +1 -0
  31. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--BVz-2Ntu.js → cynefinDiagram-TSTJHNR4-DHA9iPo--CMG2swe1.js} +1 -1
  32. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-CgKI-Did.js → dagre-VKFMJZFB--oJKqXBZ-DptBydHT.js} +1 -1
  33. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-DIRlUnKd.js → diagram-FQU43EPY-CcJCB9bG-Dzr4eBwY.js} +1 -1
  34. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-DNgB0vZj.js → diagram-G47NLZAW-C_o-WGG1-DyxwgGcN.js} +1 -1
  35. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--DtULNtOi.js → diagram-NH7WQ7WH-CXJCYvY--C7_0SJUX.js} +1 -1
  36. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-Betvk9-o.js → diagram-OA4YK3LP-BMzeJ87A-CQoIcPi7.js} +1 -1
  37. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-DpjErInC.js → diagram-WEI45ONY-D_93NKqo-6c4HxiEH.js} +1 -1
  38. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-CkKEjG4D.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-DJcz6F1R.js} +1 -1
  39. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-BOytIrTd.js → erDiagram-Q63AITRT-CiABnA0s-D-sOjz5Q.js} +1 -1
  40. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-BLoYpG_j.js +1 -0
  41. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-7Y5BNJk8.js +1 -0
  42. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-Bv6Ol2Sx.js → ganttDiagram-NO4QXBWP-DQZvdFo1-B8qQdIm8.js} +1 -1
  43. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-BbSOpldz.js +1 -0
  44. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-aUQOq1B7.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-DshsJsEP.js} +1 -1
  45. package/dist/assets/{index-CbGZuCmY.js → index-BAKRQHkN.js} +202 -200
  46. package/dist/assets/index-BAZX7G64.css +1 -0
  47. package/dist/assets/info-DKCQHKI2-Dplx5kMp-BGK6F5n3.js +1 -0
  48. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-D5riRt52.js → infoDiagram-FWYZ7A6U-7UnoB5AP-8s4vJMqL.js} +1 -1
  49. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-DUHr3ImH.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-z-9aPM5O.js} +1 -1
  50. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-BmYfNX5M.js → journeyDiagram-5HDEW3XC-c5xIah9o-Cgg8FxSO.js} +1 -1
  51. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-CYSjS-Kr.js → kanban-definition-HUTT4EX6-Cnt6loYD-BNCxCgbs.js} +1 -1
  52. package/dist/assets/{lib-4D_VJiWI.js → lib-IyO-7kbx.js} +1 -1
  53. package/dist/assets/{line-D7ziSjKi-BSuJe7JA.js → line-D7ziSjKi-VRCegTMF.js} +1 -1
  54. package/dist/assets/{marp-aQPALUt4.js → marp-CMroV6s8.js} +1 -1
  55. package/dist/assets/{mermaid-parser.core-DxEa8E3F-BAMiAMZz.js → mermaid-parser.core-DxEa8E3F-DvKDRZGq.js} +2 -2
  56. package/dist/assets/{mermaid.core-V0OYwIz3-0t7TpKVw.js → mermaid.core-V0OYwIz3-DU0Mr4W1.js} +3 -3
  57. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-Y5ILic5l.js → mindmap-definition-LN4V7U3C-CexN3O6L-DMZlX-GZ.js} +1 -1
  58. package/dist/assets/packet-7NZHBO7P-CQI3flND-Pi7SfNjX.js +1 -0
  59. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-BcRtbtj2.js → pegDiagram-2B236MQR-BiM4G0cW-CsQ2THPf.js} +1 -1
  60. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-CCC7e9os.js +1 -0
  61. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-BoLxHrrD.js → pieDiagram-ENE6RG2P-B_fBBS-2-BQVfIkOE.js} +1 -1
  62. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-CaBUCRTr.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-DWtRt8Ki.js} +1 -1
  63. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CXlDDsf0.js +1 -0
  64. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-CtqN4OYA.js +1 -0
  65. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-Cx7DnzdQ.js +1 -0
  66. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-CZCUMFL6.js +1 -0
  67. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DEZTHe61.js +1 -0
  68. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-BZODWkTp.js → railroadDiagram-RFXS5EU6--lNIlG64-DhhBhlYi.js} +1 -1
  69. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-CTG1__VO.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-CX6hh2lX.js} +1 -1
  70. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-BHkDstqV.js → sankeyDiagram-HTMAVEWB-DlutaXDv-D4lvqkh6.js} +1 -1
  71. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-B47PY7W5.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-DBzQDGiD.js} +1 -1
  72. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-4bMMcfLt.js → stateDiagram-2N3HPSRC-jf_dXUEU-DAoAJ2BI.js} +1 -1
  73. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CZK_6dwu.js +1 -0
  74. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-Dkkp7Xaf.js → swimlanes-5IMT3BWC-CYjtALQH-DOQhVlBd.js} +1 -1
  75. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-MQB8RGJN.js +8 -0
  76. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-CKcCEyil.js → timeline-definition-FHXFAJF6-DYJ4oUm8-GXp4cKym.js} +1 -1
  77. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DhjAUXqS.js +1 -0
  78. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-C3IZTO8B.js +1 -0
  79. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-D4FLVcsc.js → vennDiagram-L72KCM5P-CdKAHoek-BozSpm60.js} +1 -1
  80. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-BrvClPld.js +1 -0
  81. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-CRhp69sj.js → wardleyDiagram-EHGQE667-BF6c4_CW-BhXZxWJk.js} +1 -1
  82. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-CqYw8fCB.js → xychartDiagram-FW5EYKEG-CGiKngj7-DG_02eZy.js} +1 -1
  83. package/dist/index.html +2 -2
  84. package/package.json +1 -1
  85. package/server/config/app-config.ts +43 -0
  86. package/server/config/config-body.ts +11 -1
  87. package/server/config/config-routes.ts +14 -0
  88. package/server/config/config-schema.ts +35 -1
  89. package/server/config/dir-config.ts +11 -1
  90. package/server/files/pick-file.ts +5 -3
  91. package/server/files/win-folder-dialog.ts +112 -0
  92. package/server/git/work-comment.ts +126 -0
  93. package/server/routes/app-routes.ts +5 -0
  94. package/server/routes/decision-routes.ts +126 -0
  95. package/server/routes/dir-routes.ts +49 -2
  96. package/server/session/decisions.ts +278 -0
  97. package/server/session/last-turn.ts +4 -0
  98. package/server/session/session-reads.ts +17 -1
  99. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-BlV5di_4.js +0 -1
  100. package/dist/assets/channel-7wUqSdoX-DBiTAZ-Z.js +0 -1
  101. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-DEqD9yXA.js +0 -1
  102. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-DEqD9yXA.js +0 -1
  103. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-rrcpRbiY.js +0 -1
  104. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-lhGPdKsg.js +0 -1
  105. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-Ce3_LeXQ.js +0 -1
  106. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DgqDgNXY.js +0 -1
  107. package/dist/assets/index-BwvrN1j0.css +0 -1
  108. package/dist/assets/info-DKCQHKI2-Dplx5kMp-CbcoQy2P.js +0 -1
  109. package/dist/assets/packet-7NZHBO7P-CQI3flND-Bzib0qjU.js +0 -1
  110. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-sgdov0Km.js +0 -1
  111. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-HeF-YUqy.js +0 -1
  112. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-CVEaRb6p.js +0 -1
  113. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-Da0o3UJC.js +0 -1
  114. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-d8_8RJsZ.js +0 -1
  115. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CJVm33Iy.js +0 -1
  116. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-BhlNWKJ_.js +0 -1
  117. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-BIUkJIiD.js +0 -8
  118. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-BTmguQlN.js +0 -1
  119. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-C69GNR2Y.js +0 -1
  120. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-4mdSyYTy.js +0 -1
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  **Run a whole team of coding agents from your browser — and actually keep up with them.**
4
4
 
5
+ A **browser terminal** for **parallel AI coding agents**: several **Claude Code** and **Codex**
6
+ sessions side by side, each in its own cell, with the one that needs you marked in colour. Vibe
7
+ coding with a single agent needs nothing but a shell — this is for when you run several and lose
8
+ track of which is waiting. Sessions survive a reload (tmux), work isolates in **git worktrees**,
9
+ and a **phone push** reaches you when a turn finishes.
10
+
5
11
  ### 📖 Documentation — **[receptron.github.io/mulmoterminal](https://receptron.github.io/mulmoterminal/)**
6
12
 
7
13
  - **User guide:** [English](https://receptron.github.io/mulmoterminal/guide/en/) — the grid
@@ -482,6 +488,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
482
488
  | `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). |
483
489
  | `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). |
484
490
  | `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). |
491
+ | `issueWorkComments` | Let a cell **comment on the issue it is working on**: once when it starts, and again when its PR merges (closing the issue if GitHub has not already). The comment names the working **directory** it happened in — the folder name only, never the path — so a reader can tell which clone. **Off by default**; it writes to GitHub, often on somebody else's issue. Needs `gh` logged in. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#issue-work-comments). |
485
492
  | `prWorkdirFooter` | Ends a PR body 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. Applies to **both** paths that open PRs here: **⧉ Open PR** appends it to the PR it creates, and every Claude session is told to end the bodies it writes with the same line (the name is resolved by the server, so a session inside a managed worktree still names the main checkout). **On by default**; set `false` to opt out — read per PR and per session spawn, so no restart is needed (there is no Settings control for it). Appending is idempotent: an existing PR never gets a second copy. |
486
493
  | `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). |
487
494
 
@@ -876,7 +883,9 @@ Each grid cell's header shows two badges for its session, refreshed when a turn
876
883
  current-gen Opus / Sonnet / Fable / Mythos, **200k** otherwise). A session running on a
877
884
  [provider model](#agents-claude--codex) shows that model's name and its published window
878
885
  (`Kimi K2.7 Code · ctx 12%`); a model in neither list keeps the label and hides the %,
879
- since the window is never guessed.
886
+ since the window is never guessed. A reading **past 100%** shows `ctx ?` instead of the
887
+ number: the window is a hard cap, so an impossible percentage means the built-in window
888
+ table is out of date for that model rather than that the session is over-full.
880
889
  - **Token badge** — `⇡<in> ⇣<out>`: cumulative input (fresh + cache-read + cache-creation)
881
890
  and output tokens for the session, k/M-formatted, with a full breakdown in the tooltip.
882
891
 
@@ -1135,6 +1144,7 @@ same-origin-guarded.
1135
1144
  | `GET /api/cost?cwd=&session=` | Estimated $ cost — session / today / month. |
1136
1145
  | `GET /api/transcript/timeline?session=&cwd=` | Per-session activity timeline (tools run). |
1137
1146
  | `GET /api/transcript/last-turn?session=&cwd=&agent=` | A session's last completed exchange (`prompt`, `reply`) plus the `text` to paste into another terminal. `agent=codex` reads the codex rollout instead of the Claude transcript. |
1147
+ | `GET /api/decisions?cwd=&limit=` | The decisions a human was asked to make in this project, newest first — each question with the options it offered, their descriptions, and the answer. `answerKind` says whether the answer was one of the options, text the user wrote instead (the question was wrong), or never given. Read out of Claude's own transcripts; writes nothing. `scanned` reports how many transcripts were read (the scan is capped) and `unreadable` how many could not be, so a partial answer is visible rather than implied. A `cwd` that is not an existing directory answers an empty response rather than falling back to the default workspace. |
1138
1148
 
1139
1149
  **Git & worktrees**
1140
1150
 
@@ -0,0 +1,88 @@
1
+ // Fenced code blocks in a markdown string (#865).
2
+ //
3
+ // The point is to hand another app EXACTLY what the agent wrote. Selecting the same text off
4
+ // the terminal picks up the leading padding xterm draws with it, which is what breaks a paste
5
+ // into Discord or Slack — so the source here is the agent's own transcript markdown, never the
6
+ // screen.
7
+ //
8
+ // Deliberately not a markdown parser. It answers one question — where do the fences sit — and
9
+ // the cases below are the ones a real reply produces:
10
+ //
11
+ // - ``` and ~~~ are both fences, and a run of 3+ of the same character opens one
12
+ // - a fence closes only on the SAME character, at least as long as the opener. That is what
13
+ // lets a shell snippet contain ``` inside a ~~~~ block without cutting it short
14
+ // - an info string (```ts, ```bash) names the language; the rest of that line is dropped
15
+ // - a fence may be indented up to 3 spaces, per CommonMark
16
+ // - an UNCLOSED fence still yields a block, running to the end of the text. An agent whose
17
+ // output was cut off mid-block is exactly when someone reaches for this, and refusing to
18
+ // return anything would read as "there is no code here"
19
+
20
+ export interface FencedBlock {
21
+ /** The info string's first word, lower-cased — `ts`, `bash` — or null when absent. */
22
+ lang: string | null;
23
+ /** The block's contents, verbatim apart from the trailing newline. */
24
+ body: string;
25
+ }
26
+
27
+ interface Fence {
28
+ /** The run itself, so its LENGTH can gate what is allowed to close it. */
29
+ run: string;
30
+ /** Everything after the run — the info string on an opener, empty on a closer. */
31
+ info: string;
32
+ }
33
+
34
+ // Scanned by hand rather than with a pattern: the obvious regex (` {0,3}(`{3,}|~{3,})(.*)`)
35
+ // nests two quantifiers under an alternation, which the linter flags for super-linear
36
+ // backtracking. This walk is plainly one pass, and reads closer to the rule it implements.
37
+ // Up to three leading spaces are allowed (CommonMark); a fourth makes it an indented code
38
+ // block, not a fence.
39
+ function fenceOf(line: string): Fence | null {
40
+ let i = 0;
41
+ while (i < 3 && line[i] === " ") i++;
42
+ const char = line[i];
43
+ if (char !== "`" && char !== "~") return null;
44
+ let length = 0;
45
+ while (line[i + length] === char) length++;
46
+ if (length < 3) return null;
47
+ return { run: line.slice(i, i + length), info: line.slice(i + length) };
48
+ }
49
+
50
+ // A closing fence is the same character, at least as long, with nothing after it. Compared
51
+ // after `.trim()` because trailing whitespace is invisible and would otherwise leave the block
52
+ // silently unclosed.
53
+ const closes = (line: string, opener: string): boolean => {
54
+ const fence = fenceOf(line);
55
+ return !!fence && fence.run[0] === opener[0] && fence.run.length >= opener.length && fence.info.trim() === "";
56
+ };
57
+
58
+ export function fencedBlocks(markdown: string): FencedBlock[] {
59
+ const lines = markdown.split("\n");
60
+ const blocks: FencedBlock[] = [];
61
+ let i = 0;
62
+ while (i < lines.length) {
63
+ const open = fenceOf(lines[i]);
64
+ if (!open) {
65
+ i++;
66
+ continue;
67
+ }
68
+ const fence = open.run;
69
+ const info = open.info.trim();
70
+ const body: string[] = [];
71
+ i++;
72
+ while (i < lines.length && !closes(lines[i], fence)) {
73
+ body.push(lines[i]);
74
+ i++;
75
+ }
76
+ i++; // step over the closing fence (or past the end, for an unclosed block)
77
+ blocks.push({ lang: info ? (info.split(/\s+/)[0].toLowerCase() ?? null) : null, body: body.join("\n") });
78
+ }
79
+ return blocks;
80
+ }
81
+
82
+ /** The block a reader means by "the code you just gave me": the last one. Null when the reply
83
+ * has no fenced block, or when the only ones are empty — an empty clipboard would look
84
+ * exactly like a copy that silently failed. */
85
+ export function lastFencedBlock(markdown: string): FencedBlock | null {
86
+ const withBody = fencedBlocks(markdown).filter((b) => b.body.trim() !== "");
87
+ return withBody.length ? withBody[withBody.length - 1] : null;
88
+ }
@@ -0,0 +1,49 @@
1
+ // A decision a human was asked to make, as it was actually asked and answered.
2
+ //
3
+ // The record is not something this app writes: Claude Code already stores every
4
+ // `AskUserQuestion` in its transcript, options and chosen answer included. These types are the
5
+ // shape we read it back in, shared because both the API and the UI decide from them (#997).
6
+
7
+ /** One option the user was offered, with the reasoning shown alongside it — this is where the
8
+ * consequence of the branch NOT taken is recorded. */
9
+ export interface DecisionOption {
10
+ label: string;
11
+ description: string;
12
+ }
13
+
14
+ /** Where the answer came from. `free-text` means the user declined every option and wrote their
15
+ * own answer, which is evidence about the QUESTION (wrong options, or a premise the asker and
16
+ * the user did not share) rather than about the choice. `unanswered` is a question that was
17
+ * asked and never resolved — the session was interrupted or abandoned. */
18
+ export type DecisionAnswerKind = "option" | "free-text" | "unanswered";
19
+
20
+ export interface DecisionQuestion {
21
+ question: string;
22
+ /** The short chip the UI showed above the question ("進め方", "Auth method"). */
23
+ header: string;
24
+ multiSelect: boolean;
25
+ options: DecisionOption[];
26
+ /** Verbatim, exactly as it came back. Null when nothing came back. */
27
+ answer: string | null;
28
+ answerKind: DecisionAnswerKind;
29
+ }
30
+
31
+ /** One `AskUserQuestion` call: the questions it asked together, and what each got back. */
32
+ export interface DecisionRecord {
33
+ sessionId: string;
34
+ cwd: string | null;
35
+ /** ISO timestamp of the turn that asked. */
36
+ ts: string;
37
+ toolUseId: string;
38
+ questions: DecisionQuestion[];
39
+ }
40
+
41
+ export interface DecisionsResponse {
42
+ decisions: DecisionRecord[];
43
+ /** How many transcripts were read successfully. Bounded, so a caller can tell a quiet project
44
+ * from a truncated scan. */
45
+ scanned: number;
46
+ /** How many could not be read at all. Reported rather than folded into `scanned`, because a
47
+ * read that failed is missing decisions — not a session that had none. */
48
+ unreadable: number;
49
+ }
@@ -0,0 +1,134 @@
1
+ // A custom theme's colours, and the rules that turn a user's `themes` entry into something the
2
+ // app can paint with. Shared across the build boundary for the same reason THEME_COLOR_KEYS is
3
+ // (see themeColors.ts): the server VALIDATES a config against these keys and the client APPLIES
4
+ // it, so a key known to one side and not the other would be accepted and then silently ignored.
5
+
6
+ import { THEME_IDS, type ThemeId } from "./themeIds.js";
7
+
8
+ // The CSS custom properties one theme sets, mirroring the `:root[data-theme=...]` blocks in
9
+ // src/style.css. A theme with no `extends` has to supply every one of them — a half-painted
10
+ // theme is worse than none, because the missing half falls back to whatever the previous theme
11
+ // left on the element.
12
+ export const THEME_VAR_KEYS = [
13
+ "--bg-base",
14
+ "--bg-deep",
15
+ "--bg-panel",
16
+ "--bg-subtle",
17
+ "--bg-elevated",
18
+ "--bg-input",
19
+ "--bg-hover",
20
+ "--bg-selected",
21
+ "--bg-selected-hover",
22
+ "--border",
23
+ "--accent",
24
+ "--accent-bg",
25
+ "--accent-bg-hover",
26
+ "--on-accent",
27
+ "--text",
28
+ "--text-secondary",
29
+ "--text-muted",
30
+ "--text-dim",
31
+ "--term-fg",
32
+ "--term-selection",
33
+ ] as const;
34
+
35
+ export type ThemeVarKey = (typeof THEME_VAR_KEYS)[number];
36
+ export type ThemeVars = Record<ThemeVarKey, string>;
37
+
38
+ /** An id a user may give a custom theme: lowercase, digits and dashes, so it is safe both as a
39
+ * `data-theme` attribute value and as something to type into a `.mulmoterminal.json`. */
40
+ export const CUSTOM_THEME_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
41
+
42
+ /** Whether a value could name a theme — built-in OR user-defined.
43
+ *
44
+ * The result is `string`, deliberately NOT `ThemeId`: that is the CLOSED set of four built-ins,
45
+ * and a guard claiming it while custom ids flow through would let downstream code treat a
46
+ * four-way switch as exhaustive (Codex review on #996). Membership is not checked here — the
47
+ * server drops an id that resolves to nothing, and termThemeFor() resolves what survives. */
48
+ export function isThemeIdLike(value: unknown): value is string {
49
+ return typeof value === "string" && CUSTOM_THEME_ID_RE.test(value);
50
+ }
51
+
52
+ export function isBuiltinThemeId(id: string): id is ThemeId {
53
+ return (THEME_IDS as readonly string[]).includes(id);
54
+ }
55
+
56
+ /** Whether an id may be used for a CUSTOM theme. Built-in ids are refused rather than merged
57
+ * into: someone reading the guide's description of Midnight has to get Midnight. */
58
+ export function isUsableCustomThemeId(id: unknown): id is string {
59
+ return typeof id === "string" && CUSTOM_THEME_ID_RE.test(id) && !isBuiltinThemeId(id);
60
+ }
61
+
62
+ export interface CustomThemeInput {
63
+ id: string;
64
+ label: string;
65
+ /** A built-in theme to start from. Omitted means `colors` must be complete. */
66
+ extends?: ThemeId;
67
+ colors: Partial<ThemeVars>;
68
+ }
69
+
70
+ /** The full variable set for a theme: the base it extends, with its own colours on top.
71
+ * `builtins` supplies the base sets so this stays pure — the client reads them from the
72
+ * stylesheet's source of truth, the specs from a fixture. */
73
+ export function resolveThemeVars(theme: CustomThemeInput, builtins: Record<ThemeId, ThemeVars>): ThemeVars | null {
74
+ const base = theme.extends ? builtins[theme.extends] : null;
75
+ const merged = { ...(base ?? {}), ...theme.colors } as Partial<ThemeVars>;
76
+ const missing = THEME_VAR_KEYS.filter((key) => !merged[key]);
77
+ // No base and an incomplete list is the one case we cannot paint: report it rather than
78
+ // leaving the gaps to whatever the previously applied theme put on the element.
79
+ return missing.length ? null : (merged as ThemeVars);
80
+ }
81
+
82
+ function channel(hex: string, at: number): number {
83
+ const value = parseInt(hex.slice(at, at + 2), 16) / 255;
84
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
85
+ }
86
+
87
+ /** `#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa` as `#rrggbb`, or null. The config schema accepts
88
+ * every one of those forms, so anything reading a colour has to expand the short ones — a
89
+ * luminance check that understood only the six-digit form would read `#fff` as unmeasurable and
90
+ * call a white theme dark (Codex review on #996).
91
+ *
92
+ * Alpha is dropped rather than composited: what a translucent `--bg-base` ends up looking like
93
+ * depends on what is behind it, which nothing here can know. The opaque colour is the closest
94
+ * honest answer, and `--bg-base` is the one variable where translucency makes least sense. */
95
+ export function normalizeHexColor(color: string): string | null {
96
+ const hex = color.trim().toLowerCase();
97
+ if (!/^#[0-9a-f]{3,8}$/.test(hex)) return null;
98
+ const digits = hex.slice(1);
99
+ if (digits.length === 3 || digits.length === 4) {
100
+ return `#${[...digits.slice(0, 3)].map((d) => d + d).join("")}`;
101
+ }
102
+ if (digits.length === 6 || digits.length === 8) return `#${digits.slice(0, 6)}`;
103
+ return null;
104
+ }
105
+
106
+ /** Relative luminance (WCAG) of any hex colour the config accepts. Null for anything else. */
107
+ export function relativeLuminance(color: string): number | null {
108
+ const hex = normalizeHexColor(color);
109
+ if (!hex) return null;
110
+ return 0.2126 * channel(hex, 1) + 0.7152 * channel(hex, 3) + 0.0722 * channel(hex, 5);
111
+ }
112
+
113
+ // Above this, the chrome is treated as a light theme. The status pills (ok / warn / err) come in
114
+ // a dark-background set and a light-background set, and picking the wrong one leaves them
115
+ // unreadable — which is why this is derived rather than declared: a theme author who never
116
+ // thinks about it still gets legible pills.
117
+ const LIGHT_LUMINANCE_THRESHOLD = 0.4;
118
+
119
+ export function isLightTheme(vars: ThemeVars): boolean {
120
+ const luminance = relativeLuminance(vars["--bg-base"]);
121
+ return luminance !== null && luminance > LIGHT_LUMINANCE_THRESHOLD;
122
+ }
123
+
124
+ /** The xterm palette a theme's variables imply. xterm draws on a canvas and cannot read CSS
125
+ * variables, so the same colours have to reach it as values — derived here rather than asked
126
+ * for twice, since two copies of one colour is two chances to change only one. The 16 ANSI
127
+ * colours are NOT derived: they come from the base theme, which is what `extends` is for. */
128
+ export function termThemeFromVars(vars: ThemeVars): { background: string; foreground: string; selectionBackground: string } {
129
+ return {
130
+ background: vars["--bg-base"],
131
+ foreground: vars["--term-fg"],
132
+ selectionBackground: vars["--term-selection"],
133
+ };
134
+ }
@@ -0,0 +1,44 @@
1
+ // The comments MulmoTerminal leaves on an issue: "a cell is on this" and "it was merged" (#979).
2
+ // Pure, and shared, because the marker is the whole idempotency story — the server writes it and
3
+ // the server reads it back, and a mismatch between the two would post the same comment on every
4
+ // poll of every tab.
5
+
6
+ export type WorkCommentKind = "start" | "merged";
7
+
8
+ // An HTML comment, so it is invisible in the rendered issue but survives a round-trip through the
9
+ // GitHub API. Keyed by kind AND directory: the same issue worked on from a second clone is a
10
+ // second, honest line in the thread, not a duplicate to suppress.
11
+ //
12
+ // The directory is percent-encoded, not interpolated raw: a folder may legally be called
13
+ // `foo-->bar`, and that string ends the HTML comment early — the rest spills into the rendered
14
+ // issue as text (Codex review). Encoding also keeps a newline or a backtick in a path from
15
+ // reshaping the comment. Ordinary names encode to themselves, so markers already posted still
16
+ // match.
17
+ export function workCommentMarker(kind: WorkCommentKind, dir: string): string {
18
+ return `<!-- mulmoterminal:work:${kind} dir=${encodeURIComponent(dir)} -->`;
19
+ }
20
+
21
+ // The directory a comment names. The BASENAME only: the point is "which of my clones", and a full
22
+ // path on a public issue leaks the machine's layout (and, on a work machine, project names).
23
+ export function workCommentDirLabel(cwd: string): string {
24
+ // Split-and-take-last rather than trimming a trailing separator with a regex: an anchored
25
+ // `[/\\]+$` backtracks super-linearly, and a path is user input.
26
+ const parts = cwd.split(/[/\\]/).filter((part) => part !== "");
27
+ return parts[parts.length - 1] ?? cwd;
28
+ }
29
+
30
+ // `pr` is the number the merge came in as, when there is one — a reader of the issue should not
31
+ // have to go looking for which PR closed it.
32
+ export function workCommentBody(kind: WorkCommentKind, dir: string, pr: number | null): string {
33
+ const marker = workCommentMarker(kind, dir);
34
+ if (kind === "start") return `Working on this in \`${dir}\`.\n\n${marker}`;
35
+ const where = pr === null ? "Merged." : `Merged in #${pr}.`;
36
+ return `${where} Work done in \`${dir}\`.\n\n${marker}`;
37
+ }
38
+
39
+ // Has this exact comment already been left? Matching on the marker rather than the prose means an
40
+ // edited comment, or a change to the wording above, still counts as "already said".
41
+ export function alreadyCommented(bodies: readonly string[], kind: WorkCommentKind, dir: string): boolean {
42
+ const marker = workCommentMarker(kind, dir);
43
+ return bodies.some((body) => typeof body === "string" && body.includes(marker));
44
+ }
@@ -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-BAMiAMZz.js";import"./chunk-WYO6CB5R-BlzOfotS-CoeWhGPT.js";import"./chunk-VAUOI2AC-CS9QJ4yz-DYmUgyge.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-B6_lajzj.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-DvKDRZGq.js";import"./chunk-WYO6CB5R-BlzOfotS-DenoOpP3.js";import"./chunk-VAUOI2AC-CS9QJ4yz-r_1UiH65.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-DDn-QIzL.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-DvKDRZGq.js";export{e as createArchitectureServices};