mulmoterminal 4.26.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +26 -0
  2. package/common/codeBlocks.ts +33 -4
  3. package/common/collectionRefresh.ts +34 -0
  4. package/common/keymap.ts +12 -1
  5. package/common/themeColors.ts +17 -0
  6. package/common/themeVars.ts +20 -2
  7. package/common/transcriptView.ts +78 -0
  8. package/dist/assets/{AllPackages-B1YCJRYB-kggoNw6z.js → AllPackages-B1YCJRYB-4WQ_EIrl.js} +1 -1
  9. package/dist/assets/{BaseConfiguration-B6s_tKOW-RQzsRUNx.js → BaseConfiguration-B6s_tKOW-CuXGj6gD.js} +1 -1
  10. package/dist/assets/{Element-BMI7Nj5w-BP5E_zl7.js → Element-BMI7Nj5w-Bik3AfgD.js} +1 -1
  11. package/dist/assets/{Entities-C8p_0jCn-C-_0T6vJ.js → Entities-C8p_0jCn-CjiJEF3r.js} +1 -1
  12. package/dist/assets/{FunctionList-YWk54TCy-DyxSzbCI.js → FunctionList-YWk54TCy-Dq1GQL-M.js} +1 -1
  13. package/dist/assets/{InputJax-C5dj-ntL-BtwdMizb.js → InputJax-C5dj-ntL-DEjnMcSa.js} +1 -1
  14. package/dist/assets/{MathItem-YJvQL-XJ-CeeATAwp.js → MathItem-YJvQL-XJ-pMaMH7Pe.js} +1 -1
  15. package/dist/assets/{MmlFactory-DSoKkE7s-CYu-aCTG.js → MmlFactory-DSoKkE7s-BTFYdeyR.js} +1 -1
  16. package/dist/assets/{Options-oARQeeuT-TJSj2iQR.js → Options-oARQeeuT-DN-QpMwi.js} +1 -1
  17. package/dist/assets/{OutputJax-DfUZNNCu-oLxtQ_g0.js → OutputJax-DfUZNNCu-0eNVVspA.js} +1 -1
  18. package/dist/assets/{PrioritizedList-6lun1Xqd-D4eZQDMF.js → PrioritizedList-6lun1Xqd-B3xDwkAm.js} +1 -1
  19. package/dist/assets/{Styles-5NmwS-3M-B9XVBSSq.js → Styles-5NmwS-3M-CYH8SIw8.js} +1 -1
  20. package/dist/assets/{TeXAtom-B5ni9myz-B3RMDIY5.js → TeXAtom-B5ni9myz-B2Lg1OeP.js} +1 -1
  21. package/dist/assets/{abnfDiagram-VCTEODGH-BGhi-oVb-DQZhwQSy.js → abnfDiagram-VCTEODGH-BGhi-oVb-rfnIw5mF.js} +1 -1
  22. package/dist/assets/{arc-CK5ru50N-CnsmPLzy.js → arc-CK5ru50N-CpwYDtFh.js} +1 -1
  23. package/dist/assets/architecture-7GRP2DOG-otSkCkT5-BXYe9Y4x.js +1 -0
  24. package/dist/assets/{architectureDiagram-5GKGNRK7-ELuPYMlf-Ci2fG9OM.js → architectureDiagram-5GKGNRK7-ELuPYMlf-jRFBK2xY.js} +1 -1
  25. package/dist/assets/{assistive-mml-CdU3rUpx-PFS2LrE8.js → assistive-mml-CdU3rUpx-CfM4F5jW.js} +1 -1
  26. package/dist/assets/{blockDiagram-I7D4REHJ-BZ7-OGWQ-iSiAD4OV.js → blockDiagram-I7D4REHJ-BZ7-OGWQ-GPkhdnf1.js} +1 -1
  27. package/dist/assets/{c4Diagram-7LVT6UL2-DCovPZ_4-DLrbYW62.js → c4Diagram-7LVT6UL2-DCovPZ_4-DlgnltG1.js} +1 -1
  28. package/dist/assets/channel-Bmu_OOc6-Byl1e1Rn.js +1 -0
  29. package/dist/assets/{chunk-4HAMMTFA-DqpzLmar-Begag6kK.js → chunk-4HAMMTFA-DqpzLmar-BYwab83k.js} +1 -1
  30. package/dist/assets/{chunk-75Z2AOVW-CT5TYzp5-Che06AJn.js → chunk-75Z2AOVW-CT5TYzp5-BRr2sap2.js} +1 -1
  31. package/dist/assets/{chunk-DU6HZSFF-BVf-rtdf-BY6ORnqO.js → chunk-DU6HZSFF-BVf-rtdf-BiTD5bH2.js} +1 -1
  32. package/dist/assets/{chunk-F27PBJKO-rP8pUsk3-DnQrEHyt.js → chunk-F27PBJKO-rP8pUsk3-jZL8tnaO.js} +1 -1
  33. package/dist/assets/{chunk-IMKFNOWR-D_IT0r3G-CZbWs1tw.js → chunk-IMKFNOWR-D_IT0r3G-4Na8DCGZ.js} +1 -1
  34. package/dist/assets/{chunk-POPQ4Y6H-C5YURnHe-Pr-7cHNE.js → chunk-POPQ4Y6H-C5YURnHe-BtvIAZQ9.js} +1 -1
  35. package/dist/assets/{chunk-SHT3W25Y-CfF0J8Qh-BjabP1RZ.js → chunk-SHT3W25Y-CfF0J8Qh-CSKY6pei.js} +1 -1
  36. package/dist/assets/{chunk-SVP7TREG-CTZbRIbX-B_MoCNpo.js → chunk-SVP7TREG-CTZbRIbX-DXmyUlG6.js} +1 -1
  37. package/dist/assets/{chunk-TICWLB2K-DP1lYmZm-DBUczLZh.js → chunk-TICWLB2K-DP1lYmZm-2WQGl4am.js} +1 -1
  38. package/dist/assets/{chunk-XXDRQBXY-vg0kfC73-D9SGVKJn.js → chunk-XXDRQBXY-vg0kfC73-BNbpCBp8.js} +1 -1
  39. package/dist/assets/classDiagram-ZZMXUADV-BDQOAOBf-DGA1_qS0.js +1 -0
  40. package/dist/assets/classDiagram-v2-VYDZK3BY-CwPXtzoF-DGA1_qS0.js +1 -0
  41. package/dist/assets/{cose-bilkent-JH36ORCC-qIiD3RGD-BUItDgWJ.js → cose-bilkent-JH36ORCC-qIiD3RGD-glIzrMsu.js} +1 -1
  42. package/dist/assets/cynefin-OW5HDTMX-Xr6IPdZG-DCyyQmwN.js +1 -0
  43. package/dist/assets/{cynefinDiagram-5FMLGOSQ-BZrUCS-v-Vi8fZx7J.js → cynefinDiagram-5FMLGOSQ-BZrUCS-v-BP9iYDiL.js} +1 -1
  44. package/dist/assets/{dagre-C2SZ8h4o-BotF2A9M.js → dagre-C2SZ8h4o-Cvn7MMN8.js} +1 -1
  45. package/dist/assets/{dagre-GXQ25YYZ-CQcLM4na-DqruNADY.js → dagre-GXQ25YYZ-CQcLM4na-CUPIues9.js} +1 -1
  46. package/dist/assets/{diagram-S7CK7UJ4-BdmcmUQh-BpurSv4a.js → diagram-S7CK7UJ4-BdmcmUQh-BzmDaJl4.js} +1 -1
  47. package/dist/assets/{diagram-UQ7AKVKN-BtvmEiYl-XHbHkO0x.js → diagram-UQ7AKVKN-BtvmEiYl-C-SDkeQT.js} +1 -1
  48. package/dist/assets/{diagram-VSXAHHWV-qEG6N0bV-Cr5ut8_j.js → diagram-VSXAHHWV-qEG6N0bV-BBEGoVq0.js} +1 -1
  49. package/dist/assets/{diagram-VX7I27RA-BA8is49N-Bq5gY7-i.js → diagram-VX7I27RA-BA8is49N-C_RijuTO.js} +1 -1
  50. package/dist/assets/{diagram-Z3DM3KII-XPcU7IOQ-C0yHj4hB.js → diagram-Z3DM3KII-XPcU7IOQ-CxOwdwHR.js} +1 -1
  51. package/dist/assets/{dist-BIvNvTDO.js → dist-Bi70Jy-b.js} +1 -1
  52. package/dist/assets/{dist-BNoLRzs-.js → dist-BvG2w_9k.js} +1 -1
  53. package/dist/assets/{dist-Fs7Cl-b9.js → dist-CWF1S7vL.js} +1 -1
  54. package/dist/assets/{dist-OM0ZTf3D.js → dist-CXGYSqlO.js} +1 -1
  55. package/dist/assets/{dist-BwEUG-6y.js → dist-Cb59cZDS.js} +1 -1
  56. package/dist/assets/{dist-wMJX53du.js → dist-D2QMLIZp.js} +1 -1
  57. package/dist/assets/{dist-BP4I4Ek8.js → dist-D3PmXl_m.js} +1 -1
  58. package/dist/assets/{dist-CoNrzCGM.js → dist-DBRVDw9D.js} +1 -1
  59. package/dist/assets/{dist-DeiyD9Ki-CG6FY-Fn.js → dist-DeiyD9Ki-BDcmanZh.js} +1 -1
  60. package/dist/assets/{dist-CBAuHlC8.js → dist-Gt7_-BLP.js} +1 -1
  61. package/dist/assets/{dist-DgKptpvT.js → dist-uQlE_JYp.js} +1 -1
  62. package/dist/assets/{ebnfDiagram-PWID7BFC-CTHIHdBS-BJwIOuLO.js → ebnfDiagram-PWID7BFC-CTHIHdBS-BGwyAx_X.js} +1 -1
  63. package/dist/assets/{erDiagram-RLTQ6QDP-DVpWTNZj-D8m4MgiI.js → erDiagram-RLTQ6QDP-DVpWTNZj-CpwnuTPx.js} +1 -1
  64. package/dist/assets/eventmodeling-NTZA5JFV-B7bRrpt_-DWVjLwmJ.js +1 -0
  65. package/dist/assets/flowDiagram-HODETNUW-r-3T5g30-CjAJJOoL.js +1 -0
  66. package/dist/assets/{ganttDiagram-EL5Y4UJY-BfAgk15F-0jMcI0Yj.js → ganttDiagram-EL5Y4UJY-BfAgk15F-CuhMMDMU.js} +1 -1
  67. package/dist/assets/gitGraph-4MIJSDKK-BhzSx5wq-CmU_NC2u.js +1 -0
  68. package/dist/assets/{gitGraphDiagram-WWUBYQGX-DEVVZ6wt-CawiFyGd.js → gitGraphDiagram-WWUBYQGX-DEVVZ6wt-Bh_VaB4G.js} +1 -1
  69. package/dist/assets/{graphlib-BYLhHyYO-CEk4IQ-f.js → graphlib-BYLhHyYO-BCQclP1Y.js} +1 -1
  70. package/dist/assets/{html-Z7dGbczU-BwxpBy6_.js → html-Z7dGbczU-UIlR1xAp.js} +1 -1
  71. package/dist/assets/index-DgdW3UkV.css +1 -0
  72. package/dist/assets/{index-D7Oh_lqc.js → index-sxsMmFcI.js} +327 -379
  73. package/dist/assets/info-A6RAGUB7-fR2SUJmJ-Dc6y50d4.js +1 -0
  74. package/dist/assets/{infoDiagram-27XIBGKW-CvHUhAHd-Y8J_Kc2X.js → infoDiagram-27XIBGKW-CvHUhAHd-DigY259e.js} +1 -1
  75. package/dist/assets/{ishikawaDiagram-5VMMS53U-CeqyKdwk-q6ZNkcwN.js → ishikawaDiagram-5VMMS53U-CeqyKdwk-ACmU-Jzv.js} +1 -1
  76. package/dist/assets/{journeyDiagram-3NMN7TZE-Jfi7MPDi-Bv6pv8wY.js → journeyDiagram-3NMN7TZE-Jfi7MPDi-YIFwpJyP.js} +1 -1
  77. package/dist/assets/{kanban-definition-UXKFOSKX-yOrlojns-B2y6NbLe.js → kanban-definition-UXKFOSKX-yOrlojns-0RXVl3u9.js} +1 -1
  78. package/dist/assets/{lengths-CvmPfBid-CBfhrPT6.js → lengths-CvmPfBid-BAfNjGrF.js} +1 -1
  79. package/dist/assets/{linear-jdmEy-qs-oCunWRsH.js → linear-jdmEy-qs-Djjlqhox.js} +1 -1
  80. package/dist/assets/{liteAdaptor-pH5MXTG0-BY38m-jj.js → liteAdaptor-pH5MXTG0-pAc8cmKr.js} +1 -1
  81. package/dist/assets/{mathjax-DVEo4UYj-C7BTzxwK.js → mathjax-DVEo4UYj-Cnj1zcS1.js} +1 -1
  82. package/dist/assets/{mermaid-parser.core-CzBW8Fof-CixE9zmB.js → mermaid-parser.core-CzBW8Fof-s3bWPn0O.js} +2 -2
  83. package/dist/assets/{mermaid.core-Dv4uylek-Djmx6tFE.js → mermaid.core-Dv4uylek-DqGXjOPu.js} +4 -4
  84. package/dist/assets/{mindmap-definition-YA3MSWOX-DP_2R9Ga-kTAulFVa.js → mindmap-definition-YA3MSWOX-DP_2R9Ga-KrOUlEvG.js} +1 -1
  85. package/dist/assets/{mo-Xv_6CC7A-CcvkyIE0.js → mo-Xv_6CC7A-5T19MjVl.js} +1 -1
  86. package/dist/assets/packet-AYTQ26CC-B_oKR9KZ-DzcrK2qY.js +1 -0
  87. package/dist/assets/{pegDiagram-XKGWAZYB-BsBWryhi-BiG1mp1T.js → pegDiagram-XKGWAZYB-BsBWryhi-DYwgFu60.js} +1 -1
  88. package/dist/assets/pie-WAS4IAKB-D1ZXrN1W-EXLeDSJc.js +1 -0
  89. package/dist/assets/{pieDiagram-E7YTZNPT-OugkS2pU-C5WoCQDp.js → pieDiagram-E7YTZNPT-OugkS2pU-CitvFGku.js} +1 -1
  90. package/dist/assets/{quadrantDiagram-AXDQQJYC-DXEAAaUE-U0asGLtC.js → quadrantDiagram-AXDQQJYC-DXEAAaUE-BJz-5fWN.js} +1 -1
  91. package/dist/assets/radar-RG4KPBEZ-BmUgDkiH-CYsT6Q2i.js +1 -0
  92. package/dist/assets/railroad-74A4TZTK-6lEXVJo8-CGKkcPl-.js +1 -0
  93. package/dist/assets/railroad-abnf-HS5TGJTU-BG28Qiz1-CX4pHVjI.js +1 -0
  94. package/dist/assets/railroad-ebnf-LZEXJU2U-DzpHhtTH-DuFCuwxK.js +1 -0
  95. package/dist/assets/railroad-peg-WCYAUIDC-B2CHCTyG-BZCRCTz5.js +1 -0
  96. package/dist/assets/{railroadDiagram-O6MQD6OU-BJys8O96-Dcm_CJA0.js → railroadDiagram-O6MQD6OU-BJys8O96-8S8j18MZ.js} +1 -1
  97. package/dist/assets/{requirementDiagram-BXWQKSXE-BaDgCtM5-BFyzmV4B.js → requirementDiagram-BXWQKSXE-BaDgCtM5-D0sLbDHs.js} +1 -1
  98. package/dist/assets/{sankeyDiagram-P5KCCOFB-puZsr_vK-CTerarXU.js → sankeyDiagram-P5KCCOFB-puZsr_vK-C1OX3N7F.js} +1 -1
  99. package/dist/assets/{sequenceDiagram-WJ2MYXX4-D1ziEryC-Dk_Vh7EW.js → sequenceDiagram-WJ2MYXX4-D1ziEryC-CvoJhcsM.js} +1 -1
  100. package/dist/assets/{src-CLC0ddSd-dG3DpXd2.js → src-CLC0ddSd-D-EBSy-7.js} +1 -1
  101. package/dist/assets/{stateDiagram-D77RDMKH-HSEurT6h-CpOTqfOS.js → stateDiagram-D77RDMKH-HSEurT6h-DWNnPlrT.js} +1 -1
  102. package/dist/assets/stateDiagram-v2-MP3YSRHH-DnOa-Otc-jwvolGHy.js +1 -0
  103. package/dist/assets/{svg-D_RYJvQC-C4pZWbZ-.js → svg-D_RYJvQC-xxS_UOMd.js} +1 -1
  104. package/dist/assets/{swimlanes-42K2YHIH-umBsuvsI-C-uZTY-8.js → swimlanes-42K2YHIH-umBsuvsI-BE4MiInp.js} +1 -1
  105. package/dist/assets/swimlanesDiagram-VR7AAH4N-ZLUyNmnU-DNoFZykH.js +8 -0
  106. package/dist/assets/{tex-Dx2UJ3VU-Ch7WPvt4.js → tex-Dx2UJ3VU-Bvt244KZ.js} +1 -1
  107. package/dist/assets/{timeline-definition-24CTP7MA-D9plt22Y-BgXy-Pja.js → timeline-definition-24CTP7MA-D9plt22Y-DHwEP0-q.js} +1 -1
  108. package/dist/assets/treeView-Q6P3EWNA-BdvZ0_C5-Cw7gALPI.js +1 -0
  109. package/dist/assets/treemap-WGGIJYW6-CIJLPQlC-BuBwqHqi.js +1 -0
  110. package/dist/assets/{vennDiagram-4TSXK5OY-DZe7wY85-HHFNkULb.js → vennDiagram-4TSXK5OY-DZe7wY85-BzqCxOQu.js} +1 -1
  111. package/dist/assets/wardley-WFR3VGLG-CKqtFD8V-Ds6_n_M_.js +1 -0
  112. package/dist/assets/{wardleyDiagram-VM6X3IG4-DK9yytWx-qeOMmNlR.js → wardleyDiagram-VM6X3IG4-DK9yytWx-FK9bW_eQ.js} +1 -1
  113. package/dist/assets/{xychartDiagram-S5SC5T6Z-CLSO4Pc9-DLpk6ryk.js → xychartDiagram-S5SC5T6Z-CLSO4Pc9-BGF6axe4.js} +1 -1
  114. package/dist/index.html +3 -3
  115. package/package.json +9 -9
  116. package/server/agents/copilot-sessions.ts +12 -4
  117. package/server/agents/sqlite-read.ts +1 -1
  118. package/server/backends/calendarRefresh.ts +37 -0
  119. package/server/backends/calendarRefreshResult.ts +40 -0
  120. package/server/backends/feeds.ts +40 -9
  121. package/server/backends/remoteHost/handlers/deps.ts +1 -1
  122. package/server/backends/remoteHost/handlers.spec.ts +1 -1
  123. package/server/backends/sharedApp/headlessPreview.ts +46 -4
  124. package/server/config/config-schema.ts +6 -3
  125. package/server/files/files-browse.ts +14 -0
  126. package/server/files/project-files.ts +222 -0
  127. package/server/infra/jsonl-file.ts +13 -3
  128. package/server/routes/app-routes.ts +1 -1
  129. package/server/routes/session-routes.ts +33 -0
  130. package/server/session/transcript-view-codex.ts +4 -3
  131. package/server/session/transcript-view-copilot.ts +11 -9
  132. package/server/session/transcript-view-cursor.ts +3 -2
  133. package/server/session/transcript-view-read.ts +214 -42
  134. package/server/session/transcript-view.ts +47 -43
  135. package/server/skills/mulmoterminal-dirs/SKILL.md +3 -1
  136. package/server/skills/mulmoterminal-keys/SKILL.md +6 -0
  137. package/server/skills/mulmoterminal-theme/SKILL.md +49 -7
  138. package/dist/assets/architecture-7GRP2DOG-otSkCkT5-CFnWjMcA.js +0 -1
  139. package/dist/assets/channel-Bmu_OOc6-CLX7xRxl.js +0 -1
  140. package/dist/assets/classDiagram-ZZMXUADV-BDQOAOBf-CH44RAkG.js +0 -1
  141. package/dist/assets/classDiagram-v2-VYDZK3BY-CwPXtzoF-CH44RAkG.js +0 -1
  142. package/dist/assets/cynefin-OW5HDTMX-Xr6IPdZG-Ce8mYJyA.js +0 -1
  143. package/dist/assets/eventmodeling-NTZA5JFV-B7bRrpt_-DnpJhuuA.js +0 -1
  144. package/dist/assets/flowDiagram-HODETNUW-r-3T5g30-BWizd0qT.js +0 -1
  145. package/dist/assets/gitGraph-4MIJSDKK-BhzSx5wq-BmNB3hui.js +0 -1
  146. package/dist/assets/index-BJzCebxZ.css +0 -1
  147. package/dist/assets/info-A6RAGUB7-fR2SUJmJ-DJ6qtBt_.js +0 -1
  148. package/dist/assets/packet-AYTQ26CC-B_oKR9KZ-Pi3e-tAz.js +0 -1
  149. package/dist/assets/pie-WAS4IAKB-D1ZXrN1W-BOglNRvk.js +0 -1
  150. package/dist/assets/radar-RG4KPBEZ-BmUgDkiH-zXnWiLPM.js +0 -1
  151. package/dist/assets/railroad-74A4TZTK-6lEXVJo8-CAtjJocL.js +0 -1
  152. package/dist/assets/railroad-abnf-HS5TGJTU-BG28Qiz1-BzFi41zD.js +0 -1
  153. package/dist/assets/railroad-ebnf-LZEXJU2U-DzpHhtTH-CFTlT-st.js +0 -1
  154. package/dist/assets/railroad-peg-WCYAUIDC-B2CHCTyG-uG4QNP6w.js +0 -1
  155. package/dist/assets/stateDiagram-v2-MP3YSRHH-DnOa-Otc-DgkDdTbh.js +0 -1
  156. package/dist/assets/swimlanesDiagram-VR7AAH4N-ZLUyNmnU-CxUAVNvR.js +0 -8
  157. package/dist/assets/treeView-Q6P3EWNA-BdvZ0_C5-VCyiiE1t.js +0 -1
  158. package/dist/assets/treemap-WGGIJYW6-CIJLPQlC-jg24gWZX.js +0 -1
  159. package/dist/assets/wardley-WFR3VGLG-CKqtFD8V-hF5yPxTk.js +0 -1
