mulmoterminal 5.2.0 → 5.3.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.
- package/README.md +47 -0
- package/common/fileChannel.ts +42 -0
- package/common/fileSearch.ts +211 -0
- package/common/keymap.ts +5 -3
- package/dist/assets/{AllPackages-B1YCJRYB-Byv5Y2l8.js → AllPackages-B1YCJRYB-Dq7RtcKf.js} +1 -1
- package/dist/assets/{BaseConfiguration-B6s_tKOW-D-gjoRXo.js → BaseConfiguration-B6s_tKOW-Cqnov2Sk.js} +1 -1
- package/dist/assets/{Element-BMI7Nj5w-pmadKN44.js → Element-BMI7Nj5w-BNxoLB0I.js} +1 -1
- package/dist/assets/{Entities-C8p_0jCn-DOze-aLz.js → Entities-C8p_0jCn-nTjTp-hn.js} +1 -1
- package/dist/assets/{FunctionList-YWk54TCy-ChT_85FS.js → FunctionList-YWk54TCy-BE08xiex.js} +1 -1
- package/dist/assets/{InputJax-C5dj-ntL-QDnGDgyA.js → InputJax-C5dj-ntL-R1Dc7w96.js} +1 -1
- package/dist/assets/{MathItem-YJvQL-XJ-C1WmSlqG.js → MathItem-YJvQL-XJ-CLANtgp6.js} +1 -1
- package/dist/assets/{MmlFactory-DSoKkE7s-BSods9g3.js → MmlFactory-DSoKkE7s-5yVDGib6.js} +1 -1
- package/dist/assets/{Options-oARQeeuT-BezHG2DV.js → Options-oARQeeuT-8pBPNhr8.js} +1 -1
- package/dist/assets/{OutputJax-DfUZNNCu-CiGZyFkj.js → OutputJax-DfUZNNCu-DGRVixhw.js} +1 -1
- package/dist/assets/{PrioritizedList-6lun1Xqd-1YFOP2UD.js → PrioritizedList-6lun1Xqd-DRdQwlR3.js} +1 -1
- package/dist/assets/{Styles-5NmwS-3M-CckuzfVu.js → Styles-5NmwS-3M-CqPSzBzv.js} +1 -1
- package/dist/assets/{TeXAtom-B5ni9myz-czpsshhI.js → TeXAtom-B5ni9myz-7xixu72b.js} +1 -1
- package/dist/assets/{abnfDiagram-VCTEODGH-BGhi-oVb-CxSD522K.js → abnfDiagram-VCTEODGH-BGhi-oVb-D9vo48Kc.js} +1 -1
- package/dist/assets/{arc-CK5ru50N-DKCl2r1T.js → arc-CK5ru50N-BlqR_Ske.js} +1 -1
- package/dist/assets/architecture-7GRP2DOG-otSkCkT5-ifTMCh6E.js +1 -0
- package/dist/assets/{architectureDiagram-5GKGNRK7-ELuPYMlf-Dopud1bd.js → architectureDiagram-5GKGNRK7-ELuPYMlf-CXbm8lLb.js} +1 -1
- package/dist/assets/{assistive-mml-CdU3rUpx-CBXDqBb7.js → assistive-mml-CdU3rUpx-CCKquCU4.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-BZ7-OGWQ-o5hOntDX.js → blockDiagram-I7D4REHJ-BZ7-OGWQ-B6ff9p2Y.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-DCovPZ_4-DmxXKUN2.js → c4Diagram-7LVT6UL2-DCovPZ_4-DNdoEnm0.js} +1 -1
- package/dist/assets/channel-Bmu_OOc6-DE5nBHdm.js +1 -0
- package/dist/assets/{chunk-4HAMMTFA-DqpzLmar-DiA8EWSJ.js → chunk-4HAMMTFA-DqpzLmar-ggP-4zbL.js} +1 -1
- package/dist/assets/{chunk-75Z2AOVW-CT5TYzp5-lldrjFxV.js → chunk-75Z2AOVW-CT5TYzp5-BAYR6Gyo.js} +1 -1
- package/dist/assets/{chunk-DU6HZSFF-BVf-rtdf-CwUL1NFa.js → chunk-DU6HZSFF-BVf-rtdf--dgDiPgV.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-rP8pUsk3-CzNXy7AU.js → chunk-F27PBJKO-rP8pUsk3-BUE2LzK8.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-D_IT0r3G-B9KUwB52.js → chunk-IMKFNOWR-D_IT0r3G-DEq-HDMy.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-C5YURnHe-BSqsqkgY.js → chunk-POPQ4Y6H-C5YURnHe-BJUZN0LR.js} +1 -1
- package/dist/assets/{chunk-SHT3W25Y-CfF0J8Qh-5fHXXkh7.js → chunk-SHT3W25Y-CfF0J8Qh-BLNIWgW0.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-CTZbRIbX-CWrs29Kz.js → chunk-SVP7TREG-CTZbRIbX-DzHNPM77.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-DP1lYmZm-DDcA7wX4.js → chunk-TICWLB2K-DP1lYmZm-hPrDbcf2.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-vg0kfC73-B9m3UCLq.js → chunk-XXDRQBXY-vg0kfC73-CA6dD2B2.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-BDQOAOBf-DxjUWGXR.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-CwPXtzoF-DxjUWGXR.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-qIiD3RGD-8iqUd8fN.js → cose-bilkent-JH36ORCC-qIiD3RGD-CgZnBXDi.js} +1 -1
- package/dist/assets/cynefin-OW5HDTMX-Xr6IPdZG-DIN74MBN.js +1 -0
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-BZrUCS-v-BjGdpO1b.js → cynefinDiagram-5FMLGOSQ-BZrUCS-v-lJIPhbyX.js} +1 -1
- package/dist/assets/{dagre-C2SZ8h4o-Bv8J8VUz.js → dagre-C2SZ8h4o-BgSfmUeF.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-CQcLM4na-BEUkvakq.js → dagre-GXQ25YYZ-CQcLM4na-mO2les8S.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-BdmcmUQh-C17Q-TTS.js → diagram-S7CK7UJ4-BdmcmUQh-DZZPbcxP.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-BtvmEiYl-sOGBhdh3.js → diagram-UQ7AKVKN-BtvmEiYl-CXPBFvdO.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-qEG6N0bV-D-F3WtOn.js → diagram-VSXAHHWV-qEG6N0bV-2P5xWeyN.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-BA8is49N-DDC2KnwX.js → diagram-VX7I27RA-BA8is49N-Ce8-XrQI.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-XPcU7IOQ-DnNwyEbY.js → diagram-Z3DM3KII-XPcU7IOQ-obu8Hmlg.js} +1 -1
- package/dist/assets/{dist-B_fklXwV.js → dist-BaZxeAEj.js} +1 -1
- package/dist/assets/{dist-BfgqBdfY.js → dist-DDtEYLX4.js} +1 -1
- package/dist/assets/{dist-DeiyD9Ki-CTmQI5BY.js → dist-DeiyD9Ki-B3Qn-Kh4.js} +1 -1
- package/dist/assets/{dist-B3WvX9Mu.js → dist-DoZPZkgo.js} +1 -1
- package/dist/assets/{dist-CqavRLcS.js → dist-DpShV5wo.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-CTHIHdBS-BUK0gvW3.js → ebnfDiagram-PWID7BFC-CTHIHdBS-oyDbKuK_.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-DVpWTNZj-WczcFdQ5.js → erDiagram-RLTQ6QDP-DVpWTNZj-DD5MfJxq.js} +1 -1
- package/dist/assets/eventmodeling-NTZA5JFV-B7bRrpt_-ByYnQ_YE.js +1 -0
- package/dist/assets/flowDiagram-HODETNUW-r-3T5g30-DVt4ibmB.js +1 -0
- package/dist/assets/{ganttDiagram-EL5Y4UJY-BfAgk15F-DTGMCrLj.js → ganttDiagram-EL5Y4UJY-BfAgk15F-EJ3zMpou.js} +1 -1
- package/dist/assets/gitGraph-4MIJSDKK-BhzSx5wq-De8cUs1t.js +1 -0
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-DEVVZ6wt-HdaEqyjq.js → gitGraphDiagram-WWUBYQGX-DEVVZ6wt-Dj16r719.js} +1 -1
- package/dist/assets/{graphlib-BYLhHyYO-DQGQ3Hua.js → graphlib-BYLhHyYO-BTZzAS5v.js} +1 -1
- package/dist/assets/{html-Z7dGbczU-CrSYt5SB.js → html-Z7dGbczU-DHXoUjco.js} +1 -1
- package/dist/assets/index-CziTLExz.css +1 -0
- package/dist/assets/{index-CFCwHCAC.js → index-DE93mXAl.js} +347 -345
- package/dist/assets/info-A6RAGUB7-fR2SUJmJ-DfOs01lb.js +1 -0
- package/dist/assets/{infoDiagram-27XIBGKW-CvHUhAHd-Bqc9FCJv.js → infoDiagram-27XIBGKW-CvHUhAHd-DQWRUxRY.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-CeqyKdwk-BTaokajf.js → ishikawaDiagram-5VMMS53U-CeqyKdwk-CaqBsvlx.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-Jfi7MPDi-DG9WVFfp.js → journeyDiagram-3NMN7TZE-Jfi7MPDi-D2E43LwV.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-yOrlojns-B5-MNvAZ.js → kanban-definition-UXKFOSKX-yOrlojns-CHfxXoOA.js} +1 -1
- package/dist/assets/{lengths-CvmPfBid-Ccc3uBwx.js → lengths-CvmPfBid-pbRzi4y_.js} +1 -1
- package/dist/assets/{linear-jdmEy-qs-BRxqk9pe.js → linear-jdmEy-qs-YK-i28xN.js} +1 -1
- package/dist/assets/{liteAdaptor-pH5MXTG0-D2DVKbJh.js → liteAdaptor-pH5MXTG0-3egTzN5I.js} +1 -1
- package/dist/assets/{mathjax-DVEo4UYj-z2lf4I_o.js → mathjax-DVEo4UYj-DJM3fB8M.js} +1 -1
- package/dist/assets/{mermaid-parser.core-CzBW8Fof-kR4f_D21.js → mermaid-parser.core-CzBW8Fof-BLYnlega.js} +2 -2
- package/dist/assets/{mermaid.core-Dv4uylek-Ddm4KdK9.js → mermaid.core-Dv4uylek-BEK0XLuu.js} +4 -4
- package/dist/assets/{mindmap-definition-YA3MSWOX-DP_2R9Ga-BamcFU30.js → mindmap-definition-YA3MSWOX-DP_2R9Ga-BCnJS2OL.js} +1 -1
- package/dist/assets/{mo-Xv_6CC7A-C4qQoXD7.js → mo-Xv_6CC7A-fsTVwGAJ.js} +1 -1
- package/dist/assets/packet-AYTQ26CC-B_oKR9KZ-CIr_QE41.js +1 -0
- package/dist/assets/{pegDiagram-XKGWAZYB-BsBWryhi-BPOLi_DS.js → pegDiagram-XKGWAZYB-BsBWryhi-DnlKhijv.js} +1 -1
- package/dist/assets/pie-WAS4IAKB-D1ZXrN1W-DtjU4bsm.js +1 -0
- package/dist/assets/{pieDiagram-E7YTZNPT-OugkS2pU-BxJPZt-m.js → pieDiagram-E7YTZNPT-OugkS2pU-BQ62uhNn.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-DXEAAaUE-CeLGIG_l.js → quadrantDiagram-AXDQQJYC-DXEAAaUE-DjfBVEH6.js} +1 -1
- package/dist/assets/radar-RG4KPBEZ-BmUgDkiH-CuKD9jQP.js +1 -0
- package/dist/assets/railroad-74A4TZTK-6lEXVJo8-DYg5FlhY.js +1 -0
- package/dist/assets/railroad-abnf-HS5TGJTU-BG28Qiz1-CmzKOp6N.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-DzpHhtTH-Ca4CUyup.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC-B2CHCTyG-DW1BPRzc.js +1 -0
- package/dist/assets/{railroadDiagram-O6MQD6OU-BJys8O96-CXO-wle8.js → railroadDiagram-O6MQD6OU-BJys8O96-DT83nUM3.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-BaDgCtM5-Dt4Cfr6O.js → requirementDiagram-BXWQKSXE-BaDgCtM5-Bk6OMrCS.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-puZsr_vK-CMC9fgwz.js → sankeyDiagram-P5KCCOFB-puZsr_vK-B3mb-QqF.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-D1ziEryC-DB5eVRRY.js → sequenceDiagram-WJ2MYXX4-D1ziEryC-DA35A244.js} +1 -1
- package/dist/assets/{src-CLC0ddSd-ByF0JTrb.js → src-CLC0ddSd-DXUH8o27.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-HSEurT6h-BwSqvh1q.js → stateDiagram-D77RDMKH-HSEurT6h-BZdzB2jB.js} +1 -1
- package/dist/assets/stateDiagram-v2-MP3YSRHH-DnOa-Otc-CJvYdnZ6.js +1 -0
- package/dist/assets/{svg-D_RYJvQC-BPJwglJJ.js → svg-D_RYJvQC-CymKx1MQ.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-umBsuvsI-BAnYSTii.js → swimlanes-42K2YHIH-umBsuvsI-CFo1naDR.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-ZLUyNmnU-BUVjUqff.js +8 -0
- package/dist/assets/{tex-Dx2UJ3VU-CCsHHGCD.js → tex-Dx2UJ3VU-CQTk8uUD.js} +1 -1
- package/dist/assets/{timeline-definition-24CTP7MA-D9plt22Y-BX4CiOEk.js → timeline-definition-24CTP7MA-D9plt22Y-BhW4PYii.js} +1 -1
- package/dist/assets/treeView-Q6P3EWNA-BdvZ0_C5-DXP8JL2N.js +1 -0
- package/dist/assets/treemap-WGGIJYW6-CIJLPQlC-DfNE0Ylm.js +1 -0
- package/dist/assets/{vennDiagram-4TSXK5OY-DZe7wY85-BRytU48n.js → vennDiagram-4TSXK5OY-DZe7wY85-GcaRUyDZ.js} +1 -1
- package/dist/assets/wardley-WFR3VGLG-CKqtFD8V-Bq9rf08k.js +1 -0
- package/dist/assets/{wardleyDiagram-VM6X3IG4-DK9yytWx-DVNY3c-A.js → wardleyDiagram-VM6X3IG4-DK9yytWx-D9owzIEw.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-CLSO4Pc9-XnrHm8Bf.js → xychartDiagram-S5SC5T6Z-CLSO4Pc9-B-vSQvUZ.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +10 -10
- package/server/backends/fileChange.ts +61 -5
- package/server/files/documentWatch.ts +212 -0
- package/server/files/file-search.ts +185 -0
- package/server/files/files-browse.ts +139 -0
- package/server/files/pathContainment.ts +41 -0
- package/server/git/worktrees.ts +35 -6
- package/server/index.ts +12 -1
- package/server/infra/pubsub.ts +18 -0
- package/server/routes/routeParams.ts +20 -4
- package/server/routes/session-routes.ts +6 -5
- package/server/session/session-cwd.ts +13 -1
- package/server/skills/mulmoterminal-keys/SKILL.md +9 -6
- package/dist/assets/architecture-7GRP2DOG-otSkCkT5-YabtMIbr.js +0 -1
- package/dist/assets/channel-Bmu_OOc6-LXxQltDk.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-BDQOAOBf-BUvThf0-.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-CwPXtzoF-BUvThf0-.js +0 -1
- package/dist/assets/cynefin-OW5HDTMX-Xr6IPdZG-BzdrGHCp.js +0 -1
- package/dist/assets/eventmodeling-NTZA5JFV-B7bRrpt_-izWjOGe8.js +0 -1
- package/dist/assets/flowDiagram-HODETNUW-r-3T5g30-D06E2Yur.js +0 -1
- package/dist/assets/gitGraph-4MIJSDKK-BhzSx5wq-HItyG1D5.js +0 -1
- package/dist/assets/index-ZQzSIgvD.css +0 -1
- package/dist/assets/info-A6RAGUB7-fR2SUJmJ-CCp3Pve-.js +0 -1
- package/dist/assets/packet-AYTQ26CC-B_oKR9KZ-P1h4UNRU.js +0 -1
- package/dist/assets/pie-WAS4IAKB-D1ZXrN1W-BejuFjHy.js +0 -1
- package/dist/assets/radar-RG4KPBEZ-BmUgDkiH-Wu_t-Az-.js +0 -1
- package/dist/assets/railroad-74A4TZTK-6lEXVJo8-DJe4dT-U.js +0 -1
- package/dist/assets/railroad-abnf-HS5TGJTU-BG28Qiz1-M9sfPaQt.js +0 -1
- package/dist/assets/railroad-ebnf-LZEXJU2U-DzpHhtTH-C1RD0jlr.js +0 -1
- package/dist/assets/railroad-peg-WCYAUIDC-B2CHCTyG-CsDICDaK.js +0 -1
- package/dist/assets/stateDiagram-v2-MP3YSRHH-DnOa-Otc-DgFYwvfo.js +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-ZLUyNmnU-CM3sLR6V.js +0 -8
- package/dist/assets/treeView-Q6P3EWNA-BdvZ0_C5-Ce03QaRO.js +0 -1
- package/dist/assets/treemap-WGGIJYW6-CIJLPQlC-sLYXmnbr.js +0 -1
- package/dist/assets/wardley-WFR3VGLG-CKqtFD8V-CR-W1Hkl.js +0 -1
package/README.md
CHANGED
|
@@ -1220,6 +1220,17 @@ tree; clicking a file opens it in a **CodeMirror** editor (Markdown / JS-TS / JS
|
|
|
1220
1220
|
highlighting, everything else as plain text). Markdown files get a **Preview** toggle
|
|
1221
1221
|
that renders via the server's sandboxed `…/md` HTML. **Save** (or ⌘/Ctrl-S) writes back.
|
|
1222
1222
|
|
|
1223
|
+
**The open view follows the file on disk.** When an agent in another cell — or any editor —
|
|
1224
|
+
rewrites what you are looking at, the editor and the preview catch up on their own; there is
|
|
1225
|
+
nothing to reload. If you have unsaved edits of your own, a banner asks which copy to keep
|
|
1226
|
+
rather than choosing for you.
|
|
1227
|
+
|
|
1228
|
+
The server watches a document while a view is open on it, and only under the workspace or under
|
|
1229
|
+
a directory one of your terminals is in — the same directories it serves files from. A Markdown
|
|
1230
|
+
file outside all of those still opens and still edits; it is picked up by the pane's periodic
|
|
1231
|
+
check rather than the moment it changes, and a Canvas card on it waits for the next thing that
|
|
1232
|
+
does announce.
|
|
1233
|
+
|
|
1223
1234
|
**Beside an enlarged terminal, not only full-screen.** Expand a grid cell (**⤢**) and its
|
|
1224
1235
|
header gains a **folder** toggle that splits the enlarged area in two: terminal on the left,
|
|
1225
1236
|
the same explorer + editor on the right, rooted at that cell's directory. Drag the divider
|
|
@@ -1228,6 +1239,27 @@ shrinks the pane rather than reflowing xterm into garbage. It works in both zoom
|
|
|
1228
1239
|
(cockpit roster and thumbnail filmstrip), the pane re-roots as you walk the zoom between
|
|
1229
1240
|
terminals, and whether it's open plus how wide it is are remembered per browser.
|
|
1230
1241
|
|
|
1242
|
+
**Coming back looks the way you left it.** The pane remembers the open file, the directories
|
|
1243
|
+
you had expanded, and — for a Markdown file — whether you were reading it in **Preview** or
|
|
1244
|
+
editing it: per cell while the session lasts, and per directory across a browser reload (the
|
|
1245
|
+
first pane to open on that directory claims it, so a second terminal in the same repository
|
|
1246
|
+
starts on its own empty tree). A remembered Preview only comes back over that same file while
|
|
1247
|
+
it is still Markdown the server can render; anything else opens in the editor.
|
|
1248
|
+
|
|
1249
|
+
It comes back to **where** you were, too: the line at the top of the editor, the line the caret was
|
|
1250
|
+
on, and how far down the tree was scrolled. The top line matters on its own — scrolling moves
|
|
1251
|
+
neither the selection nor the caret, so someone reading without clicking would otherwise come back
|
|
1252
|
+
to the top of a file they were in the middle of. The caret is kept as a line rather than a scroll offset, so it survives
|
|
1253
|
+
the pane being a different width next time, and a line past the end of a file that has since been
|
|
1254
|
+
edited lands on the nearest real one. The Markdown preview's own scroll position is not remembered
|
|
1255
|
+
— its iframe is sandboxed to the point where the app cannot read it.
|
|
1256
|
+
|
|
1257
|
+
**It also paints before it has finished reading.** The last listing of each directory is kept, so
|
|
1258
|
+
opening the pane on one you have been to before shows that tree at once and swaps in what the
|
|
1259
|
+
server says when it arrives — including any directory you expanded while waiting. A directory the
|
|
1260
|
+
pane has not read before says so (`Loading…`) rather than claiming to be empty, and a read that
|
|
1261
|
+
fails shows the error rather than a tree it can no longer vouch for.
|
|
1262
|
+
|
|
1231
1263
|
The toggle is not the only way in: while a cell is enlarged, **clicking a file path the agent
|
|
1232
1264
|
printed** opens it here too, rather than in a new tab or full-screen — see
|
|
1233
1265
|
[Clicking a file path](#clicking-a-file-path).
|
|
@@ -1244,6 +1276,21 @@ than letting you believe an ignore file was applied. There is also a
|
|
|
1244
1276
|
[`files-find` shortcut](https://receptron.github.io/mulmoterminal/guide/en/config.html#keymap)
|
|
1245
1277
|
with no default binding, which opens the pane first if it is closed.
|
|
1246
1278
|
|
|
1279
|
+
A second button searches **inside** the files. Matches are grouped by file with the matching
|
|
1280
|
+
lines under them, and picking one opens the file *and* puts the cursor on that line. Queries are
|
|
1281
|
+
literal unless the regex toggle is on, and case is smart — a lower-case query matches either case.
|
|
1282
|
+
It is `git grep` underneath, so the same `.gitignore` applies, files your agent created seconds ago
|
|
1283
|
+
are searched, and — because the default reads the **working tree** rather than the index — an
|
|
1284
|
+
edited-but-unstaged file is searched as it is on disk. The one file no on-disk search can read is
|
|
1285
|
+
the one you have open with unsaved edits. In the default literal mode that one is searched in the
|
|
1286
|
+
browser from the buffer and marked `unsaved`, so its line numbers are the ones on your screen. In
|
|
1287
|
+
**regex mode it is not searched at all** — running a pattern you are still typing on the thread that
|
|
1288
|
+
draws the UI can freeze the tab — so the file is left out and the panel says to save it. Either way
|
|
1289
|
+
its stale on-disk matches are dropped, because those line numbers describe a file you are not
|
|
1290
|
+
looking at. The
|
|
1291
|
+
[`files-search` shortcut](https://receptron.github.io/mulmoterminal/guide/en/config.html#keymap)
|
|
1292
|
+
is likewise unbound by default.
|
|
1293
|
+
|
|
1247
1294
|
All reads and writes go through `GET/PUT /api/files/browse/*?cwd=&path=`, and every
|
|
1248
1295
|
`path` is **contained within the project root** (server-side) — `..`/absolute escapes
|
|
1249
1296
|
are rejected for reads and writes alike, so editing can't reach outside the directory
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The "this file changed" channel name.
|
|
2
|
+
//
|
|
3
|
+
// Both sides decide it: the server mints it when a file is written or seen to change, and the
|
|
4
|
+
// UI subscribes to it for the document it has open. That makes it a wire shape rather than a
|
|
5
|
+
// server detail, so it lives here.
|
|
6
|
+
//
|
|
7
|
+
// The format is @mulmoclaude/core/file-change's (`pluginFileChannel`), which the UI cannot
|
|
8
|
+
// import — that entry pulls in node:fs. A spec pins this copy to core's rather than trusting
|
|
9
|
+
// the two to stay the same by inspection.
|
|
10
|
+
|
|
11
|
+
const CHANNEL_PREFIX = "plugin:";
|
|
12
|
+
const PATH_MARKER = ":file:";
|
|
13
|
+
|
|
14
|
+
/** The scope the markdown plugin's View and the server's markdown matcher agree on. */
|
|
15
|
+
export const MARKDOWN_FILE_SCOPE = "markdown";
|
|
16
|
+
|
|
17
|
+
/** The channel one file is announced on, under one plugin scope. */
|
|
18
|
+
export function pluginFileChannel(scope: string, posixPath: string): string {
|
|
19
|
+
return `${CHANNEL_PREFIX}${scope}${PATH_MARKER}${posixPath}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The scope and path a channel names, or null when it is not one of ours.
|
|
23
|
+
*
|
|
24
|
+
* Split on the FIRST `:file:` only: a POSIX filename may itself contain a colon, so
|
|
25
|
+
* everything after that marker is the path verbatim and never re-parsed. */
|
|
26
|
+
export function parsePluginFileChannel(channel: string): { scope: string; path: string } | null {
|
|
27
|
+
if (!channel.startsWith(CHANNEL_PREFIX)) return null;
|
|
28
|
+
const rest = channel.slice(CHANNEL_PREFIX.length);
|
|
29
|
+
const marker = rest.indexOf(PATH_MARKER);
|
|
30
|
+
if (marker <= 0) return null;
|
|
31
|
+
const path = rest.slice(marker + PATH_MARKER.length);
|
|
32
|
+
return path ? { scope: rest.slice(0, marker), path } : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A path spelled the way the channel spells it.
|
|
36
|
+
*
|
|
37
|
+
* The publisher normalises to POSIX before it mints a channel, so a subscriber that keeps
|
|
38
|
+
* Windows separators names a channel nothing is ever published on — and nothing errors,
|
|
39
|
+
* which is why this is a function both sides call rather than a rule both sides remember. */
|
|
40
|
+
export function fileChannelPath(filePath: string): string {
|
|
41
|
+
return filePath.split("\\").join("/");
|
|
42
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// What a content search IS, for the two sides that decide it together (#2140).
|
|
2
|
+
//
|
|
3
|
+
// In `common/` because BOTH ends read these rules, not because they happen to share a shape: the
|
|
4
|
+
// server builds git's argv from them, and the browser applies the same literal and case rules to
|
|
5
|
+
// the one file open in the editor with unsaved edits — which no on-disk search can see. If the two
|
|
6
|
+
// drifted, that one file would answer to a different notion of "case-insensitive" than every other
|
|
7
|
+
// file in the list.
|
|
8
|
+
//
|
|
9
|
+
// LITERAL MODE ONLY. In regex mode the browser does not search that buffer at all: the pattern
|
|
10
|
+
// comes from the query, and running one on the thread that draws the UI can hang the tab (the
|
|
11
|
+
// measurement is on `literalMatch`). The file is dropped and the panel says so.
|
|
12
|
+
//
|
|
13
|
+
// The git-specific half — argv, output parsing, the exit codes — stays in
|
|
14
|
+
// `server/files/file-search.ts`, where nothing in the browser has any use for it.
|
|
15
|
+
import { byCodeUnit } from "./byCodeUnit.js";
|
|
16
|
+
|
|
17
|
+
/** How many matching LINES are returned across the whole search. */
|
|
18
|
+
export const MAX_SEARCH_MATCHES = 500;
|
|
19
|
+
|
|
20
|
+
/** How many matching lines any ONE file may contribute. Without it a single generated file answers
|
|
21
|
+
* the whole budget and the result reads as "only this file matches". */
|
|
22
|
+
export const MAX_MATCHES_PER_FILE = 20;
|
|
23
|
+
|
|
24
|
+
/** How much of a matching line is carried. A minified bundle is one line of a hundred thousand
|
|
25
|
+
* characters, and shipping it whole would cost more than every other result together. */
|
|
26
|
+
export const MAX_SNIPPET_CHARS = 400;
|
|
27
|
+
|
|
28
|
+
/** Shorter than the shared git timeout: this runs while the user is typing. A repository too slow
|
|
29
|
+
* to answer inside it gives an empty result that says it is incomplete, not a hung panel. */
|
|
30
|
+
export const SEARCH_TIMEOUT_MS = 10_000;
|
|
31
|
+
|
|
32
|
+
export interface SearchRequest {
|
|
33
|
+
query: string;
|
|
34
|
+
/** Treat the query as a regular expression rather than as literal characters. */
|
|
35
|
+
regex: boolean;
|
|
36
|
+
/** Force case sensitivity. Absent means SMART CASE — see `wantsCaseSensitive`. */
|
|
37
|
+
caseSensitive?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One matching line. `line` is 1-based, as git reports it and as an editor counts. */
|
|
41
|
+
export interface SearchMatch {
|
|
42
|
+
/** Relative to the searched directory, `/`-separated — the same spelling `/api/files/browse/*`
|
|
43
|
+
* already speaks, so a result can be opened without translation. */
|
|
44
|
+
path: string;
|
|
45
|
+
line: number;
|
|
46
|
+
/** The matching line, cut to MAX_SNIPPET_CHARS. */
|
|
47
|
+
text: string;
|
|
48
|
+
/** The line was longer than the cap and `text` is its beginning. */
|
|
49
|
+
clipped: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SearchResult {
|
|
53
|
+
matches: SearchMatch[];
|
|
54
|
+
/** Some matches are NOT here — a cap cut the list, or git stopped early. The one wrong answer a
|
|
55
|
+
* search can give is "it is not there" when it only means "I did not look at all of it", so this
|
|
56
|
+
* is reported rather than inferred from the array's length (which cannot show a per-file cut). */
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
/** `git` answered from a repository, so `.gitignore` was applied. `no-index` means it did not —
|
|
59
|
+
* there is no ignore file to apply outside a repository, and the UI says so rather than letting
|
|
60
|
+
* the reader assume one was read. */
|
|
61
|
+
source: "git" | "no-index";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Smart case: a query typed entirely in lower case matches either case, and one carrying an
|
|
65
|
+
* upper-case letter is taken literally. It is what every comparable tool does, which is the whole
|
|
66
|
+
* argument — a hand reaches for it without being taught.
|
|
67
|
+
*
|
|
68
|
+
* Decided on the query and NOT on a locale-aware fold: `toLowerCase()` on a Turkish `I` produces a
|
|
69
|
+
* dotless `ı` under some locales, so the comparison is against the code points as typed. */
|
|
70
|
+
export function wantsCaseSensitive(request: SearchRequest): boolean {
|
|
71
|
+
if (request.caseSensitive !== undefined) return request.caseSensitive;
|
|
72
|
+
return [...request.query].some((ch) => ch !== ch.toLowerCase() && ch === ch.toUpperCase());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A query with nothing to search for. Rejected rather than run: an empty pattern matches every
|
|
76
|
+
* line of every file, which is a slow way to answer nothing useful. Whitespace alone IS a real
|
|
77
|
+
* query — someone looking for a tab or trailing space means it — so only the empty string counts. */
|
|
78
|
+
export const isSearchable = (query: string): boolean => query.length > 0;
|
|
79
|
+
|
|
80
|
+
/** Matches grouped by file, files in path order and lines in file order — the shape the panel
|
|
81
|
+
* renders, decided here so the ordering is one rule rather than one per view.
|
|
82
|
+
*
|
|
83
|
+
* `byCodeUnit` rather than `localeCompare` for the reason the file list already uses it: a locale
|
|
84
|
+
* comparison reorders the same project differently on two machines. */
|
|
85
|
+
export function groupByFile(matches: SearchMatch[]): { path: string; matches: SearchMatch[] }[] {
|
|
86
|
+
const byPath = new Map<string, SearchMatch[]>();
|
|
87
|
+
matches.forEach((match) => {
|
|
88
|
+
const existing = byPath.get(match.path);
|
|
89
|
+
if (existing) existing.push(match);
|
|
90
|
+
else byPath.set(match.path, [match]);
|
|
91
|
+
});
|
|
92
|
+
return [...byPath.keys()].sort(byCodeUnit).map((path) => ({ path, matches: (byPath.get(path) ?? []).sort((a, b) => a.line - b.line) }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether a LITERAL query matches a line, under the same case rule the server applies.
|
|
96
|
+
*
|
|
97
|
+
* A plain string search, deliberately — not a RegExp built from the query. `(a+)+b` against a
|
|
98
|
+
* THIRTY-TWO character line takes the better part of a minute in a JS engine, so a regex built
|
|
99
|
+
* from untrusted text cannot run on the thread that draws the UI: the tab would freeze, taking the
|
|
100
|
+
* unsaved buffer this whole mechanism exists to respect with it.
|
|
101
|
+
*
|
|
102
|
+
* Literal is the default mode and needs no regex to begin with, which is what makes the default
|
|
103
|
+
* path both correct and unable to hang. Regex mode does not come here at all — see
|
|
104
|
+
* `withBufferMatches`. */
|
|
105
|
+
function literalMatch(line: string, request: SearchRequest): boolean {
|
|
106
|
+
if (wantsCaseSensitive(request)) return line.includes(request.query);
|
|
107
|
+
return line.toLowerCase().includes(request.query.toLowerCase());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The matching lines of one in-memory buffer, in the shape the disk search returns.
|
|
111
|
+
*
|
|
112
|
+
* This exists because an unsaved buffer is invisible to ANY on-disk search — git, ripgrep or a
|
|
113
|
+
* hand-rolled scan alike — and the pane can have one. It is affordable precisely because there is
|
|
114
|
+
* at most one: `FilesPane` opens a single file, so this is never a scan of a project.
|
|
115
|
+
*
|
|
116
|
+
* LITERAL QUERIES ONLY. The caller decides that; see `withBufferMatches`. */
|
|
117
|
+
export function matchesInBuffer(path: string, text: string, request: SearchRequest): SearchMatch[] {
|
|
118
|
+
const out: SearchMatch[] = [];
|
|
119
|
+
text.split("\n").forEach((raw, index) => {
|
|
120
|
+
if (out.length >= MAX_MATCHES_PER_FILE) return;
|
|
121
|
+
const line = raw.replace(/\r$/, "");
|
|
122
|
+
if (!literalMatch(line, request)) return;
|
|
123
|
+
out.push({ path, line: index + 1, text: line.slice(0, MAX_SNIPPET_CHARS), clipped: line.length > MAX_SNIPPET_CHARS });
|
|
124
|
+
});
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** What the panel shows, once the open buffer has had its say. */
|
|
129
|
+
export interface BufferMerge {
|
|
130
|
+
matches: SearchMatch[];
|
|
131
|
+
/** The open buffer was NOT searched, so a match in it is not in this list. True only in regex
|
|
132
|
+
* mode — see `withBufferMatches`. The panel has to SAY this: silence would read as "there is
|
|
133
|
+
* nothing in that file", which is the one wrong answer a search can give. */
|
|
134
|
+
bufferUnsearched: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Disk matches with the open buffer's own answer substituted for it.
|
|
139
|
+
*
|
|
140
|
+
* The trap this closes is the INVERSE of the obvious one. Missing a match in an unsaved file is a
|
|
141
|
+
* loud failure — nothing appears. But a match found ON DISK in a file that is open and edited is a
|
|
142
|
+
* QUIET one: the line number and the snippet describe the saved text, so the snippet shows
|
|
143
|
+
* something not on screen and jumping lands in the wrong place.
|
|
144
|
+
*
|
|
145
|
+
* So the file's disk matches are dropped entirely rather than merged: the two describe different
|
|
146
|
+
* documents and there is no correspondence between their line numbers. **That half holds in BOTH
|
|
147
|
+
* modes** — it needs no matching at all, only the knowledge that the file has diverged.
|
|
148
|
+
*
|
|
149
|
+
* What regex mode loses is the second half: the buffer is not searched, because doing so means
|
|
150
|
+
* running an untrusted pattern on the UI thread (see `literalMatch`). The file is dropped and the
|
|
151
|
+
* panel says so, which is the safe direction — a note the reader can act on by saving the file,
|
|
152
|
+
* rather than a frozen tab or a line number pointing at the wrong place.
|
|
153
|
+
*/
|
|
154
|
+
export function withBufferMatches(diskMatches: SearchMatch[], buffer: { path: string; text: string } | null, request: SearchRequest): BufferMerge {
|
|
155
|
+
if (!buffer) return { matches: diskMatches, bufferUnsearched: false };
|
|
156
|
+
const withoutStale = diskMatches.filter((match) => match.path !== buffer.path);
|
|
157
|
+
if (request.regex) return { matches: withoutStale, bufferUnsearched: true };
|
|
158
|
+
return { matches: [...withoutStale, ...matchesInBuffer(buffer.path, buffer.text, request)], bufferUnsearched: false };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** How many lines above and below a selected match the panel shows. Small on purpose: the block
|
|
162
|
+
* replaces a one-line row, so every extra line pushes the rest of the list further down. */
|
|
163
|
+
export const CONTEXT_RADIUS_LINES = 2;
|
|
164
|
+
|
|
165
|
+
/** One line of a context window. Carries `clipped` for the reason `SearchMatch` does — a silently
|
|
166
|
+
* cut line is indistinguishable from a short one, and a minified file's single line is enormous. */
|
|
167
|
+
export interface WindowLine {
|
|
168
|
+
text: string;
|
|
169
|
+
clipped: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The lines around one line of a file. */
|
|
173
|
+
export interface LineWindow {
|
|
174
|
+
/** 1-based line number of `lines[0]`. Meaningless when `lines` is empty. */
|
|
175
|
+
from: number;
|
|
176
|
+
lines: WindowLine[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The lines around `around` (1-based), clamped to the text.
|
|
181
|
+
*
|
|
182
|
+
* In `common/` because BOTH sides run it: the server over a file it read from disk, the browser
|
|
183
|
+
* over the buffer being edited — whose surroundings no on-disk read can produce. One rule rather
|
|
184
|
+
* than two, so the file on screen does not get a different notion of "the lines around this one"
|
|
185
|
+
* than every other file in the list.
|
|
186
|
+
*
|
|
187
|
+
* `around` past the end gives an EMPTY window rather than the tail of the file. The search answered
|
|
188
|
+
* before this read, so a file that shrank in between has no such line — and the tail would be
|
|
189
|
+
* numbered correctly while still being wrong: it would surround a match that is no longer there
|
|
190
|
+
* with a neighbourhood that looks real. Nothing at all says what is true, which is that the line
|
|
191
|
+
* the caller asked about is gone.
|
|
192
|
+
*
|
|
193
|
+
* The clause earns its place only at the BOUNDARY — one line past the end, where `to < from` has
|
|
194
|
+
* not yet become true on its own. Anything further out is already empty without it.
|
|
195
|
+
*/
|
|
196
|
+
export function lineWindow(text: string, around: number, radius: number): LineWindow {
|
|
197
|
+
const split = text.split("\n");
|
|
198
|
+
// A file ending in a newline splits into a trailing "" that is not a line — git does not count
|
|
199
|
+
// it and neither does an editor. Only ONE is dropped: "a\n\n" really does have a blank line 2.
|
|
200
|
+
const all = split.length > 1 && split[split.length - 1] === "" ? split.slice(0, -1) : split;
|
|
201
|
+
const from = Math.max(1, around - radius);
|
|
202
|
+
const to = Math.min(all.length, around + radius);
|
|
203
|
+
if (around > all.length || to < from) return { from, lines: [] };
|
|
204
|
+
return {
|
|
205
|
+
from,
|
|
206
|
+
lines: all.slice(from - 1, to).map((raw) => {
|
|
207
|
+
const line = raw.replace(/\r$/, "");
|
|
208
|
+
return { text: line.slice(0, MAX_SNIPPET_CHARS), clipped: line.length > MAX_SNIPPET_CHARS };
|
|
209
|
+
}),
|
|
210
|
+
};
|
|
211
|
+
}
|
package/common/keymap.ts
CHANGED
|
@@ -25,6 +25,7 @@ export const KEYMAP_ACTIONS = [
|
|
|
25
25
|
"terminal-close",
|
|
26
26
|
"terminal-restart",
|
|
27
27
|
"files-find",
|
|
28
|
+
"files-search",
|
|
28
29
|
"copy",
|
|
29
30
|
"paste",
|
|
30
31
|
] as const;
|
|
@@ -52,9 +53,9 @@ export const TERMINAL_SCOPED_ACTIONS: readonly KeymapAction[] = ["copy", "paste"
|
|
|
52
53
|
// Here rather than beside gridShortcutFor because BOTH sides decide from it: the grid dispatches on
|
|
53
54
|
// it, and validateKeymap has to know that these decline the key — the handler returns WITHOUT
|
|
54
55
|
// stopping the event, so a `send` on the same keystroke fires instead (codex on #1906).
|
|
55
|
-
// `files-find`
|
|
56
|
-
// docs/grid-view-modes.md) — a tiled grid has nowhere to put it, so the key declines
|
|
57
|
-
// guessing which of nine terminals was meant.
|
|
56
|
+
// `files-find` and `files-search` are here because the pane they open exists only in the ENLARGED
|
|
57
|
+
// row (see docs/grid-view-modes.md) — a tiled grid has nowhere to put it, so the key declines
|
|
58
|
+
// rather than guessing which of nine terminals was meant.
|
|
58
59
|
export const NEEDS_A_CURRENT_TERMINAL: readonly KeymapAction[] = [
|
|
59
60
|
"zoom-next",
|
|
60
61
|
"zoom-prev",
|
|
@@ -62,6 +63,7 @@ export const NEEDS_A_CURRENT_TERMINAL: readonly KeymapAction[] = [
|
|
|
62
63
|
"terminal-close",
|
|
63
64
|
"terminal-restart",
|
|
64
65
|
"files-find",
|
|
66
|
+
"files-search",
|
|
65
67
|
];
|
|
66
68
|
|
|
67
69
|
// The mirror of the list above: actions that walk the TILED grid, and so need nothing enlarged.
|