@cardstack/boxel-cli 0.11.0-unstable.1 → 0.11.0-unstable.3
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/bundled-realms/pretui/components/agent-mark.gts +98 -0
- package/bundled-realms/pretui/components/agent-question.gts +340 -37
- package/bundled-realms/pretui/components/answer-footer.gts +249 -0
- package/bundled-realms/pretui/components/card-embed.gts +523 -0
- package/bundled-realms/pretui/components/card-pills.gts +725 -0
- package/bundled-realms/pretui/components/code-block.gts +87 -45
- package/bundled-realms/pretui/components/collapsed-turn.gts +133 -12
- package/bundled-realms/pretui/components/compaction-chip.gts +81 -67
- package/bundled-realms/pretui/components/composer-cap.gts +516 -0
- package/bundled-realms/pretui/components/context-ring.gts +108 -0
- package/bundled-realms/pretui/components/diff-block.gts +1002 -31
- package/bundled-realms/pretui/components/diff-stat.gts +102 -0
- package/bundled-realms/pretui/components/jump-chip.gts +86 -53
- package/bundled-realms/pretui/components/markdown-stream.gts +799 -0
- package/bundled-realms/pretui/components/markdown-surface.gts +71 -0
- package/bundled-realms/pretui/components/plan-mark.gts +147 -0
- package/bundled-realms/pretui/components/prose.gts +185 -8
- package/bundled-realms/pretui/components/prose.md +6 -8
- package/bundled-realms/pretui/components/quick-look.gts +377 -0
- package/bundled-realms/pretui/components/reactions.gts +136 -0
- package/bundled-realms/pretui/components/recommendation.gts +67 -36
- package/bundled-realms/pretui/components/result-card.gts +58 -22
- package/bundled-realms/pretui/components/segmented-control.gts +13 -8
- package/bundled-realms/pretui/components/session-prep.gts +12 -11
- package/bundled-realms/pretui/components/sliding-highlight.gts +8 -3
- package/bundled-realms/pretui/components/speaker-label.gts +132 -0
- package/bundled-realms/pretui/components/state-glyph.gts +121 -0
- package/bundled-realms/pretui/components/step-list.gts +62 -60
- package/bundled-realms/pretui/components/stick-to-bottom.gts +1150 -0
- package/bundled-realms/pretui/components/system-line.gts +143 -0
- package/bundled-realms/pretui/components/task-checklist.gts +184 -0
- package/bundled-realms/pretui/components/thinking.gts +63 -94
- package/bundled-realms/pretui/components/tool-change-table.gts +285 -0
- package/bundled-realms/pretui/components/tool-gate.gts +462 -0
- package/bundled-realms/pretui/components/tool-group.gts +920 -0
- package/bundled-realms/pretui/components/tool-icon.ts +64 -0
- package/bundled-realms/pretui/components/tool-meta.gts +82 -0
- package/bundled-realms/pretui/components/tool-plan.gts +197 -0
- package/bundled-realms/pretui/components/tool-row.gts +2087 -0
- package/bundled-realms/pretui/components/turn-changes.gts +227 -0
- package/bundled-realms/pretui/components/user-bubble.gts +575 -0
- package/bundled-realms/pretui/components/work-item.gts +185 -48
- package/bundled-realms/pretui/docs/scroll-anchoring-protocol.md +136 -0
- package/bundled-realms/pretui/internal/markdown-projection.ts +438 -0
- package/bundled-realms/pretui/internal/mount-queue.ts +79 -0
- package/bundled-realms/pretui/internal/tool-phrases.ts +168 -0
- package/bundled-realms/pretui/package.json +2 -1
- package/bundled-test-harness/assets/{ai-module-creation-test-beRNwH-e.js → ai-module-creation-test-B6FQKqIl.js} +1 -1
- package/bundled-test-harness/assets/{app-QAuw0l8v.js → app-KpPqdyX3.js} +4 -4
- package/bundled-test-harness/assets/{binary-upload-dedupe-test-bI8T0QLl.js → binary-upload-dedupe-test-DN00y1tM.js} +1 -1
- package/bundled-test-harness/assets/{can-invoke-test-DVaU_yRT.js → can-invoke-test-Bl-EUFM3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-paging-test-DnxMnXKR.js → card-chooser-paging-test-BNuZRHX3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-test-DUSnLr-u.js → card-chooser-test-wKfPOOT4.js} +1 -1
- package/bundled-test-harness/assets/{card-def-field-def-relationships-test-GBUr3tFK.js → card-def-field-def-relationships-test-CduQ6zan.js} +1 -1
- package/bundled-test-harness/assets/{card-delete-test-CQveO1VC.js → card-delete-test-DKxolj5o.js} +1 -1
- package/bundled-test-harness/assets/{check-correctness-test-D2M2p66N.js → check-correctness-test-B-kgHztp.js} +1 -1
- package/bundled-test-harness/assets/choreo-BEHW-NOc.js +2 -0
- package/bundled-test-harness/assets/{choreo-CJfBb_EY.js → choreo-C2cpCfhA.js} +164 -13
- package/bundled-test-harness/assets/{choreo-feature-reel-test-D2OrygZv.js → choreo-feature-reel-test-CPI_6nkT.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-w07xqtO-.js → choreo-gallery-BxA9cyzF.js} +3 -3
- package/bundled-test-harness/assets/{choreo-gallery-build-order-test-yQg4JER6.js → choreo-gallery-build-order-test-CLVoseAJ.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-compositor-test-C2THCXJN.js → choreo-gallery-compositor-test-BpT5Ia6B.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-cover-test-D4T_Tk4s.js → choreo-gallery-cover-test-DL_fdSsh.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-crossing-stage-test-tHUPJ-Hy.js → choreo-gallery-crossing-stage-test-BcKHhpOK.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-demos-alive-test-BOtBOnEZ.js → choreo-gallery-demos-alive-test-KufLhXG8.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-drift-test-D_6U1Z2c.js → choreo-gallery-drift-test-Cx58rYIA.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-escort-test-DDBU0n4C.js → choreo-gallery-escort-test-Wz-emmID.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-films-test-CgW1NSdr.js → choreo-gallery-films-test-BFrVs5WW.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-filter-test-DUIsNwdO.js → choreo-gallery-filter-test-DCKQkhwe.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-fold-test-Chzlxvdr.js → choreo-gallery-fold-test-UtpDgpG9.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-guide-pages-test-DOWnqALT.js → choreo-gallery-guide-pages-test-rXDU9JLM.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-guides-test-Dra2J4LJ.js → choreo-gallery-guides-test-DfVFdIuU.js} +3 -3
- package/bundled-test-harness/assets/{choreo-gallery-hang-test-zzn1eiB-.js → choreo-gallery-hang-test-Cj7MbkFf.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-highlight-test-CW99hncQ.js → choreo-gallery-highlight-test-D6CF3IYZ.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-inline-edit-test-CYFm1kUz.js → choreo-gallery-inline-edit-test-LfHWsbqc.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-interruption-test-CYIBrKV9.js → choreo-gallery-interruption-test-CvQQAgTp.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-lone-stack-test-DL8R3SG7.js → choreo-gallery-lone-stack-test-D7Zci7fm.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-navigation-test-Dcv04kDx.js → choreo-gallery-navigation-test-DB1EnBEW.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-playhead-transport-test-Cfz7GsXl.js → choreo-gallery-playhead-transport-test-CtBlc6AJ.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-poplayout-subtree-test-CwoklBkW.js → choreo-gallery-poplayout-subtree-test-BQbmjsvI.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-presence-modes-test-BDCIZzJ0.js → choreo-gallery-presence-modes-test-CIdXVG1K.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-presentation-test-DlyiHwFf.js → choreo-gallery-presentation-test-C_rPFp29.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-reduced-motion-test-DVz5SKNR.js → choreo-gallery-reduced-motion-test-CH5w8Lw-.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-sheet-standalone-test-CpD_-BvS.js → choreo-gallery-sheet-standalone-test-CDgI4mGX.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-sheet-test-BjvAKHDL.js → choreo-gallery-sheet-test-5lsFX7Rk.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-site-test-oq_7y_PN.js → choreo-gallery-site-test-DbMa2xeC.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-stage-Bzr4_8Xd.js → choreo-gallery-stage-b3Sk86hp.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-stage-keyboard-test-a83d2euJ.js → choreo-gallery-stage-keyboard-test-ds6fT5Eg.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-stages-test-C9Sl1kH6.js → choreo-gallery-stages-test-ahioKCct.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-subdivision-test-BVVnkOwk.js → choreo-gallery-subdivision-test-g7anTDhT.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-table-plan-test-B0YdYEA0.js → choreo-gallery-table-plan-test-Bozc4uUf.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-theater-test-BLA147xw.js → choreo-gallery-theater-test-kE535d-b.js} +3 -3
- package/bundled-test-harness/assets/{choreo-gallery-tuning-test-DSXiNzCh.js → choreo-gallery-tuning-test-6rHkQBnZ.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-widget-room-test-BI574PdO.js → choreo-gallery-widget-room-test-0Vq-uTo1.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-wires-test-B6Mz2WZl.js → choreo-gallery-wires-test-CdtnAWYO.js} +1 -1
- package/bundled-test-harness/assets/clip-BLfuHi8g.js +2 -0
- package/bundled-test-harness/assets/{clip-C_pXjVvY.js → clip-q1QE5f73.js} +1 -1
- package/bundled-test-harness/assets/{codeblocks-test-AdolhdCA.js → codeblocks-test-BDD7qK_V.js} +1 -1
- package/bundled-test-harness/assets/{compaction-test-DvyAQq56.js → compaction-test-BweolkhG.js} +1 -1
- package/bundled-test-harness/assets/{container-DIF_9lZ9.css → container-B5B6u-xm.css} +29 -22
- package/bundled-test-harness/assets/{container-DvvqmNpg.js → container-BnJjIM6J.js} +9 -6
- package/bundled-test-harness/assets/{create-listing-modal-test-D95JfQab.js → create-listing-modal-test-FvcAKpIa.js} +1 -1
- package/bundled-test-harness/assets/{debug-message-test-BcNtj7Wr.js → debug-message-test-CJ18Evt7.js} +1 -1
- package/bundled-test-harness/assets/{externals-6ZWftYIb.js → externals-BWjhmCD4.js} +12 -12
- package/bundled-test-harness/assets/{fallback-banner-live-delete-test-5lnN1pv2.js → fallback-banner-live-delete-test-C2H3BrT-.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-test-DFb_m_Kq.js → fallback-banner-test-C10aYjkj.js} +1 -1
- package/bundled-test-harness/assets/{fallback-models-test-p3YIwj1L.js → fallback-models-test-BxoPiaGF.js} +1 -1
- package/bundled-test-harness/assets/{file-attachment-test-CmyjNWRR.js → file-attachment-test-DeKyzYaz.js} +1 -1
- package/bundled-test-harness/assets/film-B68PjRlr.js +2 -0
- package/bundled-test-harness/assets/{film-D7vtJO5q.js → film-CeUpnHN_.js} +6 -6
- package/bundled-test-harness/assets/{film-DN0A8JHx.js → film-DBZqyqW4.js} +6 -6
- package/bundled-test-harness/assets/{general-test-CX-TFDuX.js → general-test-BewzrpoB.js} +1 -1
- package/bundled-test-harness/assets/{glimmer-motion-and-choreo-in-cards-test-BHte2t9G.js → glimmer-motion-and-choreo-in-cards-test-HA-INN3d.js} +14 -14
- package/bundled-test-harness/assets/{glimmer-motion-and-choreo-test-BPNVeIio.js → glimmer-motion-and-choreo-test-BZ0ZuuAw.js} +3 -3
- package/bundled-test-harness/assets/{index-query-engine-test-Dz7Q4Xmk.js → index-query-engine-test-BtI8Y2LR.js} +1 -1
- package/bundled-test-harness/assets/{instance-filter-matcher-test-BoLU3bVg.js → instance-filter-matcher-test-BHXHGHjE.js} +1 -1
- package/bundled-test-harness/assets/{main-Dlgp8c7v.js → main-CqAHZ2kK.js} +1 -1
- package/bundled-test-harness/assets/{message-tool-test-CfjRSJvt.js → message-tool-test-NMaPJFsd.js} +1 -1
- package/bundled-test-harness/assets/{model-cost-tier-test-mZG84Uso.js → model-cost-tier-test-BgbfN2LH.js} +1 -1
- package/bundled-test-harness/assets/{operation-lowering-test-B0KpI1xu.js → operation-lowering-test-C4IaOjsA.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-card-chooser-test-KWS8lq16.js → operator-mode-card-chooser-test-Bwe47Rn7.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-links-test-CViFt0Ws.js → operator-mode-links-test-BU4jkC-B.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-test-D0gZM2yw.js → operator-mode-test-Bh0rGM9z.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-ui-test-CEvHJ9DS.js → operator-mode-ui-test-whgUw0ju.js} +1 -1
- package/bundled-test-harness/assets/{overlay-menu-items-test-Pw80RXsr.js → overlay-menu-items-test-Cwn1vr8q.js} +1 -1
- package/bundled-test-harness/assets/{overlays-BnOM360u.js → overlays-BOzLbVDK.js} +1 -1
- package/bundled-test-harness/assets/{overlays-kBVfa4i8.js → overlays-C_PuGDGh.js} +1 -1
- package/bundled-test-harness/assets/{past-sessions-test-CW0-Bjav.js → past-sessions-test-Bh24N-Kz.js} +1 -1
- package/bundled-test-harness/assets/{permission-checking-realm-test-D5uXQypS.js → permission-checking-realm-test-DKnnAy0f.js} +1 -1
- package/bundled-test-harness/assets/{plate-CFvCDv2t.js → plate-BL7UtyKE.js} +1 -1
- package/bundled-test-harness/assets/plate-CqG3igCJ.js +2 -0
- package/bundled-test-harness/assets/player-BJfsgdSI.js +2 -0
- package/bundled-test-harness/assets/{player-DzuYjrTU.js → player-Da_CIsp_.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-policy-standing-test-Btatheni.js → realm-config-policy-standing-test-DngdYypq.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-Dh5zW13o.js → realm-config-routing-rule-edit-test-_rkFU8SN.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-settings-test-BJa8gv_C.js → realm-config-settings-test-CR9NH0Zn.js} +1 -1
- package/bundled-test-harness/assets/{reasoning-test-2Zp37cQB.js → reasoning-test-0lH10kRl.js} +1 -1
- package/bundled-test-harness/assets/{room-message-test-BsrON13e.js → room-message-test-C-PddE4Y.js} +1 -1
- package/bundled-test-harness/assets/{run-BMyvn_UL.js → run-D4n4LPcK.js} +96 -6
- package/bundled-test-harness/assets/{run-realm-code-BxktQf6R.js → run-realm-code-C8N7DtN-.js} +1 -1
- package/bundled-test-harness/assets/{run-realm-code-test-7grpTos-.js → run-realm-code-test-DBdp147q.js} +1 -1
- package/bundled-test-harness/assets/{schema-generation-test-CM9ggQji.js → schema-generation-test-CB1Hb7GQ.js} +1 -1
- package/bundled-test-harness/assets/{scrolling-test-BER00htB.js → scrolling-test-3PhVin8h.js} +1 -1
- package/bundled-test-harness/assets/{searchable-option-test-BBr5BPow.js → searchable-option-test-BAdOiL4U.js} +1 -1
- package/bundled-test-harness/assets/{sending-test-BJGobs2E.js → sending-test-KSmu1tSJ.js} +1 -1
- package/bundled-test-harness/assets/{skills-test-GPOsHRN6.js → skills-test-D-dlcFpd.js} +1 -1
- package/bundled-test-harness/assets/{src-Df148w_b.js → src-B_DEhPze.js} +4 -4
- package/bundled-test-harness/assets/{src-Cf2wv-Le.js → src-BoTgwqUA.js} +2 -2
- package/bundled-test-harness/assets/{store-test-QvvHimzH.js → store-test-CEW7xQjZ.js} +1 -1
- package/bundled-test-harness/assets/{test-support-rNHuieEc.js → test-support-FXDPpIl3.js} +1 -1
- package/bundled-test-harness/assets/{test-support-C6N_2X-l.js → test-support-VX6ukQNR.js} +1 -1
- package/bundled-test-harness/assets/{tests-CmsQ-Z_3.js → tests-QYqqa6nv.js} +84 -84
- package/bundled-test-harness/assets/{text-input-validator-test-CYYH2N0L.js → text-input-validator-test-D8iNOWep.js} +1 -1
- package/bundled-test-harness/assets/{titles-B1pTaIfP.js → titles-DFJj5O0E.js} +1 -1
- package/bundled-test-harness/assets/titles-xGlgu7ys.js +2 -0
- package/bundled-test-harness/assets/{to-device-streaming-test-Ctqa0bVp.js → to-device-streaming-test-trCX-505.js} +1 -1
- package/bundled-test-harness/assets/{token-usage-test-BQVbK3X0.js → token-usage-test-C6sULooH.js} +1 -1
- package/bundled-test-harness/assets/{tools-DU8Rc15s.js → tools-UJfD-_al.js} +1 -1
- package/bundled-test-harness/assets/{tools-test-B13yvEKE.js → tools-test-Brav31AV.js} +1 -1
- package/bundled-test-harness/assets/{worker-DHBEBihX.js → worker-9ph9_Lpv.js} +31 -26
- package/bundled-test-harness/index.html +8 -8
- package/bundled-test-harness/tests/index.html +8 -8
- package/bundled-types/choreo/beacons.ts +16 -4
- package/bundled-types/choreo/changeset.ts +25 -5
- package/bundled-types/choreo/choreo.gts +245 -3
- package/bundled-types/choreo/far.ts +39 -0
- package/bundled-types/choreo/index.ts +2 -0
- package/bundled-types/choreo/run.ts +104 -1
- package/bundled-types/host-app/lib/realm-runner/worker.ts +8 -51
- package/bundled-types/runtime-common/realm-runner-script.d.ts +3 -0
- package/package.json +1 -1
- package/bundled-test-harness/assets/choreo-B3Z9QhDH.js +0 -2
- package/bundled-test-harness/assets/clip-BjcXuTwN.js +0 -2
- package/bundled-test-harness/assets/film-DMDPP7ma.js +0 -2
- package/bundled-test-harness/assets/plate-66qnQ-M9.js +0 -2
- package/bundled-test-harness/assets/player-pygg1PwA.js +0 -2
- package/bundled-test-harness/assets/titles-Ccpcl-jq.js +0 -2
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// Pretui — the streaming projection behind MarkdownStream: a fence-aware
|
|
2
|
+
// block splitter, an incremental projector, and a tail healer. Pure
|
|
3
|
+
// functions, no DOM, no markdown engine (Law 9): turning a block's markdown
|
|
4
|
+
// into HTML is the caller's renderer's job. What this module decides is
|
|
5
|
+
// where blocks begin and end, which of them are finished, and how to make a
|
|
6
|
+
// half-written tail render sensibly. The split is line-based, so the kit
|
|
7
|
+
// carries no markdown parser.
|
|
8
|
+
|
|
9
|
+
export type BlockKind = 'text' | 'code' | 'directive';
|
|
10
|
+
export type BlockMode = 'frozen' | 'live';
|
|
11
|
+
/** `markdown` is CommonMark/GFM; `bfm` is Boxel Flavored Markdown, which
|
|
12
|
+
* adds `$`/`$$` math, `:card[…]`/`:file[…]` references (inline and, with
|
|
13
|
+
* `::`, block) and treats Mermaid fences as diagrams. */
|
|
14
|
+
export type MarkdownFlavor = 'markdown' | 'bfm';
|
|
15
|
+
|
|
16
|
+
export interface ProjectedBlock {
|
|
17
|
+
/** stable across updates: the block's start offset plus its kind */
|
|
18
|
+
key: string;
|
|
19
|
+
/** offset of the block's first character in the full text */
|
|
20
|
+
start: number;
|
|
21
|
+
kind: BlockKind;
|
|
22
|
+
/** the block's markdown exactly as written */
|
|
23
|
+
raw: string;
|
|
24
|
+
mode: BlockMode;
|
|
25
|
+
/** code blocks: the source between the fences */
|
|
26
|
+
source?: string;
|
|
27
|
+
/** code blocks: the info string's first word */
|
|
28
|
+
language?: string;
|
|
29
|
+
/** code blocks: whether the closing fence has arrived */
|
|
30
|
+
complete?: boolean;
|
|
31
|
+
/** directive blocks */
|
|
32
|
+
directive?: Directive;
|
|
33
|
+
/** bfm text blocks: a `$$` display-math block whose closing `$$` has not
|
|
34
|
+
* arrived. It must not reach the renderer half-written. */
|
|
35
|
+
pendingMath?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Directive {
|
|
39
|
+
name: string;
|
|
40
|
+
/** the text inside the brackets before the first `|` */
|
|
41
|
+
body: string;
|
|
42
|
+
/** the `|`-separated arguments after it, trimmed */
|
|
43
|
+
args: string[];
|
|
44
|
+
/** false while the closing `]` has not arrived (bfm references only) */
|
|
45
|
+
complete: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Projection {
|
|
49
|
+
text: string;
|
|
50
|
+
blocks: ProjectedBlock[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
54
|
+
const DIRECTIVE = /^::([A-Za-z][\w-]*)\[(.*)\]\s*$/;
|
|
55
|
+
const HEADING = /^ {0,3}#{1,6}(\s|$)/;
|
|
56
|
+
const LIST_ITEM = /^ {0,3}([-*+]|\d{1,9}[.)])\s/;
|
|
57
|
+
const CONTINUATION = /^( {2,}|\t)\S/;
|
|
58
|
+
const MATH_OPEN = /^ {0,3}\$\$/;
|
|
59
|
+
const PARTIAL_REF = /^::(card|file)\[([^\]\n]*)$/;
|
|
60
|
+
|
|
61
|
+
/** Parse `::name[body | arg | arg]`. */
|
|
62
|
+
export function parseDirective(line: string): Directive | undefined {
|
|
63
|
+
let match = DIRECTIVE.exec(line);
|
|
64
|
+
if (!match) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
let [body = '', ...args] = match[2]!.split('|').map((part) => part.trim());
|
|
68
|
+
return { name: match[1]!, body, args, complete: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface Line {
|
|
72
|
+
text: string;
|
|
73
|
+
start: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function linesOf(text: string, offset: number): Line[] {
|
|
77
|
+
let lines: Line[] = [];
|
|
78
|
+
let start = 0;
|
|
79
|
+
while (start <= text.length) {
|
|
80
|
+
let end = text.indexOf('\n', start);
|
|
81
|
+
if (end === -1) {
|
|
82
|
+
if (start < text.length) {
|
|
83
|
+
lines.push({ text: text.slice(start), start: offset + start });
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
lines.push({ text: text.slice(start, end), start: offset + start });
|
|
88
|
+
start = end + 1;
|
|
89
|
+
}
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isBlank(line: string): boolean {
|
|
94
|
+
return line.trim() === '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Split `text` (which starts at `offset` in the full message) into blocks.
|
|
99
|
+
* Every block is returned `frozen`; the projector decides which is live.
|
|
100
|
+
*/
|
|
101
|
+
export function splitBlocks(
|
|
102
|
+
text: string,
|
|
103
|
+
offset = 0,
|
|
104
|
+
flavor: MarkdownFlavor = 'markdown',
|
|
105
|
+
): ProjectedBlock[] {
|
|
106
|
+
let bfm = flavor === 'bfm';
|
|
107
|
+
let lines = linesOf(text, offset);
|
|
108
|
+
let blocks: ProjectedBlock[] = [];
|
|
109
|
+
let i = 0;
|
|
110
|
+
let sliceRaw = (from: number, to: number) =>
|
|
111
|
+
text.slice(from - offset, to - offset);
|
|
112
|
+
|
|
113
|
+
while (i < lines.length) {
|
|
114
|
+
let line = lines[i]!;
|
|
115
|
+
if (isBlank(line.text)) {
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let fence = FENCE_OPEN.exec(line.text);
|
|
121
|
+
if (fence) {
|
|
122
|
+
let marker = fence[1]!;
|
|
123
|
+
let close = new RegExp(
|
|
124
|
+
`^ {0,3}${marker[0] === '`' ? '`' : '~'}{${marker.length},}\\s*$`,
|
|
125
|
+
);
|
|
126
|
+
let j = i + 1;
|
|
127
|
+
while (j < lines.length && !close.test(lines[j]!.text)) {
|
|
128
|
+
j++;
|
|
129
|
+
}
|
|
130
|
+
let complete = j < lines.length;
|
|
131
|
+
let bodyLines = lines.slice(i + 1, j).map((l) => l.text);
|
|
132
|
+
let last = complete ? lines[j]! : lines[lines.length - 1]!;
|
|
133
|
+
let end = last.start + last.text.length;
|
|
134
|
+
// an open fence's source keeps its trailing newline state as typed, so
|
|
135
|
+
// the code grows by exactly the appended characters
|
|
136
|
+
let source = complete
|
|
137
|
+
? bodyLines.join('\n')
|
|
138
|
+
: text.slice(
|
|
139
|
+
Math.min(line.start + line.text.length + 1, offset + text.length) -
|
|
140
|
+
offset,
|
|
141
|
+
);
|
|
142
|
+
blocks.push({
|
|
143
|
+
key: `${line.start}:code`,
|
|
144
|
+
start: line.start,
|
|
145
|
+
kind: 'code',
|
|
146
|
+
raw: sliceRaw(line.start, end),
|
|
147
|
+
mode: 'frozen',
|
|
148
|
+
source,
|
|
149
|
+
language: fence[2]!.trim().split(/\s+/, 1)[0] || undefined,
|
|
150
|
+
complete,
|
|
151
|
+
});
|
|
152
|
+
i = complete ? j + 1 : lines.length;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// bfm display math: `$$` to the line ending in `$$`, blank lines included,
|
|
157
|
+
// so a formula is never cut into separately rendered pieces
|
|
158
|
+
if (bfm && MATH_OPEN.test(line.text)) {
|
|
159
|
+
let single =
|
|
160
|
+
line.text.trim().length > 2 && line.text.trim().endsWith('$$');
|
|
161
|
+
let j = i;
|
|
162
|
+
if (!single) {
|
|
163
|
+
j = i + 1;
|
|
164
|
+
while (j < lines.length && !lines[j]!.text.trim().endsWith('$$')) {
|
|
165
|
+
j++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let closed = j < lines.length;
|
|
169
|
+
let last = closed ? lines[j]! : lines[lines.length - 1]!;
|
|
170
|
+
blocks.push({
|
|
171
|
+
key: `${line.start}:text`,
|
|
172
|
+
start: line.start,
|
|
173
|
+
kind: 'text',
|
|
174
|
+
raw: sliceRaw(line.start, last.start + last.text.length),
|
|
175
|
+
mode: 'frozen',
|
|
176
|
+
pendingMath: !closed,
|
|
177
|
+
});
|
|
178
|
+
i = closed ? j + 1 : lines.length;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// a bfm block reference still arriving: the last line, no closing `]`
|
|
183
|
+
let partial =
|
|
184
|
+
bfm && i === lines.length - 1 ? PARTIAL_REF.exec(line.text) : null;
|
|
185
|
+
if (partial) {
|
|
186
|
+
let [body = '', ...args] = partial[2]!.split('|').map((p) => p.trim());
|
|
187
|
+
blocks.push({
|
|
188
|
+
key: `${line.start}:directive`,
|
|
189
|
+
start: line.start,
|
|
190
|
+
kind: 'directive',
|
|
191
|
+
raw: line.text,
|
|
192
|
+
mode: 'frozen',
|
|
193
|
+
directive: { name: partial[1]!, body, args, complete: false },
|
|
194
|
+
});
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let directive = parseDirective(line.text);
|
|
200
|
+
if (directive) {
|
|
201
|
+
blocks.push({
|
|
202
|
+
key: `${line.start}:directive`,
|
|
203
|
+
start: line.start,
|
|
204
|
+
kind: 'directive',
|
|
205
|
+
raw: line.text,
|
|
206
|
+
mode: 'frozen',
|
|
207
|
+
directive,
|
|
208
|
+
});
|
|
209
|
+
i++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (HEADING.test(line.text)) {
|
|
214
|
+
blocks.push({
|
|
215
|
+
key: `${line.start}:text`,
|
|
216
|
+
start: line.start,
|
|
217
|
+
kind: 'text',
|
|
218
|
+
raw: line.text,
|
|
219
|
+
mode: 'frozen',
|
|
220
|
+
});
|
|
221
|
+
i++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// A text block runs to a blank line, a fence, a directive or a heading.
|
|
226
|
+
// A list keeps going across a blank line when the next line is another
|
|
227
|
+
// item or an indented continuation, so a loose list stays one block.
|
|
228
|
+
let isList = LIST_ITEM.test(line.text);
|
|
229
|
+
let j = i + 1;
|
|
230
|
+
while (j < lines.length) {
|
|
231
|
+
let next = lines[j]!.text;
|
|
232
|
+
if (
|
|
233
|
+
FENCE_OPEN.test(next) ||
|
|
234
|
+
DIRECTIVE.test(next) ||
|
|
235
|
+
HEADING.test(next) ||
|
|
236
|
+
(bfm && (MATH_OPEN.test(next) || PARTIAL_REF.test(next)))
|
|
237
|
+
) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (isBlank(next)) {
|
|
241
|
+
let after = lines[j + 1]?.text;
|
|
242
|
+
if (
|
|
243
|
+
isList &&
|
|
244
|
+
after !== undefined &&
|
|
245
|
+
(LIST_ITEM.test(after) || CONTINUATION.test(after))
|
|
246
|
+
) {
|
|
247
|
+
j++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
j++;
|
|
253
|
+
}
|
|
254
|
+
let last = lines[j - 1]!;
|
|
255
|
+
blocks.push({
|
|
256
|
+
key: `${line.start}:text`,
|
|
257
|
+
start: line.start,
|
|
258
|
+
kind: 'text',
|
|
259
|
+
raw: sliceRaw(line.start, last.start + last.text.length),
|
|
260
|
+
mode: 'frozen',
|
|
261
|
+
});
|
|
262
|
+
i = j;
|
|
263
|
+
}
|
|
264
|
+
return blocks;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function markLive(
|
|
268
|
+
blocks: ProjectedBlock[],
|
|
269
|
+
streaming: boolean,
|
|
270
|
+
): ProjectedBlock[] {
|
|
271
|
+
if (!streaming || blocks.length === 0) {
|
|
272
|
+
return blocks;
|
|
273
|
+
}
|
|
274
|
+
let last = blocks[blocks.length - 1]!;
|
|
275
|
+
// an open fence grows by appending; a closed one is finished
|
|
276
|
+
if (last.kind === 'code') {
|
|
277
|
+
return blocks;
|
|
278
|
+
}
|
|
279
|
+
return [...blocks.slice(0, -1), { ...last, mode: 'live' }];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Project `text` into blocks, reusing `previous` where the new text extends
|
|
284
|
+
* it: only the region from the previous last block onwards is re-split, so
|
|
285
|
+
* an update costs the tail, not the whole message.
|
|
286
|
+
*/
|
|
287
|
+
export function project(
|
|
288
|
+
previous: Projection | undefined,
|
|
289
|
+
text: string,
|
|
290
|
+
streaming: boolean,
|
|
291
|
+
flavor: MarkdownFlavor = 'markdown',
|
|
292
|
+
): Projection {
|
|
293
|
+
if (
|
|
294
|
+
previous &&
|
|
295
|
+
previous.blocks.length > 0 &&
|
|
296
|
+
text.startsWith(previous.text)
|
|
297
|
+
) {
|
|
298
|
+
let tailStart = previous.blocks[previous.blocks.length - 1]!.start;
|
|
299
|
+
let kept = previous.blocks
|
|
300
|
+
.slice(0, -1)
|
|
301
|
+
.map((block) =>
|
|
302
|
+
block.mode === 'frozen' ? block : { ...block, mode: 'frozen' as const },
|
|
303
|
+
);
|
|
304
|
+
let tail = splitBlocks(text.slice(tailStart), tailStart, flavor);
|
|
305
|
+
return { text, blocks: markLive([...kept, ...tail], streaming) };
|
|
306
|
+
}
|
|
307
|
+
return { text, blocks: markLive(splitBlocks(text, 0, flavor), streaming) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── healing ──────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
const INLINE_CODE = /`[^`\n]*`/g;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Make a half-written block render sensibly: close unbalanced inline code
|
|
316
|
+
* and emphasis, and reduce an unterminated link to its text. Only ever
|
|
317
|
+
* applied to the live tail; the raw text is what freezes.
|
|
318
|
+
*/
|
|
319
|
+
export function heal(src: string): string {
|
|
320
|
+
let out = src;
|
|
321
|
+
|
|
322
|
+
// an image or link whose URL is still arriving: keep the text, drop the rest
|
|
323
|
+
out = out.replace(/!\[([^\]\n]*)\]\([^)\n]*$/, '$1');
|
|
324
|
+
out = out.replace(/\[([^\]\n]*)\]\([^)\n]*$/, '$1');
|
|
325
|
+
out = out.replace(/!?\[([^\]\n]*)$/, '$1');
|
|
326
|
+
|
|
327
|
+
// an odd number of single backticks: the last code span is still open
|
|
328
|
+
let ticks = (out.match(/`/g) ?? []).length;
|
|
329
|
+
if (ticks % 2 === 1) {
|
|
330
|
+
return `${out}\``;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// emphasis markers outside code spans, closed innermost (latest) first
|
|
334
|
+
let bare = out.replace(INLINE_CODE, (span) => ' '.repeat(span.length));
|
|
335
|
+
let open: { marker: string; at: number }[] = [];
|
|
336
|
+
for (let marker of ['**', '__', '~~']) {
|
|
337
|
+
let positions: number[] = [];
|
|
338
|
+
let index = bare.indexOf(marker);
|
|
339
|
+
while (index !== -1) {
|
|
340
|
+
positions.push(index);
|
|
341
|
+
index = bare.indexOf(marker, index + marker.length);
|
|
342
|
+
}
|
|
343
|
+
if (positions.length % 2 === 1) {
|
|
344
|
+
open.push({ marker, at: positions[positions.length - 1]! });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// a single `*` that is not a list bullet and not part of `**`
|
|
348
|
+
let singles: number[] = [];
|
|
349
|
+
let withoutDoubles = bare.replace(/\*\*/g, ' ');
|
|
350
|
+
for (let i = 0; i < withoutDoubles.length; i++) {
|
|
351
|
+
if (withoutDoubles[i] !== '*') {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
let lineStart = withoutDoubles.lastIndexOf('\n', i - 1) + 1;
|
|
355
|
+
let isBullet =
|
|
356
|
+
withoutDoubles.slice(lineStart, i).trim() === '' &&
|
|
357
|
+
withoutDoubles[i + 1] === ' ';
|
|
358
|
+
if (!isBullet) {
|
|
359
|
+
singles.push(i);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (singles.length % 2 === 1) {
|
|
363
|
+
open.push({ marker: '*', at: singles[singles.length - 1]! });
|
|
364
|
+
}
|
|
365
|
+
open.sort((a, b) => b.at - a.at);
|
|
366
|
+
return out + open.map((o) => o.marker).join('');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* BFM's additions to healing, applied before `heal`: an inline `:card[` or
|
|
371
|
+
* `:file[` reference whose `]` has not arrived renders as nothing yet (a
|
|
372
|
+
* half-written URL is neither text nor a card), and a lone unclosed `$` is
|
|
373
|
+
* escaped so it renders as a dollar sign rather than an open formula.
|
|
374
|
+
*/
|
|
375
|
+
export function healBfm(src: string): string {
|
|
376
|
+
let out = src.replace(/(^|[^:]):(?:card|file)\[[^\]\n]*$/, '$1');
|
|
377
|
+
let bare = out.replace(INLINE_CODE, (span) => ' '.repeat(span.length));
|
|
378
|
+
let singles: number[] = [];
|
|
379
|
+
for (let i = 0; i < bare.length; i++) {
|
|
380
|
+
if (bare[i] !== '$' || bare[i - 1] === '\\') {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (bare[i + 1] === '$' || bare[i - 1] === '$') {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
singles.push(i);
|
|
387
|
+
}
|
|
388
|
+
if (singles.length % 2 === 1) {
|
|
389
|
+
let at = singles[singles.length - 1]!;
|
|
390
|
+
out = `${out.slice(0, at)}\\${out.slice(at)}`;
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const DEFINITION = /^ {0,3}\[(\^?)[^\]\n]+\]:[ \t]*\S.*$/gm;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Every link-reference and footnote definition in the document, one per
|
|
399
|
+
* line. A block rendered on its own cannot see definitions that live in
|
|
400
|
+
* another block, so the renderer receives them as context.
|
|
401
|
+
*/
|
|
402
|
+
export function collectDefinitions(text: string): string {
|
|
403
|
+
return (text.match(DEFINITION) ?? []).join('\n');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── links ────────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
const PATH_LIKE =
|
|
409
|
+
/^(?:\.{0,2}\/)?[\w@.-]+(?:\/[\w@.-]+)*\.[A-Za-z0-9]{1,8}(?::\d+(?:-\d+)?)?(?:#L\d+(?:-L?\d+)?)?$/;
|
|
410
|
+
|
|
411
|
+
/** Whether inline code text looks like a file path worth resolving:
|
|
412
|
+
* `src/a.gts`, `a.gts:42`, `card.json#L4-L9`. */
|
|
413
|
+
export function isPathLike(text: string): boolean {
|
|
414
|
+
let trimmed = text.trim();
|
|
415
|
+
return trimmed.length > 2 && trimmed.length < 300 && PATH_LIKE.test(trimmed);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Whether an href leaves the app (and so needs no lookup). */
|
|
419
|
+
export function isExternalHref(href: string): boolean {
|
|
420
|
+
return /^(https?:|mailto:)/i.test(href);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── default rendering ────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
function escapeHtml(text: string): string {
|
|
426
|
+
return text
|
|
427
|
+
.replace(/&/g, '&')
|
|
428
|
+
.replace(/</g, '<')
|
|
429
|
+
.replace(/>/g, '>')
|
|
430
|
+
.replace(/"/g, '"')
|
|
431
|
+
.replace(/'/g, ''');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The renderer used when the caller supplies none: the text escaped, one
|
|
435
|
+
* paragraph per block, line breaks kept. Safe by construction. */
|
|
436
|
+
export function renderPlain(src: string): string {
|
|
437
|
+
return `<p>${escapeHtml(src).replace(/\n/g, '<br>')}</p>`;
|
|
438
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Pretui — the shared, bottom-first mount queue for heavy transcript bodies.
|
|
2
|
+
//
|
|
3
|
+
// A transcript renders its rows top to bottom, but the reader is at the
|
|
4
|
+
// bottom. Mounting every open tool body in document order makes the newest
|
|
5
|
+
// one (the one the reader is looking at) interactive last. So bodies are
|
|
6
|
+
// queued and mounted newest first: the queue is a stack, flushed after two
|
|
7
|
+
// animation frames (the first lets the rows themselves paint), one body per
|
|
8
|
+
// frame so no single frame pays for all of them.
|
|
9
|
+
//
|
|
10
|
+
// Cancelling marks an entry inactive instead of splicing it out, so a row
|
|
11
|
+
// that closes, or unmounts under virtualization, costs O(1) and the flush
|
|
12
|
+
// simply skips it.
|
|
13
|
+
//
|
|
14
|
+
// Timer-free: requestAnimationFrame only. Where it does not exist (the
|
|
15
|
+
// prerenderer), each mount runs on a microtask instead, so the prerendered
|
|
16
|
+
// output carries the mounted body and nothing waits on a frame that never
|
|
17
|
+
// comes. A microtask rather than a synchronous call, because the caller is
|
|
18
|
+
// usually mid-render and must not write tracked state there.
|
|
19
|
+
|
|
20
|
+
interface Entry {
|
|
21
|
+
active: boolean;
|
|
22
|
+
run: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stack: Entry[] = [];
|
|
26
|
+
let frame: number | undefined;
|
|
27
|
+
|
|
28
|
+
function hasFrames(): boolean {
|
|
29
|
+
return typeof requestAnimationFrame === 'function';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function flush() {
|
|
33
|
+
while (stack.length > 0) {
|
|
34
|
+
let entry = stack.pop()!;
|
|
35
|
+
if (entry.active) {
|
|
36
|
+
entry.active = false;
|
|
37
|
+
frame = stack.length > 0 ? requestAnimationFrame(flush) : undefined;
|
|
38
|
+
entry.run();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
frame = undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function schedule() {
|
|
46
|
+
if (frame !== undefined) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
frame = requestAnimationFrame(() => {
|
|
50
|
+
frame = requestAnimationFrame(flush);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Queue `run` to mount a heavy body. Returns a cancel function; calling it
|
|
56
|
+
* after `run` has happened is harmless.
|
|
57
|
+
*/
|
|
58
|
+
export function scheduleMount(run: () => void): () => void {
|
|
59
|
+
let entry: Entry = { active: true, run };
|
|
60
|
+
if (!hasFrames()) {
|
|
61
|
+
queueMicrotask(() => {
|
|
62
|
+
if (entry.active) {
|
|
63
|
+
entry.active = false;
|
|
64
|
+
entry.run();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
stack.push(entry);
|
|
69
|
+
schedule();
|
|
70
|
+
}
|
|
71
|
+
return () => {
|
|
72
|
+
entry.active = false;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** How many mounts are still waiting. For tests and diagnostics. */
|
|
77
|
+
export function pendingMounts(): number {
|
|
78
|
+
return stack.filter((entry) => entry.active).length;
|
|
79
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Pretui — the phrase grammar for tool calls: what a run of calls DID, in
|
|
2
|
+
// words, rather than which tools it used. "Read 3 files, searched 2
|
|
3
|
+
// queries, and edited 1 file" instead of "Used 6 tools · Read, Search,
|
|
4
|
+
// Patch". While the run is still going, the phrase for the newest call reads
|
|
5
|
+
// in the present tense ("editing 1 file"); the rest are past.
|
|
6
|
+
|
|
7
|
+
export type ToolKind =
|
|
8
|
+
| 'read'
|
|
9
|
+
| 'search'
|
|
10
|
+
| 'write'
|
|
11
|
+
| 'command'
|
|
12
|
+
| 'web'
|
|
13
|
+
| 'agent'
|
|
14
|
+
| 'other';
|
|
15
|
+
|
|
16
|
+
export interface ToolPhrase {
|
|
17
|
+
/** present tense, while the call is running: 'Reading' */
|
|
18
|
+
active: string;
|
|
19
|
+
/** past tense, once it settled: 'Read' */
|
|
20
|
+
done: string;
|
|
21
|
+
/** the noun for one, and for several: 'file', 'files' */
|
|
22
|
+
one: string;
|
|
23
|
+
many: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const KIND_PHRASES: Record<ToolKind, ToolPhrase> = {
|
|
27
|
+
read: { active: 'Reading', done: 'Read', one: 'file', many: 'files' },
|
|
28
|
+
search: {
|
|
29
|
+
active: 'Searching',
|
|
30
|
+
done: 'Searched',
|
|
31
|
+
one: 'query',
|
|
32
|
+
many: 'queries',
|
|
33
|
+
},
|
|
34
|
+
write: { active: 'Editing', done: 'Edited', one: 'file', many: 'files' },
|
|
35
|
+
command: { active: 'Running', done: 'Ran', one: 'command', many: 'commands' },
|
|
36
|
+
web: {
|
|
37
|
+
active: 'Researching',
|
|
38
|
+
done: 'Researched',
|
|
39
|
+
one: 'source',
|
|
40
|
+
many: 'sources',
|
|
41
|
+
},
|
|
42
|
+
agent: {
|
|
43
|
+
active: 'Delegating to',
|
|
44
|
+
done: 'Delegated to',
|
|
45
|
+
one: 'agent',
|
|
46
|
+
many: 'agents',
|
|
47
|
+
},
|
|
48
|
+
other: { active: 'Using', done: 'Used', one: 'tool', many: 'tools' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface PhraseItem {
|
|
52
|
+
/** stable: identifies the phrase across updates */
|
|
53
|
+
key: string;
|
|
54
|
+
/** the verb in the right tense, lower-case unless it opens the receipt */
|
|
55
|
+
verb: string;
|
|
56
|
+
count: number;
|
|
57
|
+
noun: string;
|
|
58
|
+
/** present tense: this is what the run is doing now */
|
|
59
|
+
active: boolean;
|
|
60
|
+
/** the whole phrase as text */
|
|
61
|
+
text: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function phraseKey(phrase: ToolPhrase): string {
|
|
65
|
+
return `${phrase.done}|${phrase.one}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function lowerFirst(text: string): string {
|
|
69
|
+
return text ? text[0]!.toLowerCase() + text.slice(1) : text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function upperFirst(text: string): string {
|
|
73
|
+
return text ? text[0]!.toUpperCase() + text.slice(1) : text;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Group `items` by phrase, in order of first appearance, counting each.
|
|
78
|
+
* `activeIndex` is the index of the call still running, if any: its phrase
|
|
79
|
+
* reads in the present tense.
|
|
80
|
+
*/
|
|
81
|
+
export function phraseRun<T>(
|
|
82
|
+
items: T[],
|
|
83
|
+
phraseOf: (item: T) => ToolPhrase,
|
|
84
|
+
activeIndex?: number,
|
|
85
|
+
): PhraseItem[] {
|
|
86
|
+
let order: string[] = [];
|
|
87
|
+
let groups = new Map<string, { phrase: ToolPhrase; count: number }>();
|
|
88
|
+
items.forEach((item) => {
|
|
89
|
+
let phrase = phraseOf(item);
|
|
90
|
+
let key = phraseKey(phrase);
|
|
91
|
+
let group = groups.get(key);
|
|
92
|
+
if (group) {
|
|
93
|
+
group.count++;
|
|
94
|
+
} else {
|
|
95
|
+
groups.set(key, { phrase, count: 1 });
|
|
96
|
+
order.push(key);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
let activeKey =
|
|
100
|
+
activeIndex !== undefined && items[activeIndex] !== undefined
|
|
101
|
+
? phraseKey(phraseOf(items[activeIndex]!))
|
|
102
|
+
: undefined;
|
|
103
|
+
return order.map((key, index) => {
|
|
104
|
+
let { phrase, count } = groups.get(key)!;
|
|
105
|
+
let active = key === activeKey;
|
|
106
|
+
let verb = active ? phrase.active : phrase.done;
|
|
107
|
+
verb = index === 0 ? upperFirst(verb) : lowerFirst(verb);
|
|
108
|
+
let noun = count === 1 ? phrase.one : phrase.many;
|
|
109
|
+
return { key, verb, count, noun, active, text: `${verb} ${count} ${noun}` };
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// English, like every verb and noun around it: a list joined in the
|
|
114
|
+
// browser's locale would read "Read 3 files und edited 1 file". Change this
|
|
115
|
+
// together with the phrases when they are localised.
|
|
116
|
+
const LIST_LOCALE = 'en';
|
|
117
|
+
|
|
118
|
+
function formatter(): Intl.ListFormat | undefined {
|
|
119
|
+
return typeof Intl !== 'undefined' && 'ListFormat' in Intl
|
|
120
|
+
? new Intl.ListFormat(LIST_LOCALE, { style: 'long', type: 'conjunction' })
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The phrases as one list, in English. */
|
|
125
|
+
export function joinPhrases(texts: string[]): string {
|
|
126
|
+
if (texts.length < 2) {
|
|
127
|
+
return texts[0] ?? '';
|
|
128
|
+
}
|
|
129
|
+
let format = formatter();
|
|
130
|
+
if (format) {
|
|
131
|
+
return format.format(texts);
|
|
132
|
+
}
|
|
133
|
+
return `${texts.slice(0, -1).join(', ')} and ${texts[texts.length - 1]}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The literal separators the list puts between `count` items, so a
|
|
138
|
+
* template can lay the items out as components (a rolling count each) and
|
|
139
|
+
* still read as one grammatical list. separators[i] goes before item i; the
|
|
140
|
+
* first is ''.
|
|
141
|
+
*/
|
|
142
|
+
export function listSeparators(count: number): string[] {
|
|
143
|
+
if (count <= 1) {
|
|
144
|
+
return count === 1 ? [''] : [];
|
|
145
|
+
}
|
|
146
|
+
let format = formatter();
|
|
147
|
+
let placeholders = Array.from({ length: count }, (_, i) => `\u0000${i}`);
|
|
148
|
+
if (!format || typeof format.formatToParts !== 'function') {
|
|
149
|
+
return placeholders.map((_, i) =>
|
|
150
|
+
i === 0 ? '' : i === count - 1 ? ' and ' : ', ',
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
let separators: string[] = [''];
|
|
154
|
+
let pending = '';
|
|
155
|
+
let seen = 0;
|
|
156
|
+
for (let part of format.formatToParts(placeholders)) {
|
|
157
|
+
if (part.type === 'element') {
|
|
158
|
+
if (seen > 0) {
|
|
159
|
+
separators.push(pending);
|
|
160
|
+
}
|
|
161
|
+
seen++;
|
|
162
|
+
pending = '';
|
|
163
|
+
} else {
|
|
164
|
+
pending += part.value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return separators;
|
|
168
|
+
}
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"lint:usage-proof": "node scripts/usage-proof.mjs"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"ember-modifier": "^4.1.0"
|
|
17
|
+
"ember-modifier": "^4.1.0",
|
|
18
|
+
"glimmer-motion": "workspace:*"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@babel/plugin-proposal-decorators": "catalog:",
|
|
@@ -8,7 +8,7 @@ import { H as templateFactory } from "./index-DaCZcoCY-CgzbilMy.js";
|
|
|
8
8
|
import "./component-BJO47HTh.js";
|
|
9
9
|
import { t as Component } from "./dist-CkA1rgqO.js";
|
|
10
10
|
import "./template-factory-ds9s0u8l.js";
|
|
11
|
-
import { t as OperatorModeContainer } from "./container-
|
|
11
|
+
import { t as OperatorModeContainer } from "./container-BnJjIM6J.js";
|
|
12
12
|
import { d as click, i as findAll, o as waitFor } from "./dist-BOAdzfkP.js";
|
|
13
13
|
import { t as require_qunit } from "./qunit-C-Foo4Nd.js";
|
|
14
14
|
import { F as setupLocalIndexing, M as setupCardLogs, P as setupIntegrationTestRealm, lt as setupRenderingTest, ot as setupOperatorModeStateCleanup, t as setupMockMatrix } from "./mock-matrix-rxYRI9xw.js";
|