package/README.md CHANGED
@@ -1232,6 +1232,18 @@ The toggle is not the only way in: while a cell is enlarged, **clicking a file p
1232
1232
  printed** opens it here too, rather than in a new tab or full-screen — see
1233
1233
  [Clicking a file path](#clicking-a-file-path).
1234
1234
 
1235
+ **Open a file by name instead of walking the tree.** The pane's header has a **search** button
1236
+ that opens a finder over it: type part of a file's name or path and it narrows a list of every
1237
+ file in the project — fuzzily, so `fpane` finds `src/components/FilesPane.vue`. Arrow keys and
1238
+ Enter pick one; it opens in the editor **with the tree expanded down to it**, so the files
1239
+ around it are one click away. In a git repository the candidates come from `git ls-files`, so
1240
+ everything your `.gitignore` excludes — `node_modules` and the rest — is never offered; outside
1241
+ one the tree is walked instead: no ignore file is read, and only a short list of directories
1242
+ nobody authors by hand (`node_modules`, virtualenvs, caches) is skipped. The panel says so rather
1243
+ than letting you believe an ignore file was applied. There is also a
1244
+ [`files-find` shortcut](https://receptron.github.io/mulmoterminal/guide/en/config.html#keymap)
1245
+ with no default binding, which opens the pane first if it is closed.
1246
+
1235
1247
  All reads and writes go through `GET/PUT /api/files/browse/*?cwd=&path=`, and every
1236
1248
  `path` is **contained within the project root** (server-side) — `..`/absolute escapes
1237
1249
  are rejected for reads and writes alike, so editing can't reach outside the directory
@@ -1629,6 +1641,20 @@ narrow and tall for one record you are discussing — and each position keeps it
1629
1641
  prompt history, codex's rollout), so a prompt sent mid-turn is there and text a skill
1630
1642
  injected is not. A `/clear` draws a line: the pane shows what you have asked *since*, the
1631
1643
  same way the header, the title and the last reply all stop describing the ended conversation.
1644
+ - **Conversation pane** — the enlarged cell's conversation, read as TURNS rather than as the
1645
+ screen, from `GET /api/transcript/view`. A Claude cell runs on the alternate screen, so the
1646
+ terminal keeps no scrollback worth scrolling; this reads the agent's own transcript instead,
1647
+ and **scrolling up walks the session back to its first turn**, a page at a time.
1648
+ - It **takes the terminal's place when you open it** — the one pane that does, because reading
1649
+ is its whole subject. The expand button puts the terminal back beside it.
1650
+ - **A frame per speaker**: what you asked in one, what the agent answered in another, named
1651
+ ("You" / "Claude"). The reply is **rendered markdown** — headings, lists, tables, fenced
1652
+ code — not the characters it is written with.
1653
+ - **Tool traffic is folded away**, one line saying what ran (`Bash · Read`); click to open it.
1654
+ It is most of a turn's bulk and almost none of what you came back to read.
1655
+ - A **snapshot**, not a live tail — the terminal beside it is the live view — and the header's
1656
+ reload fetches the newest turns again. Claude, Codex, Cursor and Copilot; an agent whose
1657
+ conversation no reader here can read yet says so rather than showing an empty pane.
1632
1658
  - **Notifications** (🔔) — a toolbar bell with an unread badge and a dropdown of active
1633
1659
  notifications; click a row to jump to its session.
1634
1660
  - **Star MulmoTerminal** — a star button in the grid toolbar that stars the project on GitHub
@@ -55,9 +55,31 @@ const closes = (line: string, opener: string): boolean => {
55
55
  return !!fence && fence.run[0] === opener[0] && fence.run.length >= opener.length && fence.info.trim() === "";
56
56
  };
57
57
 
58
- export function fencedBlocks(markdown: string): FencedBlock[] {
58
+ /** A run of prose, or one fenced block — the whole text, in order and with nothing dropped.
59
+ *
60
+ * This is the ONE walk over the fences, and `fencedBlocks` below is it with the prose dropped. It
61
+ * is not a second scanner: the two used to be separate loops over the same rules, which is two
62
+ * places for a fence rule to drift.
63
+ *
64
+ * It is deliberately not tied to any one reader. The transcript pane rendered its segments for a
65
+ * while and now renders the reply as markdown instead (#2112); saying otherwise here sends the next
66
+ * reader looking for a consumer that no longer exists (Claude review, round 1). */
67
+ export type MarkdownSegment = { kind: "text"; text: string } | { kind: "code"; lang: string | null; body: string };
68
+
69
+ /** The one walk over the fences. `fencedBlocks` is this with the prose dropped.
70
+ *
71
+ * Prose runs are joined with the newlines they had, so a run's own blank lines survive; a segment
72
+ * is emitted only when it has a line in it, so an input that opens with a fence does not lead with
73
+ * an empty one. */
74
+ export function markdownSegments(markdown: string): MarkdownSegment[] {
59
75
  const lines = markdown.split("\n");
60
- const blocks: FencedBlock[] = [];
76
+ const segments: MarkdownSegment[] = [];
77
+ const prose: string[] = [];
78
+ const flushProse = (): void => {
79
+ if (prose.length === 0) return;
80
+ segments.push({ kind: "text", text: prose.join("\n") });
81
+ prose.length = 0;
82
+ };
61
83
  let i = 0;
62
84
  // Each line is read into a const before use: with noUncheckedIndexedAccess, `lines[i]` is
63
85
  // `string | undefined` even inside `i < lines.length`, and the loop condition is what actually
@@ -66,9 +88,11 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
66
88
  const line = lines[i];
67
89
  const open = line === undefined ? null : fenceOf(line);
68
90
  if (!open) {
91
+ if (line !== undefined) prose.push(line);
69
92
  i++;
70
93
  continue;
71
94
  }
95
+ flushProse();
72
96
  const fence = open.run;
73
97
  const info = open.info.trim();
74
98
  const body: string[] = [];
@@ -78,9 +102,14 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
78
102
  i++;
79
103
  }
80
104
  i++; // step over the closing fence (or past the end, for an unclosed block)
81
- blocks.push({ lang: info ? (info.split(/\s+/)[0]?.toLowerCase() ?? null) : null, body: body.join("\n") });
105
+ segments.push({ kind: "code", lang: info ? (info.split(/\s+/)[0]?.toLowerCase() ?? null) : null, body: body.join("\n") });
82
106
  }
83
- return blocks;
107
+ flushProse();
108
+ return segments;
109
+ }
110
+
111
+ export function fencedBlocks(markdown: string): FencedBlock[] {
112
+ return markdownSegments(markdown).flatMap((segment) => (segment.kind === "code" ? [{ lang: segment.lang, body: segment.body }] : []));
84
113
  }
85
114
 
86
115
  /** The block a reader means by "the code you just gave me": the last one. Null when the reply
@@ -0,0 +1,34 @@
1
+ // The `POST /api/collections/:slug/refresh` response. One shape for BOTH arms of that
2
+ // route — a declarative/agent feed's `ingest` re-run and a `googleCalendar` sync — because
3
+ // the collection view reads one result whichever arm answered, and two host-side shapes for
4
+ // one button is how a field ends up set by one arm and never read from the other.
5
+ //
6
+ // It mirrors MulmoClaude's `CollectionRefreshBody` (server/api/routes/collectionCalendarRefresh.ts)
7
+ // so the two hosts over the shared workspace answer the same plugin identically. Re-stated
8
+ // rather than imported, for `collectionPush.ts`'s reason: the plugin ships its
9
+ // `CollectionRefreshResult` from `@mulmoclaude/collection-plugin/vue`, and the server has no
10
+ // business pulling a Vue package in to describe its own response.
11
+ //
12
+ // An absent field is an ABSENT KEY, never an explicit `undefined`. That is what the wire carries
13
+ // either way (JSON.stringify drops an undefined value), and under `exactOptionalPropertyTypes` it
14
+ // is also what makes this assignable to the plugin's own `CollectionRefreshResult` — so the
15
+ // binding in `collectionUi.ts` type-checks the server's shape against the view's expectation
16
+ // instead of both sides describing it separately.
17
+
18
+ export interface CollectionRefreshResult {
19
+ /** Always true — "the refresh ran", not "records changed". A refusal is told through
20
+ * `errors`, the way the push's `pushed` works. */
21
+ refreshed: true;
22
+ written: number;
23
+ /** Why the refresh did not do what was asked. Read beside the button, so an empty list is
24
+ * the only thing that means "it worked". */
25
+ errors: string[];
26
+ /** Records a `googleCalendar` sync deleted (the event was cancelled in Google). Sent by the
27
+ * calendar arm only: the feed engine counts its own `maxItems` evictions, but the feed arm
28
+ * has never reported them and MulmoClaude's does not either. */
29
+ removed?: number;
30
+ /** An agent-ingest feed dispatched a worker session instead of writing records itself. */
31
+ dispatched?: boolean;
32
+ /** The dispatched session, for the view to navigate to. */
33
+ chatId?: string;
34
+ }
package/common/keymap.ts CHANGED
@@ -22,6 +22,7 @@ export const KEYMAP_ACTIONS = [
22
22
  "terminal-new-adjacent",
23
23
  "terminal-close",
24
24
  "terminal-restart",
25
+ "files-find",
25
26
  "copy",
26
27
  "paste",
27
28
  ] as const;
