@bivy/bivy 0.20.9-staging.9 → 0.21.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 (168) hide show
  1. package/README.md +1 -2
  2. package/dist/agent-instructions.js +14 -4
  3. package/dist/apps/remote-preview.js +7 -0
  4. package/dist/apps/service.js +6 -1
  5. package/dist/apps/styles.css +56 -70
  6. package/dist/automation-reseal.js +30 -0
  7. package/dist/config-cli.js +2 -0
  8. package/dist/controllers/credential-commands.js +3 -27
  9. package/dist/credentials/api.js +2 -43
  10. package/dist/credentials/document.js +6 -14
  11. package/dist/credentials/store.js +5 -6
  12. package/dist/device-registry.js +27 -0
  13. package/dist/harness/checkpoint.js +0 -0
  14. package/dist/node-config.js +4 -3
  15. package/dist/runtime/model-catalog.js +16 -0
  16. package/dist/runtime/tool-call-map.js +1 -1
  17. package/dist/runtime/tool-trace.js +1 -1
  18. package/dist/server.js +136 -244
  19. package/dist/session/event-log.js +136 -29
  20. package/dist/session/inline-image-fetch.js +24 -33
  21. package/dist/session/message-components.js +429 -0
  22. package/dist/session/run-terminal.js +9 -7
  23. package/dist/session/transcript-persistence.js +135 -10
  24. package/dist/work-instructions.js +50 -0
  25. package/package.json +2 -2
  26. package/web/assets/AutomationsView-HnUfeti_.js +51 -0
  27. package/web/assets/ChangesCard-Dnkguen5.js +4 -0
  28. package/web/assets/LibraryView-CSKKV8TI.js +2 -0
  29. package/web/assets/{ProviderConnect-BPwnaFGk.js → ProviderConnect-DNgxGmP9.js} +1 -1
  30. package/web/assets/SessionList-CYU9IGnd.js +1 -0
  31. package/web/assets/Settings-HQZtxiMV.js +10 -0
  32. package/web/assets/{Sheet-DOOJosQY.js → Sheet-DoPtOHBC.js} +1 -1
  33. package/web/assets/{StatusDot-dpbztK53.js → Spinner-CPdRfDcT.js} +1 -1
  34. package/web/assets/{Terminal-DVGXS6Yd.js → Terminal-DY6UB5U-.js} +1 -1
  35. package/web/assets/{VoiceSettings-DX-n0SFl.js → VoiceSettings-BoQ_Mhpm.js} +1 -1
  36. package/web/assets/{abnfDiagram-VCTEODGH-CnJ4astd.js → abnfDiagram-VCTEODGH-n-P11_L4.js} +1 -1
  37. package/web/assets/arc-BNLueWOY.js +1 -0
  38. package/web/assets/architecture-7GRP2DOG-DyG-wgaH.js +1 -0
  39. package/web/assets/{architectureDiagram-5GKGNRK7-B26-V_Xd.js → architectureDiagram-5GKGNRK7-C0b9IP5U.js} +1 -1
  40. package/web/assets/{blockDiagram-I7D4REHJ-DuoZkDHs.js → blockDiagram-I7D4REHJ-iVx1FQBD.js} +1 -1
  41. package/web/assets/{c4Diagram-7LVT6UL2-B81PZ-Vw.js → c4Diagram-7LVT6UL2-DIwEaAvJ.js} +1 -1
  42. package/web/assets/channel-HmCJmS4T.js +1 -0
  43. package/web/assets/{chunk-4HAMMTFA-DmSrqg9X.js → chunk-4HAMMTFA-DSsyukSz.js} +1 -1
  44. package/web/assets/chunk-75Z2AOVW-zwLYo-XL.js +2 -0
  45. package/web/assets/{chunk-DU6HZSFF-jMBIr1yK.js → chunk-DU6HZSFF-PGuLkqeP.js} +15 -15
  46. package/web/assets/chunk-F27PBJKO-CpkyeGC9.js +1 -0
  47. package/web/assets/{chunk-GMAD6QVW-C5YdBKWY.js → chunk-GMAD6QVW-CZSmK3Ms.js} +1 -1
  48. package/web/assets/{chunk-GVQU2GXP-DOOAmJLJ.js → chunk-GVQU2GXP-C2XMkY6v.js} +1 -1
  49. package/web/assets/{chunk-IMKFNOWR-CISXYXGE.js → chunk-IMKFNOWR-DAIVz3j5.js} +1 -1
  50. package/web/assets/{chunk-L3NEJ4N5-CvWtiEy-.js → chunk-L3NEJ4N5-C0geNTDK.js} +1 -1
  51. package/web/assets/{chunk-OSK3NFVY-yVAn5yky.js → chunk-OSK3NFVY-D-WN2fBr.js} +2 -2
  52. package/web/assets/{chunk-P2QGCYS3-D-VGoELb.js → chunk-P2QGCYS3-DYGhprKz.js} +1 -1
  53. package/web/assets/{chunk-POPQ4Y6H-0Mj4Bp-N.js → chunk-POPQ4Y6H-Dlsu2a2Q.js} +1 -1
  54. package/web/assets/{chunk-PWAF6VOD-Dvvdg6s1.js → chunk-PWAF6VOD-DK0QWYdc.js} +1 -1
  55. package/web/assets/{chunk-SHT3W25Y-DcllnbDq.js → chunk-SHT3W25Y-BpQHIdUi.js} +1 -1
  56. package/web/assets/{chunk-SVP7TREG-DnuOGeio.js → chunk-SVP7TREG-Cs5YWxpG.js} +1 -1
  57. package/web/assets/{chunk-TICWLB2K-Dr76NyiT.js → chunk-TICWLB2K-BRsWLPXA.js} +1 -1
  58. package/web/assets/chunk-XXDRQBXY-CjF14Z7v.js +1 -0
  59. package/web/assets/classDiagram-ZZMXUADV-BmdAoNdO.js +1 -0
  60. package/web/assets/classDiagram-v2-VYDZK3BY-BmdAoNdO.js +1 -0
  61. package/web/assets/colors-Cg5Vh3br.js +1 -0
  62. package/web/assets/{cose-bilkent-JH36ORCC-BIqBF2fY.js → cose-bilkent-JH36ORCC-CEUD3V3W.js} +1 -1
  63. package/web/assets/{cynefin-OW5HDTMX-DoAt_4E6.js → cynefin-OW5HDTMX-DoHWnu_-.js} +1 -1
  64. package/web/assets/cynefinDiagram-5FMLGOSQ-CgPeHmV5.js +62 -0
  65. package/web/assets/{dagre-GXQ25YYZ-DIAXxVMd.js → dagre-GXQ25YYZ-CrIlgwM7.js} +1 -1
  66. package/web/assets/{defaultLocale-BFoDCU3G.js → defaultLocale-CNPGWpMu.js} +1 -1
  67. package/web/assets/{diagram-S7CK7UJ4-DA78JGp6.js → diagram-S7CK7UJ4-BM_l51zV.js} +1 -1
  68. package/web/assets/diagram-UQ7AKVKN-CfkxqgPI.js +41 -0
  69. package/web/assets/{diagram-VSXAHHWV-DCkvR_pJ.js → diagram-VSXAHHWV-0y5vCI9g.js} +1 -1
  70. package/web/assets/diagram-VX7I27RA-BMkMy7jn.js +24 -0
  71. package/web/assets/diagram-Z3DM3KII--0fxfDqG.js +24 -0
  72. package/web/assets/dist-kYfyPZP9.js +1 -0
  73. package/web/assets/{ebnfDiagram-PWID7BFC-CSCD0PAi.js → ebnfDiagram-PWID7BFC-6tPNzbJl.js} +1 -1
  74. package/web/assets/embed-BXV41wg7.js +201 -0
  75. package/web/assets/{erDiagram-RLTQ6QDP-M512qtMu.js → erDiagram-RLTQ6QDP-Bjqm_o3R.js} +1 -1
  76. package/web/assets/eventmodeling-NTZA5JFV-CoCZxN1n.js +1 -0
  77. package/web/assets/flowDiagram-HODETNUW-CznfWzEq.js +1 -0
  78. package/web/assets/ganttDiagram-EL5Y4UJY-92X7KOOI.js +292 -0
  79. package/web/assets/{gitGraph-4MIJSDKK-CwjwFpZb.js → gitGraph-4MIJSDKK-B5F-u7Vq.js} +1 -1
  80. package/web/assets/gitGraphDiagram-WWUBYQGX-Ct2hVG0b.js +106 -0
  81. package/web/assets/index-BKSbod42.css +1 -0
  82. package/web/assets/{index-CNs5i7tU.js → index-CnjJQpgZ.js} +2 -2
  83. package/web/assets/{info-A6RAGUB7-D6nXu5V2.js → info-A6RAGUB7-D3xmYWyC.js} +1 -1
  84. package/web/assets/infoDiagram-27XIBGKW-lzqZ7xaA.js +2 -0
  85. package/web/assets/init-ZlXS9mIS.js +1 -0
  86. package/web/assets/{ishikawaDiagram-5VMMS53U-4bsiyG2x.js → ishikawaDiagram-5VMMS53U-CsVZHXR0.js} +1 -1
  87. package/web/assets/{journeyDiagram-3NMN7TZE-o3U3L2rt.js → journeyDiagram-3NMN7TZE-DN9AdyVy.js} +1 -1
  88. package/web/assets/{kanban-definition-UXKFOSKX-CQOPj0NZ.js → kanban-definition-UXKFOSKX-gvkBaOFe.js} +1 -1
  89. package/web/assets/linear-CeYXFQza.js +1 -0
  90. package/web/assets/{mermaid-parser.core-Ba2-DGvb.js → mermaid-parser.core-CPwOm7-a.js} +3 -3
  91. package/web/assets/{mermaid.core-BxdMVbm9.js → mermaid.core-F09Wr40d.js} +6 -6
  92. package/web/assets/{mindmap-definition-YA3MSWOX-1Eu4-zLM.js → mindmap-definition-YA3MSWOX-Qzc2J9Pm.js} +1 -1
  93. package/web/assets/mount-Bvl-yslT.js +36 -0
  94. package/web/assets/ordinal-BdBDtYTt.js +1 -0
  95. package/web/assets/{packet-AYTQ26CC-BTZ7CbVd.js → packet-AYTQ26CC-Unxh5mP2.js} +1 -1
  96. package/web/assets/{pegDiagram-XKGWAZYB-DNdK3axo.js → pegDiagram-XKGWAZYB-BIqcRp3m.js} +1 -1
  97. package/web/assets/{pie-WAS4IAKB-DidjfqgW.js → pie-WAS4IAKB-BipHZKbc.js} +1 -1
  98. package/web/assets/{pieDiagram-E7YTZNPT-Dme68TUA.js → pieDiagram-E7YTZNPT-gaTNuzPD.js} +2 -2
  99. package/web/assets/{quadrantDiagram-AXDQQJYC-z0xeSJSB.js → quadrantDiagram-AXDQQJYC-3BEmlbuE.js} +1 -1
  100. package/web/assets/{radar-RG4KPBEZ-BV50NO32.js → radar-RG4KPBEZ-Cm1_Q0XJ.js} +1 -1
  101. package/web/assets/{railroad-74A4TZTK-BOkd4wGS.js → railroad-74A4TZTK-CJ4K8tjT.js} +1 -1
  102. package/web/assets/railroad-abnf-HS5TGJTU-BVKI4cmM.js +1 -0
  103. package/web/assets/railroad-ebnf-LZEXJU2U-UMcDSelb.js +1 -0
  104. package/web/assets/railroad-peg-WCYAUIDC-C00zpjQU.js +1 -0
  105. package/web/assets/{railroadDiagram-O6MQD6OU-2eK1mPm_.js → railroadDiagram-O6MQD6OU-Nzyn7Xx6.js} +1 -1
  106. package/web/assets/range-EplFIxc7.js +1 -0
  107. package/web/assets/{requirementDiagram-BXWQKSXE-DvlVp433.js → requirementDiagram-BXWQKSXE-9zXuiai_.js} +1 -1
  108. package/web/assets/{sankeyDiagram-P5KCCOFB-C6CsEyCg.js → sankeyDiagram-P5KCCOFB-Cnw1rQun.js} +1 -1
  109. package/web/assets/{sequenceDiagram-WJ2MYXX4-1wwxxN6P.js → sequenceDiagram-WJ2MYXX4-BSLLRV-U.js} +3 -3
  110. package/web/assets/{settingsRoute-Dxrvnqih.js → settingsRoute-C2Up46o6.js} +1 -1
  111. package/web/assets/src-CFHfg5gg.js +1 -0
  112. package/web/assets/stateDiagram-D77RDMKH-D07b4cJB.js +1 -0
  113. package/web/assets/stateDiagram-v2-MP3YSRHH-DaG9Yzhk.js +1 -0
  114. package/web/assets/step-BDqHAzds.js +1 -0
  115. package/web/assets/{swimlanes-42K2YHIH-XLThGmTf.js → swimlanes-42K2YHIH-RpDl0CG9.js} +1 -1
  116. package/web/assets/swimlanesDiagram-VR7AAH4N-7003ldWX.js +8 -0
  117. package/web/assets/time-Dzw-YSCF.js +1 -0
  118. package/web/assets/{timeline-definition-24CTP7MA-BEOV0lDT.js → timeline-definition-24CTP7MA-BgQOP12r.js} +1 -1
  119. package/web/assets/{treeView-Q6P3EWNA-Dm4FCZVW.js → treeView-Q6P3EWNA-BiSBsrlo.js} +1 -1
  120. package/web/assets/treemap-BfjIY-m6.js +1 -0
  121. package/web/assets/{treemap-WGGIJYW6-DEZGmkE8.js → treemap-WGGIJYW6-CNdjKqUG.js} +1 -1
  122. package/web/assets/useStore-CxjHbFPA.js +16 -0
  123. package/web/assets/{vennDiagram-4TSXK5OY-CDRUEJ_c.js → vennDiagram-4TSXK5OY-DLzOr3Yv.js} +1 -1
  124. package/web/assets/{wardley-WFR3VGLG-CjZ5JazT.js → wardley-WFR3VGLG-LCC8FWVb.js} +1 -1
  125. package/web/assets/{wardleyDiagram-VM6X3IG4-COsxLnU6.js → wardleyDiagram-VM6X3IG4-WrwzM3YW.js} +1 -1
  126. package/web/assets/{xychartDiagram-S5SC5T6Z-BfwzNIP-.js → xychartDiagram-S5SC5T6Z-B_HuoAnl.js} +3 -3
  127. package/web/index.html +2 -2
  128. package/web/sw.js +1 -1
  129. package/dist/ephemeral-exec.js +0 -108
  130. package/dist/hosted-custody.js +0 -30
  131. package/web/assets/AutomationsView-Dk1aHviY.js +0 -51
  132. package/web/assets/ChangesCard-Bn37dRew.js +0 -4
  133. package/web/assets/LibraryView-CGkpNOgR.js +0 -2
  134. package/web/assets/Settings-DJjEVwmu.js +0 -10
  135. package/web/assets/arc-Pry-7G1_.js +0 -1
  136. package/web/assets/architecture-7GRP2DOG-kbFZ_7eU.js +0 -1
  137. package/web/assets/array-BifhSqXX.js +0 -1
  138. package/web/assets/channel-CpJ3OYta.js +0 -1
  139. package/web/assets/chunk-75Z2AOVW-CGlA2AB-.js +0 -2
  140. package/web/assets/chunk-F27PBJKO-Cto7X88Z.js +0 -1
  141. package/web/assets/chunk-XXDRQBXY-DqAYeljN.js +0 -1
  142. package/web/assets/classDiagram-ZZMXUADV-BSqiC-6r.js +0 -1
  143. package/web/assets/classDiagram-v2-VYDZK3BY-BSqiC-6r.js +0 -1
  144. package/web/assets/cynefinDiagram-5FMLGOSQ-B2jZZ_es.js +0 -62
  145. package/web/assets/diagram-UQ7AKVKN-D7BIW4QU.js +0 -41
  146. package/web/assets/diagram-VX7I27RA-qmkf9rFQ.js +0 -24
  147. package/web/assets/diagram-Z3DM3KII-BZf2h-T8.js +0 -24
  148. package/web/assets/dist-v5Q1xZ2K.js +0 -1
  149. package/web/assets/eventmodeling-NTZA5JFV-DHYpZdDY.js +0 -1
  150. package/web/assets/flowDiagram-HODETNUW-BdNS-1c7.js +0 -1
  151. package/web/assets/ganttDiagram-EL5Y4UJY-DGMQzJ8U.js +0 -292
  152. package/web/assets/gitGraphDiagram-WWUBYQGX-DNfbdhPa.js +0 -106
  153. package/web/assets/index-kWXUrhG8.css +0 -1
  154. package/web/assets/infoDiagram-27XIBGKW-QcpEFiaQ.js +0 -2
  155. package/web/assets/init-C-OQMol4.js +0 -1
  156. package/web/assets/line-Cq9nN3X2.js +0 -1
  157. package/web/assets/linear-SzVmGSw4.js +0 -1
  158. package/web/assets/mount-C9EzYXqA.js +0 -31
  159. package/web/assets/ordinal-BDEzSJ7C.js +0 -1
  160. package/web/assets/path-fybaL0A-.js +0 -1
  161. package/web/assets/railroad-abnf-HS5TGJTU-DiQLjR_J.js +0 -1
  162. package/web/assets/railroad-ebnf-LZEXJU2U-D4bSTzXb.js +0 -1
  163. package/web/assets/railroad-peg-WCYAUIDC-DDhl4AMh.js +0 -1
  164. package/web/assets/src-B6xuSHsQ.js +0 -1
  165. package/web/assets/stateDiagram-D77RDMKH-_fkXrLZe.js +0 -1
  166. package/web/assets/stateDiagram-v2-MP3YSRHH-Cuo-j5c1.js +0 -1
  167. package/web/assets/swimlanesDiagram-VR7AAH4N-DOpCt5KW.js +0 -8
  168. package/web/assets/useStore-C1E8iwAQ.js +0 -29