@@ -49,7 +50,17 @@ export const TERMINAL_SCOPED_ACTIONS: readonly KeymapAction[] = ["copy", "paste"
49
50
  // Here rather than beside gridShortcutFor because BOTH sides decide from it: the grid dispatches on
50
51
  // it, and validateKeymap has to know that these decline the key — the handler returns WITHOUT
51
52
  // stopping the event, so a `send` on the same keystroke fires instead (codex on #1906).
52
- export const NEEDS_A_CURRENT_TERMINAL: readonly KeymapAction[] = ["zoom-next", "zoom-prev", "terminal-new-adjacent", "terminal-close", "terminal-restart"];
53
+ // `files-find` is here because the pane it opens exists only in the ENLARGED row (see
54
+ // docs/grid-view-modes.md) — a tiled grid has nowhere to put it, so the key declines rather than
55
+ // guessing which of nine terminals was meant.
56
+ export const NEEDS_A_CURRENT_TERMINAL: readonly KeymapAction[] = [
57
+ "zoom-next",
58
+ "zoom-prev",
59
+ "terminal-new-adjacent",
60
+ "terminal-close",
61
+ "terminal-restart",
62
+ "files-find",
63
+ ];
53
64
 
54
65
  // A key that puts BYTES into the focused terminal instead of running an app action (#1005) —
55
66
  // Cmd+Right as Ctrl+E for end-of-line, say, or Alt+B for word-back.
@@ -28,3 +28,20 @@ export const THEME_COLOR_KEYS = [
28
28
  "brightCyan",
29
29
  "brightWhite",
30
30
  ] as const;
31
+
32
+ export type ThemeColorKey = (typeof THEME_COLOR_KEYS)[number];
33
+
34
+ /** What a palette colour may look like: xterm accepts `#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa`.
35
+ * Here rather than in the server schema because BOTH sides check it — the server rejects a config
36
+ * on it, and the client re-checks the response before the value reaches a canvas. Two copies of
37
+ * this pattern is how one side ends up accepting what the other drops. */
38
+ export const PALETTE_COLOR_RE = /^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
39
+
40
+ /** STRICT — surrounding whitespace is a reject, not something to trim away. The server's
41
+ * `paletteColor` trims before it matches and hands back the trimmed value, so everything this
42
+ * predicate legitimately sees has already been normalised; accepting `" #fff "` here would mean
43
+ * forwarding that exact string to xterm, which does not trim and cannot parse it. Validate the
44
+ * value you are about to pass on, not a cleaned-up version of it. */
45
+ export function isPaletteColor(value: unknown): value is string {
46
+ return typeof value === "string" && PALETTE_COLOR_RE.test(value);
47
+ }
@@ -4,6 +4,7 @@
4
4
  // it, so a key known to one side and not the other would be accepted and then silently ignored.
5
5
 
6
6
  import { THEME_IDS, type ThemeId } from "./themeIds.js";
7
+ import type { ThemeColorKey } from "./themeColors.js";
7
8
 
8
9
  // The CSS custom properties one theme sets, mirroring the `:root[data-theme=...]` blocks in
9
10
  // src/style.css. A theme with no `extends` has to supply every one of them — a half-painted
@@ -65,6 +66,10 @@ export interface CustomThemeInput {
65
66
  /** A built-in theme to start from. Omitted means `colors` must be complete. */
66
67
  extends?: ThemeId;
67
68
  colors: Partial<ThemeVars>;
69
+ /** xterm palette entries set outright, for the canvas colours `colors` cannot reach: the
70
+ * cursor pair, the selection foreground, the 16 ANSI colours. Wins over what `colors`
71
+ * implies (#2097). */
72
+ term?: Partial<Record<ThemeColorKey, string>>;
68
73
  }
69
74
 
70
75
  /** The full variable set for a theme: the base it extends, with its own colours on top.
@@ -132,11 +137,24 @@ export function isLightTheme(vars: ThemeVars): boolean {
132
137
  /** The xterm palette a theme's variables imply. xterm draws on a canvas and cannot read CSS
133
138
  * variables, so the same colours have to reach it as values — derived here rather than asked
134
139
  * for twice, since two copies of one colour is two chances to change only one. The 16 ANSI
135
- * colours are NOT derived: they come from the base theme, which is what `extends` is for. */
136
- export function termThemeFromVars(vars: ThemeVars): { background: string; foreground: string; selectionBackground: string } {
140
+ * colours are NOT derived: they come from the base theme, which is what `extends` is for.
141
+ *
142
+ * The cursor pair is derived from the SAME two variables the cell it sits on uses, which makes
143
+ * the cursor an inverted cell — so the character under it stays as readable as the characters
144
+ * beside it. Inheriting the cursor from the base instead is what made a light theme extending a
145
+ * dark one draw a dark block with xterm's default black glyph on top (#2097). */
146
+ export function termThemeFromVars(vars: ThemeVars): {
147
+ background: string;
148
+ foreground: string;
149
+ selectionBackground: string;
150
+ cursor: string;
151
+ cursorAccent: string;
152
+ } {
137
153
  return {
138
154
  background: vars["--bg-base"],
139
155
  foreground: vars["--term-fg"],
140
156
  selectionBackground: vars["--term-selection"],
157
+ cursor: vars["--term-fg"],
158
+ cursorAccent: vars["--bg-base"],
141
159
  };
142
160
  }
@@ -0,0 +1,78 @@
1
+ // What a hosted agent's conversation looks like ON THE WIRE.
2
+ //
3
+ // In `common/` because both ends decide from it: the host folds an agent's JSONL (or copilot's
4
+ // table) into these shapes in `server/session/transcript-view.ts`, and two clients render them —
5
+ // the phone's terminal detail page over the remoteHost socket, and the browser's transcript pane
6
+ // over `/api/transcript/view`. Mirroring the shape into `src/` would let one side gain a status the
7
+ // other silently renders as nothing.
8
+ //
9
+ // The RULES that produce these — the turn boundary, the line budget, the tool-result cap — stay on
10
+ // the server. This file is the vocabulary, not the folding.
11
+
12
+ export type TranscriptRowKind = "user" | "assistant" | "tool" | "unknown";
13
+
14
+ /** One content block, rendered.
15
+ *
16
+ * `text` may itself contain newlines — an assistant answer is passed through whole, because the
17
+ * wrapping belongs to the client's CSS and not to a host that cannot know its width.
18
+ *
19
+ * `clipped` says THIS row's text was cut, which is a different fact from `TranscriptView.truncated`
20
+ * (a whole turn was dropped). Naming them the same word would leave the client unable to decide
21
+ * which mark to draw. */
22
+ export interface TranscriptRow {
23
+ kind: TranscriptRowKind;
24
+ text: string;
25
+ clipped?: boolean;
26
+ /** This tool row is the CALL — the tool's name and the head of its arguments — rather than what
27
+ * the tool answered (#2112).
28
+ *
29
+ * Said by the host because only the host can say it: both halves arrive as `kind: "tool"`, and a
30
+ * client telling them apart by shape ("short and single-line means a name") is guessing about
31
+ * four agents' output at once. A pane that collapses tool traffic needs it to label the fold with
32
+ * what RAN — the part a reader is looking for when they open it again. */
33
+ call?: boolean;
34
+ }
35
+
36
+ /** One exchange: a user prompt and everything that followed it.
37
+ *
38
+ * `at` is the BOUNDARY record's own timestamp — the moment the turn started — or null when it is
39
+ * not a string. Null rather than dropping the turn: a turn is worth more than its clock. */
40
+ export interface TranscriptTurn {
41
+ at: string | null;
42
+ rows: TranscriptRow[];
43
+ }
44
+
45
+ /** What the host answers. A discriminated union rather than "readable: boolean": "no transcript
46
+ * yet", "the conversation was ended with /clear" and "too big to find a turn in" are three
47
+ * different things to tell a person, and one boolean collapses them into the same blank view. */
48
+ export type TranscriptView =
49
+ | { status: "ok"; turns: TranscriptTurn[]; truncated: boolean }
50
+ | { status: "none" }
51
+ | { status: "cleared" }
52
+ | { status: "too-large" }
53
+ /** This session's agent keeps a conversation somewhere, and no reader here can read it yet
54
+ * (#1822). A DIFFERENT fact from `none`, which means "this session has written nothing we can
55
+ * find" — the phone falls back to the screen for both, but only one of them is worth a sentence
56
+ * to a person, and only one of them is a thing to go and implement.
57
+ *
58
+ * Never answered for a shell or a launcher cell: those have no conversation and never will, so
59
+ * the screen IS their content rather than a fallback from something missing. */
60
+ | { status: "not-supported" };
61
+
62
+ /** One page of a conversation, read backwards from `older` (#2112).
63
+ *
64
+ * The cursor is OPAQUE to the client on purpose: it is a byte offset for the three agents that
65
+ * keep a file and a `turn_index` for the one that keeps a table, and a client that knew which
66
+ * would be deciding something only the host can know. It is handed back verbatim to ask for the
67
+ * page before this one.
68
+ *
69
+ * `older: null` means this page reaches the start of what can be read — either the head of the
70
+ * transcript, or the point past which the host refuses to read (`status: "too-large"`).
71
+ *
72
+ * The view is carried whole rather than flattened in beside the cursor: every non-`ok` status is
73
+ * a sentence the client already knows how to show, and re-deciding them per page is how the two
74
+ * ends drift. */
75
+ export interface TranscriptPage {
76
+ view: TranscriptView;
77
+ older: string | null;
78
+ }