@@ -0,0 +1,429 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ //
4
+ // The grammar for things an agent places INSIDE its chat message by writing
5
+ // them in the markdown, rather than out-of-band beside it.
6
+ //
7
+ // Three parties have to agree on it, byte for byte:
8
+ //
9
+ // 1. the renderer (packages/core/src/markdown.ts) turns each reference into an
10
+ // EMPTY placeholder element carrying the reference as a data attribute;
11
+ // 2. the node (src/session/transcript-persistence.ts) finds the same
12
+ // references in the raw message, resolves each one, and stores the bytes in
13
+ // the content-addressed AttachmentStore under that same reference string;
14
+ // 3. the view (packages/web ChatView) looks the reference up in the entry's
15
+ // resolved map and hydrates the placeholder.
16
+ //
17
+ // Those three live in two packages that deliberately do not import each other
18
+ // (src/ is the node; @bivy/core is the client library). The agreement used to be
19
+ // two copies of one regex kept in lock-step by comment — which silently diverges
20
+ // the moment someone edits one. This file is the single source instead: the node
21
+ // compiles THIS file through a symlink at src/session/message-components.ts, and
22
+ // scripts/check-module-boundaries.mjs fails if the two stop being one inode.
23
+ //
24
+ // Being compiled by both builds, it imports nothing at all.
25
+ /** How many components one message may resolve. A pathological or hostile
26
+ * message must not fan out into an unbounded number of file reads / outbound
27
+ * fetches, nor bloat the event log that replicates to a phone. */
28
+ export const MAX_COMPONENTS_PER_MESSAGE = 6;
29
+ /** Longest reference we will even look at. Generous enough for a signed CDN URL
30
+ * and far past any real workspace path, but a hard bound — it is also the
31
+ * repetition limit in the pattern below, which is what keeps matching linear. */
32
+ const MAX_REF_LENGTH = 2048;
33
+ /** Longest alt text we will match. Bounded for the same reason, not because a
34
+ * caption that long means anything. */
35
+ const MAX_ALT_LENGTH = 500;
36
+ /**
37
+ * Markdown image syntax. One literal, used by every caller here, so the "keep
38
+ * these two regexes identical" hazard cannot come back.
39
+ *
40
+ * Deliberately matches ANY target, not just the ones we accept:
41
+ * `classifyImageTarget` decides, and a target it rejects is left in the message
42
+ * as the literal text the agent wrote. Matching narrowly instead would make a
43
+ * rejected image silently vanish.
44
+ *
45
+ * Returned fresh per call: a shared `g`-flagged regex carries `lastIndex`
46
+ * between calls, which is exactly the kind of cross-call state that makes two
47
+ * consumers of one module disagree.
48
+ *
49
+ * Both repetitions are BOUNDED, and must stay that way. Unbounded `[^\]]*` and
50
+ * `[^)\s]+` make matching quadratic in the message length: on input like
51
+ * `![![![…` every `!` starts a match attempt that scans to the end of the
52
+ * string. This runs on assistant message text — which a prompt injection or a
53
+ * tool result echoed into a reply can shape — on the node, at every message
54
+ * boundary. With fixed upper bounds each attempt costs at most a constant, so
55
+ * the whole scan is linear. (CodeQL js/polynomial-redos flags the unbounded
56
+ * form; it was flagging the shape this module inherited.)
57
+ */
58
+ function imagePattern() {
59
+ return new RegExp(`!\\[([^\\]]{0,${MAX_ALT_LENGTH}})\\]\\(([^)\\s]{1,${MAX_REF_LENGTH}})\\)`, "g");
60
+ }
61
+ /**
62
+ * Reverse the entity escaping `esc()` in markdown.ts applies to the whole
63
+ * message before the image rule ever runs.
64
+ *
65
+ * Without this the two sides disagree for any reference containing `&`: the
66
+ * renderer would key on `…?a=1&b=2` while the node — which scans the raw
67
+ * message text — keys on `…?a=1&b=2`, and the image never hydrates. (That
68
+ * mismatch predates this module; it is fixed here because this is now the one
69
+ * place that derives a reference key.) On already-raw text this is a no-op
70
+ * unless the text literally contains an entity, in which case both sides
71
+ * normalise it the same way and still agree.
72
+ */
73
+ export function unescapeComponentRef(target) {
74
+ return String(target || "")
75
+ .replace(/&lt;/g, "<")
76
+ .replace(/&gt;/g, ">")
77
+ .replace(/&amp;/g, "&");
78
+ }
79
+ /**
80
+ * Decide what an image target means, or `null` to refuse it.
81
+ *
82
+ * `remote` targets are fetched BY THE NODE under its SSRF checks (see
83
+ * src/session/inline-image-fetch.ts) — never by the viewer's browser.
84
+ *
85
+ * `workspace` targets are read from the session's working directory. This
86
+ * function is NOT the security boundary for them: `planAttachment()`
87
+ * (src/session/attach-to-chat.ts) is, because only it can resolve symlinks
88
+ * against the real workspace root. What this does is refuse the obviously
89
+ * hostile shapes up front, so an absolute path or a traversal never becomes a
90
+ * map key or reaches the filesystem at all. Message text is agent-authored and
91
+ * therefore reachable by prompt injection, so both layers earn their place.
92
+ */
93
+ export function classifyImageTarget(target) {
94
+ const t = String(target || "").trim();
95
+ if (!t || t.length > MAX_REF_LENGTH)
96
+ return null;
97
+ if (t.startsWith("https://"))
98
+ return "remote";
99
+ // Every other scheme is refused. `http:` would be an unencrypted fetch,
100
+ // `data:`/`blob:` smuggle bytes past the size and mime checks, `file:` reads
101
+ // the node's disk outside any workspace, and `javascript:` is self-evident.
102
+ if (/^[a-z][a-z0-9+.-]*:/i.test(t))
103
+ return null;
104
+ if (t.startsWith("//"))
105
+ return null; // protocol-relative URL
106
+ if (t.startsWith("/") || t.startsWith("~"))
107
+ return null; // absolute / home-relative
108
+ if (t.startsWith("#"))
109
+ return null; // in-page anchor
110
+ if (t.includes("\\"))
111
+ return null;
112
+ if (t.split("/").some((segment) => segment === ".."))
113
+ return null; // traversal
114
+ // eslint-disable-next-line no-control-regex
115
+ if (/[\x00-\x1f\x7f]/.test(t))
116
+ return null;
117
+ return "workspace";
118
+ }
119
+ /**
120
+ * Every image reference a message makes, de-duplicated, in first-seen order and
121
+ * capped at `limit`. Targets `classifyImageTarget` refuses are left out: they
122
+ * are not components, they are prose.
123
+ */
124
+ export function extractImageReferences(text, limit = MAX_COMPONENTS_PER_MESSAGE) {
125
+ if (!text)
126
+ return [];
127
+ const out = [];
128
+ const seen = new Set();
129
+ for (const match of String(text).matchAll(imagePattern())) {
130
+ const ref = unescapeComponentRef(match[2] ?? "");
131
+ const origin = classifyImageTarget(ref);
132
+ if (!origin || seen.has(ref))
133
+ continue;
134
+ seen.add(ref);
135
+ out.push({ ref, origin });
136
+ if (out.length >= limit)
137
+ break;
138
+ }
139
+ return out;
140
+ }
141
+ /**
142
+ * Replace each accepted image reference via `render`, leaving refused ones as
143
+ * the literal text the agent wrote. The renderer uses this so the one pattern
144
+ * above stays the only place image syntax is recognised.
145
+ */
146
+ export function replaceImageReferences(text, render) {
147
+ return String(text || "").replace(imagePattern(), (whole, alt, target) => {
148
+ const ref = unescapeComponentRef(target);
149
+ const origin = classifyImageTarget(ref);
150
+ if (!origin)
151
+ return whole;
152
+ return render({ alt: alt ?? "", ref, origin });
153
+ });
154
+ }
155
+ // ---------------------------------------------------------------------------
156
+ // Components the agent places with a directive or a spec
157
+ //
158
+ // `![alt](path)` says "show this image here" and nothing else. The two forms
159
+ // below are how an agent says anything more — show this FILE, show this CSV as a
160
+ // table — without a second out-of-band channel per kind:
161
+ //
162
+ // ::view{src=data/sales.csv} point at something in the workspace
163
+ // ```bivy … ``` carry a small spec inline
164
+ //
165
+ // Both land as the same value, a ComponentPlacement, and both render through one
166
+ // table keyed by `kind`. Adding a kind is a row in that table, not another
167
+ // branch here. Anything this file cannot make sense of stays the text the agent
168
+ // wrote, because an agent WILL get the syntax wrong and a message must still
169
+ // read.
170
+ // ---------------------------------------------------------------------------
171
+ /** The info string that marks a fenced block as a component spec. */
172
+ export const COMPONENT_FENCE_LANG = "bivy";
173
+ /** Ceiling for one inline spec. A spec rides in the message and then in every
174
+ * rendered copy of it, so it is for a handful of values — a file is the right
175
+ * home for real data. Past this the fence stays an ordinary code block. */
176
+ export const MAX_COMPONENT_SPEC_CHARS = 16 * 1024;
177
+ /** Ceiling for a directive's attribute list, bounding the parse below. */
178
+ const MAX_ATTRS_CHARS = 512;
179
+ /** An app id as the apps registry mints them. Narrow on purpose: an id is the
180
+ * only thing about an app the chat ever holds, so anything that is not plainly
181
+ * one is not treated as an app reference. */
182
+ const APP_ID = /^[\w-]{1,64}$/;
183
+ /** Leaf directives this grammar knows. `view` is the general one; a named leaf
184
+ * can be added later without touching the parser. */
185
+ const LEAF_NAMES = new Set(["view"]);
186
+ /**
187
+ * `{src=a/b.csv height=200 caption="Last quarter" #fig-1 .wide}` →
188
+ * `{ src: "a/b.csv", height: "200", caption: "Last quarter", id: "fig-1", class: "wide" }`
189
+ *
190
+ * Every repetition is bounded (see imagePattern's note — this runs on the same
191
+ * agent-authored text), and the whole list is length-capped, so a pathological
192
+ * attribute string is refused rather than parsed slowly.
193
+ */
194
+ export function parseAttrs(source) {
195
+ const out = {};
196
+ const text = String(source || "").trim().replace(/^\{|\}$/g, "");
197
+ if (!text || text.length > MAX_ATTRS_CHARS)
198
+ return out;
199
+ const re = /([#.]?[\w-]{1,64})(?:\s*=\s*(?:"([^"\n]{0,512})"|'([^'\n]{0,512})'|([^\s"'}]{1,512})))?/g;
200
+ for (const m of text.matchAll(re)) {
201
+ const key = m[1] ?? "";
202
+ const value = m[2] ?? m[3] ?? m[4];
203
+ if (key.startsWith("#"))
204
+ out.id = key.slice(1);
205
+ else if (key.startsWith("."))
206
+ out.class = out.class ? `${String(out.class)} ${key.slice(1)}` : key.slice(1);
207
+ else if (key)
208
+ out[key] = value === undefined ? true : value;
209
+ }
210
+ return out;
211
+ }
212
+ /**
213
+ * A whole line that is a leaf directive, or null. Block-level by construction:
214
+ * the directive must own its line, so a `::view{…}` written mid-sentence is
215
+ * prose and a half-streamed one cannot render until its line is complete.
216
+ *
217
+ * `src` is validated as a WORKSPACE reference. A directive is not a link — it
218
+ * names something Bivy has to read — so a remote URL is refused here even
219
+ * though `![](https://…)` accepts one.
220
+ */
221
+ export function parseComponentDirective(line) {
222
+ // Surrounding whitespace is trimmed rather than matched, leaving exactly ONE
223
+ // whitespace quantifier in the pattern. Two of them around an optional group
224
+ // (`::name[ \t]*{…}?[ \t]*$`) is ambiguous — a run of tabs can be split
225
+ // between them in as many ways as it is long, which is quadratic on a line
226
+ // like `::a` followed by whitespace, and this runs on agent-authored text.
227
+ // See imagePattern's note; the same rule applies to every pattern here.
228
+ const m = /^::([a-z][\w-]{0,31})[ \t]{0,32}(\{[^}\n]{0,512}\})?$/i.exec(String(line || "").trim());
229
+ if (!m || !LEAF_NAMES.has((m[1] ?? "").toLowerCase()))
230
+ return null;
231
+ const { src, app, ...attrs } = parseAttrs(m[2] ?? "");
232
+ // `app=<id>` names something the MACHINE already published, not a file, so it
233
+ // is not a workspace reference and the node resolves nothing for it: the id
234
+ // travels in the transcript exactly as `AppMessage` has always stored it, and
235
+ // the view layer turns it into a preview. Kept as a spec rather than a `ref`
236
+ // so nothing downstream mistakes it for a path to read.
237
+ if (typeof app === "string" && APP_ID.test(app)) {
238
+ return { ref: null, spec: { type: "app", appId: app }, attrs };
239
+ }
240
+ const ref = typeof src === "string" ? unescapeComponentRef(src) : "";
241
+ if (!ref || classifyImageTarget(ref) !== "workspace")
242
+ return null;
243
+ return { ref, spec: null, attrs };
244
+ }
245
+ /** True for the info string of a component fence (```bivy). */
246
+ export function isComponentFence(info) {
247
+ return String(info || "").trim().toLowerCase() === COMPONENT_FENCE_LANG;
248
+ }
249
+ /**
250
+ * A component fence's body as a spec object, or null to leave the fence as an
251
+ * ordinary code block — which is what an agent gets for malformed JSON, and is
252
+ * strictly better than an error: the source stays readable and copyable.
253
+ */
254
+ export function parseComponentSpec(body) {
255
+ const text = String(body || "").trim();
256
+ if (!text || text.length > MAX_COMPONENT_SPEC_CHARS)
257
+ return null;
258
+ let parsed;
259
+ try {
260
+ parsed = JSON.parse(text);
261
+ }
262
+ catch {
263
+ return null;
264
+ }
265
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
266
+ return null;
267
+ return parsed;
268
+ }
269
+ /**
270
+ * A Vega-Lite spec, recognised by its own shape rather than by a `type` an
271
+ * agent would have to remember to add. A spec written for anywhere else works
272
+ * here unchanged, which is the point of using Vega-Lite instead of a dialect
273
+ * only Bivy reads.
274
+ */
275
+ function isVegaLite(spec) {
276
+ if (typeof spec.$schema === "string" && /vega-lite/i.test(spec.$schema))
277
+ return true;
278
+ return ["mark", "layer", "hconcat", "vconcat", "concat", "facet", "repeat"].some((key) => key in spec);
279
+ }
280
+ /**
281
+ * How a component is rendered, from its spec's own `type` or from the file it
282
+ * points at. One table, so a new kind is a row here plus a row in the view
283
+ * layer's registry — never a branch in the parser.
284
+ *
285
+ * Returns a kind string even for a kind this build cannot render; the registry
286
+ * decides, and falls back visibly rather than dropping the component.
287
+ */
288
+ export function componentKind(opts) {
289
+ const declared = opts.spec?.type;
290
+ if (typeof declared === "string" && declared.trim())
291
+ return declared.trim().toLowerCase();
292
+ if (opts.spec)
293
+ return isVegaLite(opts.spec) ? "chart" : "unknown";
294
+ const mime = String(opts.mimeType || "").toLowerCase();
295
+ if (mime.startsWith("image/"))
296
+ return "image";
297
+ const ext = String(opts.path || "").toLowerCase().split(".").pop() ?? "";
298
+ if (ext === "csv" || ext === "tsv")
299
+ return "table";
300
+ // Everything else is a file the reader can open or download. Rows for richer
301
+ // readings of a file — a notebook, a diff — belong here alongside the renderer
302
+ // that can actually draw them. A row added early would promise a kind the view
303
+ // layer has to decline, turning a useful download into "this version cannot
304
+ // show that yet".
305
+ return "file";
306
+ }
307
+ /** How deep into a spec to look for references. Bounded so a pathological
308
+ * nesting cannot turn one message into an unbounded walk. */
309
+ const MAX_SPEC_DEPTH = 8;
310
+ /**
311
+ * Workspace files a SPEC references, by putting a relative path in a `url`
312
+ * field. That is Vega-Lite's `data.url`, which is how a chart points at a
313
+ * dataset rather than carrying every row inline — and the whole spec is walked
314
+ * because Vega-Lite allows one per layer, facet and lookup, not just at the top.
315
+ *
316
+ * Deliberately keyed on the field NAME, not on anything Vega-specific: this
317
+ * file must not learn one kind's schema, and "a `url` field holding a
318
+ * workspace path is a reference" is a rule any future kind can use.
319
+ *
320
+ * A `url` that is not a workspace path (an `https://` address, say) is not
321
+ * returned, and so is never resolved — the view layer refuses those outright
322
+ * rather than letting a chart make the reader's browser fetch it.
323
+ */
324
+ export function specReferences(spec, limit = MAX_COMPONENTS_PER_MESSAGE) {
325
+ const out = [];
326
+ const seen = new Set();
327
+ const walk = (value, depth) => {
328
+ if (out.length >= limit || depth > MAX_SPEC_DEPTH || !value || typeof value !== "object")
329
+ return;
330
+ if (Array.isArray(value)) {
331
+ for (const item of value)
332
+ walk(item, depth + 1);
333
+ return;
334
+ }
335
+ for (const [key, child] of Object.entries(value)) {
336
+ if (key === "url" && typeof child === "string") {
337
+ const ref = unescapeComponentRef(child);
338
+ if (classifyImageTarget(ref) === "workspace" && !seen.has(ref)) {
339
+ seen.add(ref);
340
+ out.push(ref);
341
+ if (out.length >= limit)
342
+ return;
343
+ }
344
+ continue;
345
+ }
346
+ walk(child, depth + 1);
347
+ }
348
+ };
349
+ walk(spec, 0);
350
+ return out;
351
+ }
352
+ /** Each ```bivy fence in `text`, as a parsed spec with its offset. Line-wise,
353
+ * because that is how a fence is defined; an unterminated one yields nothing,
354
+ * which is also how a half-streamed message behaves. */
355
+ function componentFences(text) {
356
+ const out = [];
357
+ const lines = String(text).split("\n");
358
+ let at = 0;
359
+ for (let i = 0; i < lines.length; i++) {
360
+ const line = lines[i] ?? "";
361
+ const start = at;
362
+ at += line.length + 1;
363
+ if (!isComponentFence(line.trim().replace(/^```+/, "")))
364
+ continue;
365
+ const body = [];
366
+ let j = i + 1;
367
+ for (; j < lines.length && !lines[j].trim().startsWith("```"); j++) {
368
+ body.push(lines[j]);
369
+ at += lines[j].length + 1;
370
+ }
371
+ if (j < lines.length)
372
+ at += lines[j].length + 1;
373
+ const spec = parseComponentSpec(body.join("\n"));
374
+ if (spec)
375
+ out.push({ spec, at: start });
376
+ i = j;
377
+ }
378
+ return out;
379
+ }
380
+ /**
381
+ * Every reference a message makes that Bivy has to resolve — from image syntax,
382
+ * from a directive, or from a `url` inside a spec — de-duplicated, in first-seen
383
+ * order, under ONE shared cap. One budget across all three forms is deliberate:
384
+ * the cost being bounded is file reads and persisted bytes, which do not care
385
+ * which syntax asked.
386
+ */
387
+ export function extractMessageReferences(text, limit = MAX_COMPONENTS_PER_MESSAGE) {
388
+ if (!text)
389
+ return [];
390
+ const source = String(text);
391
+ const found = [];
392
+ // Images are matched against the WHOLE text, not line by line: alt text may
393
+ // contain a newline (an agent wrapping a long caption at 80 columns), and the
394
+ // renderer — which sees a paragraph with its lines already joined — would
395
+ // place a mount point the node then never resolved.
396
+ for (const match of source.matchAll(imagePattern())) {
397
+ const ref = unescapeComponentRef(match[2] ?? "");
398
+ const origin = classifyImageTarget(ref);
399
+ if (origin)
400
+ found.push({ ref, origin, syntax: "image", at: match.index ?? 0 });
401
+ }
402
+ // Directives own a whole line, so they are matched per line; the running
403
+ // offset keeps them in document order with the images above.
404
+ let at = 0;
405
+ for (const line of source.split("\n")) {
406
+ const directive = parseComponentDirective(line);
407
+ if (directive?.ref)
408
+ found.push({ ref: directive.ref, origin: "workspace", syntax: "view", at });
409
+ at += line.length + 1;
410
+ }
411
+ // A spec can point at a dataset instead of carrying it (a chart's data.url).
412
+ for (const { spec, at: offset } of componentFences(source)) {
413
+ for (const ref of specReferences(spec, limit)) {
414
+ found.push({ ref, origin: "workspace", syntax: "spec", at: offset });
415
+ }
416
+ }
417
+ found.sort((a, b) => a.at - b.at);
418
+ const out = [];
419
+ const seen = new Set();
420
+ for (const { ref, origin, syntax } of found) {
421
+ if (seen.has(ref))
422
+ continue;
423
+ seen.add(ref);
424
+ out.push({ ref, origin, syntax });
425
+ if (out.length >= limit)
426
+ break;
427
+ }
428
+ return out;
429
+ }
@@ -146,7 +146,7 @@ export function createRunTerminals(deps) {
146
146
  deps.broadcast({ type: "terminal.closed", termId });
147
147
  }
148
148
  async function runTerminalList() {
149
- const runs = terminals.list((meta) => meta.kind === "run").filter((t) => runTerminals.has(t.id));
149
+ const runs = terminals.list((meta) => meta.kind === "run" && !meta.background).filter((t) => runTerminals.has(t.id));
150
150
  let saved = [];
151
151
  if (runs.some((t) => t.meta.autoName)) {
152
152
  try {
@@ -294,7 +294,8 @@ export function createRunTerminals(deps) {
294
294
  const commandLine = [spec.command, ...spec.args].join(" ");
295
295
  const explicitName = spec.name?.trim();
296
296
  const name = explicitName || (spec.mux ? undefined : defaultRunName(spec.agent, commandLine, workspace));
297
- const notifiable = !spec.mux;
297
+ // A mux attach or an app's own program is not a session of its own.
298
+ const isSession = !spec.mux && !spec.background;
298
299
  const createdAt = Date.now();
299
300
  // The agent inside gets the same session env a chat session's agent does, so
300
301
  // `bivy notify`/`ask`/`app` reach this run: under its pinned session id, else
@@ -312,7 +313,7 @@ export function createRunTerminals(deps) {
312
313
  cols: spec.cols,
313
314
  rows: spec.rows,
314
315
  clientId: spec.clientId,
315
- meta: { kind: "run", agent: spec.agent, model: spec.model, label, name, autoName: !explicitName && !spec.mux, command: commandLine, mux: spec.mux, sessionId: spec.sessionId },
316
+ meta: { kind: "run", agent: spec.agent, model: spec.model, label, name, autoName: !explicitName && !spec.mux, command: commandLine, mux: spec.mux, sessionId: spec.sessionId, background: spec.background },
316
317
  onData: (data) => {
317
318
  emitRunOutput(id, data);
318
319
  const at = Date.now();
@@ -320,7 +321,7 @@ export function createRunTerminals(deps) {
320
321
  lastActivityPing.set(id, at);
321
322
  deps.broadcast({ type: "terminal.activity", termId: id, at });
322
323
  }
323
- if (notifiable)
324
+ if (isSession)
324
325
  armRunIdleNotify(id, name);
325
326
  },
326
327
  onExit: (code, _signal, scrollback) => {
@@ -341,7 +342,7 @@ export function createRunTerminals(deps) {
341
342
  catch { /* best-effort */ }
342
343
  return;
343
344
  }
344
- if (!spec.mux) {
345
+ if (isSession) {
345
346
  // The durable record of this run, in order of fidelity: the agent's
346
347
  // own session (pinned at launch), the session discovered in the
347
348
  // agent's store, else — for any agent at all — the terminal
@@ -383,7 +384,7 @@ export function createRunTerminals(deps) {
383
384
  catch { /* best-effort */ }
384
385
  })();
385
386
  }
386
- if (!spec.mux) {
387
+ if (isSession) {
387
388
  void ingestAgentCredentials(spec.agent, deps.credsDir, deps.piDir)
388
389
  .then(async (imported) => {
389
390
  if (imported > 0) {
@@ -406,7 +407,8 @@ export function createRunTerminals(deps) {
406
407
  catch { /* best-effort: a metadata write must never block the run launch */ }
407
408
  }
408
409
  emit({ type: "terminal.opened", termId: id, workspace, mode: "run", agent: spec.agent, label, name });
409
- deps.broadcast({ type: "terminal.created", terminal: { termId: id, workspace, createdAt, lastActivityAt: createdAt, kind: "run", agent: spec.agent, model: spec.model, label, name, command: commandLine, mux: spec.mux, sessionId: spec.sessionId, pid: terminals.pid(id) } });
410
+ if (!spec.background)
411
+ deps.broadcast({ type: "terminal.created", terminal: { termId: id, workspace, createdAt, lastActivityAt: createdAt, kind: "run", agent: spec.agent, model: spec.model, label, name, command: commandLine, mux: spec.mux, sessionId: spec.sessionId, pid: terminals.pid(id) } });
410
412
  // A pinned run is now a durable session too: push the authoritative list
411
413
  // (status "working" while this PTY lives) and re-advertise, so clients on
412
414
  // other nodes — which never see this node's terminal.created — still get