@bivy/bivy 0.20.9-staging.20 → 0.20.9-staging.21

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 (111) hide show
  1. package/dist/agent-instructions.js +5 -3
  2. package/dist/apps/styles.css +15 -0
  3. package/dist/server.js +1 -1
  4. package/dist/session/inline-image-fetch.js +8 -5
  5. package/dist/session/message-components.js +169 -0
  6. package/dist/session/transcript-persistence.js +39 -20
  7. package/package.json +1 -1
  8. package/web/assets/AutomationsView-Cobo4Kep.js +51 -0
  9. package/web/assets/{ChangesCard-z3_ukf1u.js → ChangesCard-Bo6E8C_y.js} +3 -3
  10. package/web/assets/LibraryView-BTeMc6BO.js +2 -0
  11. package/web/assets/{ProviderConnect-B3eBVETd.js → ProviderConnect-CsidS7kq.js} +1 -1
  12. package/web/assets/SessionList-B7IbeXSa.js +1 -0
  13. package/web/assets/{Settings-BNot7mbn.js → Settings-BQRQsQ36.js} +5 -5
  14. package/web/assets/{Sheet-D6GzuZLS.js → Sheet-C0BIPbMr.js} +1 -1
  15. package/web/assets/{Terminal-BsC5uIMV.js → Terminal-BTRgonB5.js} +1 -1
  16. package/web/assets/{VoiceSettings-ls5IroKf.js → VoiceSettings-UZ1SA0lQ.js} +1 -1
  17. package/web/assets/{abnfDiagram-VCTEODGH-DbgBwr4N.js → abnfDiagram-VCTEODGH-CJoWsvVk.js} +1 -1
  18. package/web/assets/architecture-7GRP2DOG-CuzcodTO.js +1 -0
  19. package/web/assets/{architectureDiagram-5GKGNRK7-Bsmv18_k.js → architectureDiagram-5GKGNRK7-_qpa4O1v.js} +1 -1
  20. package/web/assets/{blockDiagram-I7D4REHJ-qHhTUAop.js → blockDiagram-I7D4REHJ-DmLgwSr-.js} +1 -1
  21. package/web/assets/{c4Diagram-7LVT6UL2-2zjJl4E9.js → c4Diagram-7LVT6UL2-DeZuOOSk.js} +1 -1
  22. package/web/assets/channel-B0OL-aPd.js +1 -0
  23. package/web/assets/{chunk-4HAMMTFA-BNe2pfM4.js → chunk-4HAMMTFA-Cr6O6lBG.js} +1 -1
  24. package/web/assets/{chunk-75Z2AOVW-CKl0NV3C.js → chunk-75Z2AOVW-gSgNIYMY.js} +1 -1
  25. package/web/assets/{chunk-DU6HZSFF-B3P59NKh.js → chunk-DU6HZSFF-BacnRf2Y.js} +1 -1
  26. package/web/assets/{chunk-F27PBJKO-CAWJsp2_.js → chunk-F27PBJKO-DRrpfF1z.js} +1 -1
  27. package/web/assets/{chunk-GMAD6QVW-BlDnxBjY.js → chunk-GMAD6QVW-DXCi6v6B.js} +1 -1
  28. package/web/assets/{chunk-GVQU2GXP-Bczr5jKx.js → chunk-GVQU2GXP-Yd74hIeT.js} +1 -1
  29. package/web/assets/{chunk-IMKFNOWR-CWeRsdUj.js → chunk-IMKFNOWR-D5xz0-QC.js} +1 -1
  30. package/web/assets/{chunk-L3NEJ4N5-BQPTEPpn.js → chunk-L3NEJ4N5-BrS0CGPM.js} +1 -1
  31. package/web/assets/{chunk-OSK3NFVY-DzGYbGz2.js → chunk-OSK3NFVY-C6d1DSJe.js} +1 -1
  32. package/web/assets/{chunk-P2QGCYS3-CSvykgBa.js → chunk-P2QGCYS3-BrKgIngm.js} +1 -1
  33. package/web/assets/{chunk-POPQ4Y6H-CSd2VGfh.js → chunk-POPQ4Y6H-Bkwx5BZs.js} +1 -1
  34. package/web/assets/{chunk-PWAF6VOD-DR2HyrI3.js → chunk-PWAF6VOD-AeCCeE0F.js} +1 -1
  35. package/web/assets/{chunk-SHT3W25Y-CSMOeLrh.js → chunk-SHT3W25Y-01NYjozy.js} +1 -1
  36. package/web/assets/{chunk-SVP7TREG-BTIkXcEs.js → chunk-SVP7TREG-CCh7wXAI.js} +1 -1
  37. package/web/assets/{chunk-TICWLB2K-DS3eZA5z.js → chunk-TICWLB2K-CcWKqwdM.js} +1 -1
  38. package/web/assets/classDiagram-ZZMXUADV-D8dSdFQt.js +1 -0
  39. package/web/assets/classDiagram-v2-VYDZK3BY-D8dSdFQt.js +1 -0
  40. package/web/assets/{cynefin-OW5HDTMX-qmnX5Drm.js → cynefin-OW5HDTMX-CYYc_tBE.js} +1 -1
  41. package/web/assets/{cynefinDiagram-5FMLGOSQ-Cuh1jGM6.js → cynefinDiagram-5FMLGOSQ-Dnz7OfxK.js} +1 -1
  42. package/web/assets/{dagre-GXQ25YYZ-7GZbtRVA.js → dagre-GXQ25YYZ-di5CyaMt.js} +1 -1
  43. package/web/assets/{diagram-S7CK7UJ4-zQJ-kaiE.js → diagram-S7CK7UJ4-BVC93OLB.js} +1 -1
  44. package/web/assets/{diagram-UQ7AKVKN-B5Ed0jaZ.js → diagram-UQ7AKVKN-D7pXHAvQ.js} +1 -1
  45. package/web/assets/{diagram-VSXAHHWV-D5vZvTR7.js → diagram-VSXAHHWV-d0zW-IUz.js} +1 -1
  46. package/web/assets/{diagram-VX7I27RA-DEkORtmo.js → diagram-VX7I27RA-BRlMo9oa.js} +1 -1
  47. package/web/assets/{diagram-Z3DM3KII-DPb9NWrB.js → diagram-Z3DM3KII-BmfntB1i.js} +1 -1
  48. package/web/assets/{ebnfDiagram-PWID7BFC-gVpp4zKg.js → ebnfDiagram-PWID7BFC-BxUYvqsz.js} +1 -1
  49. package/web/assets/{erDiagram-RLTQ6QDP-4YNUcQlu.js → erDiagram-RLTQ6QDP-CbUl1Cqa.js} +1 -1
  50. package/web/assets/eventmodeling-NTZA5JFV-sTVSUyNU.js +1 -0
  51. package/web/assets/flowDiagram-HODETNUW-Beo3zBEU.js +1 -0
  52. package/web/assets/{ganttDiagram-EL5Y4UJY-DUk_l5ka.js → ganttDiagram-EL5Y4UJY-CkpR0avQ.js} +1 -1
  53. package/web/assets/{gitGraph-4MIJSDKK-CGS2CP5_.js → gitGraph-4MIJSDKK-CmbJxSaM.js} +1 -1
  54. package/web/assets/{gitGraphDiagram-WWUBYQGX-C7rXPKT3.js → gitGraphDiagram-WWUBYQGX-iyO408KF.js} +1 -1
  55. package/web/assets/index-B4HaCD-n.css +1 -0
  56. package/web/assets/{index-D_E_e1lg.js → index-fdEPLcPs.js} +2 -2
  57. package/web/assets/{info-A6RAGUB7-D2F_gpvF.js → info-A6RAGUB7--6JCM0xX.js} +1 -1
  58. package/web/assets/{infoDiagram-27XIBGKW-DdRRRHRV.js → infoDiagram-27XIBGKW-Dchx6QcB.js} +1 -1
  59. package/web/assets/{ishikawaDiagram-5VMMS53U-Co_J8u-p.js → ishikawaDiagram-5VMMS53U-DOY3izUi.js} +1 -1
  60. package/web/assets/{journeyDiagram-3NMN7TZE-CrJ7j8qZ.js → journeyDiagram-3NMN7TZE-oqZgC9oJ.js} +1 -1
  61. package/web/assets/{kanban-definition-UXKFOSKX-Csa-Hm-V.js → kanban-definition-UXKFOSKX-HCRC0D9X.js} +1 -1
  62. package/web/assets/{line-CWxnZANW.js → line-qcY2eok7.js} +1 -1
  63. package/web/assets/{mermaid-parser.core-DDL03Yeh.js → mermaid-parser.core-Bullp1xl.js} +3 -3
  64. package/web/assets/{mermaid.core-CBFjpUnO.js → mermaid.core-FBmsAtMC.js} +4 -4
  65. package/web/assets/{mindmap-definition-YA3MSWOX-BIwMlu7Q.js → mindmap-definition-YA3MSWOX-CfgOAkVQ.js} +1 -1
  66. package/web/assets/mount-IY4-2elJ.js +31 -0
  67. package/web/assets/{packet-AYTQ26CC-ClFWscdE.js → packet-AYTQ26CC-COAmiopo.js} +1 -1
  68. package/web/assets/{pegDiagram-XKGWAZYB-Css6F3UD.js → pegDiagram-XKGWAZYB-BYafVbeG.js} +1 -1
  69. package/web/assets/{pie-WAS4IAKB-7l7k0PF3.js → pie-WAS4IAKB-ZYe346n6.js} +1 -1
  70. package/web/assets/{pieDiagram-E7YTZNPT-8IXxZkpM.js → pieDiagram-E7YTZNPT-DnRbaA7i.js} +1 -1
  71. package/web/assets/{quadrantDiagram-AXDQQJYC-lAd65vKY.js → quadrantDiagram-AXDQQJYC-B2uxyKLd.js} +1 -1
  72. package/web/assets/{radar-RG4KPBEZ-CXOQDEjR.js → radar-RG4KPBEZ-DwFlqYgB.js} +1 -1
  73. package/web/assets/{railroad-74A4TZTK-DyAhTgOw.js → railroad-74A4TZTK-DBv5jh9o.js} +1 -1
  74. package/web/assets/railroad-abnf-HS5TGJTU-VQxaaUVU.js +1 -0
  75. package/web/assets/railroad-ebnf-LZEXJU2U-DWKO1s90.js +1 -0
  76. package/web/assets/railroad-peg-WCYAUIDC-DvViPquF.js +1 -0
  77. package/web/assets/{railroadDiagram-O6MQD6OU-B4xtr-GL.js → railroadDiagram-O6MQD6OU-CIUFwDWd.js} +1 -1
  78. package/web/assets/{requirementDiagram-BXWQKSXE-ZEB-t5Qu.js → requirementDiagram-BXWQKSXE-BXwDlf9Q.js} +1 -1
  79. package/web/assets/{sankeyDiagram-P5KCCOFB-QivhI6_D.js → sankeyDiagram-P5KCCOFB-DTiRcy5E.js} +1 -1
  80. package/web/assets/{sequenceDiagram-WJ2MYXX4-DwJybUss.js → sequenceDiagram-WJ2MYXX4-DFPRDr4a.js} +1 -1
  81. package/web/assets/{settingsRoute-DIPLWa9I.js → settingsRoute-3489vHS0.js} +1 -1
  82. package/web/assets/{stateDiagram-D77RDMKH-CZKza48A.js → stateDiagram-D77RDMKH-CFQ-4xlY.js} +1 -1
  83. package/web/assets/stateDiagram-v2-MP3YSRHH-DIIQkE2r.js +1 -0
  84. package/web/assets/{swimlanes-42K2YHIH-BuSVwpr4.js → swimlanes-42K2YHIH-Bke6VXs0.js} +1 -1
  85. package/web/assets/swimlanesDiagram-VR7AAH4N-Drds9YUy.js +8 -0
  86. package/web/assets/{timeline-definition-24CTP7MA-D5gldQJo.js → timeline-definition-24CTP7MA-Cs2-dpWN.js} +1 -1
  87. package/web/assets/{treeView-Q6P3EWNA-DE5s4Bjg.js → treeView-Q6P3EWNA-Dr8vsD3U.js} +1 -1
  88. package/web/assets/{treemap-WGGIJYW6-B3IETKms.js → treemap-WGGIJYW6-DbvC3nsA.js} +1 -1
  89. package/web/assets/useStore-BDEjghiw.js +23 -0
  90. package/web/assets/{vennDiagram-4TSXK5OY-9OQdQ7F9.js → vennDiagram-4TSXK5OY-CdUoLWPo.js} +1 -1
  91. package/web/assets/{wardley-WFR3VGLG-CSVfZgVH.js → wardley-WFR3VGLG-BL7j6fb-.js} +1 -1
  92. package/web/assets/{wardleyDiagram-VM6X3IG4-BmOudRh6.js → wardleyDiagram-VM6X3IG4-BEVYmHWI.js} +1 -1
  93. package/web/assets/{xychartDiagram-S5SC5T6Z-CqyBA2yY.js → xychartDiagram-S5SC5T6Z-C9zn_1JB.js} +1 -1
  94. package/web/index.html +2 -2
  95. package/web/sw.js +1 -1
  96. package/web/assets/AutomationsView-CKGbGbUJ.js +0 -51
  97. package/web/assets/LibraryView-De-EIx0E.js +0 -2
  98. package/web/assets/architecture-7GRP2DOG-BbwsPIjP.js +0 -1
  99. package/web/assets/channel-Cb8DRt8H.js +0 -1
  100. package/web/assets/classDiagram-ZZMXUADV-BfpJx-fl.js +0 -1
  101. package/web/assets/classDiagram-v2-VYDZK3BY-BfpJx-fl.js +0 -1
  102. package/web/assets/eventmodeling-NTZA5JFV-D6hImpfS.js +0 -1
  103. package/web/assets/flowDiagram-HODETNUW-Cm1vFAOu.js +0 -1
  104. package/web/assets/index-Cxa5QrGG.css +0 -1
  105. package/web/assets/mount-Co7Zfyst.js +0 -31
  106. package/web/assets/railroad-abnf-HS5TGJTU-D9nETWcm.js +0 -1
  107. package/web/assets/railroad-ebnf-LZEXJU2U-C9tk_YfZ.js +0 -1
  108. package/web/assets/railroad-peg-WCYAUIDC-DKIluUM5.js +0 -1
  109. package/web/assets/stateDiagram-v2-MP3YSRHH-D0QqHEXP.js +0 -1
  110. package/web/assets/swimlanesDiagram-VR7AAH4N-BGaunElN.js +0 -8
  111. package/web/assets/useStore-DcyF6rrO.js +0 -22
@@ -33,9 +33,11 @@ export const BIVY_AGENT_NOTE = [
33
33
  "- Show an IMAGE (screenshot, chart, diagram) inside your reply: write it as markdown, " +
34
34
  "`![short caption](relative/path.png)`, where you want it to appear. The path must be inside the session " +
35
35
  "workspace; a remote `https://` image works too. No tool call needed.",
36
- "- Send a FILE, or an image on its own (a report, a download they asked for): run " +
37
- '`bivy attach <path> [--caption "short note"]`, or call the `attach_to_chat` tool if you have it. Same ' +
38
- "workspace-only rule; files arrive as downloads.",
36
+ "- Show a FILE (a report, a log, data they asked for) inside your reply: put " +
37
+ '`::view{src=relative/path.pdf caption="what it is"}` on its own line, where you want it. It arrives as a ' +
38
+ "download. Same workspace-only rule.",
39
+ "- Send a file WITHOUT writing a reply (or from a script): run " +
40
+ '`bivy attach <path> [--caption "short note"]`, or call the `attach_to_chat` tool if you have it.',
39
41
  "- Something with a UI: `bivy app publish <manifest.json>` (a web server's port, a static build, a terminal) or " +
40
42
  "`bivy app run -- <command>` (a desktop app) gives the user a live preview; `bivy app shot` screenshots it so you " +
41
43
  "can check your work; `bivy app present` tells the user a visible change is ready to look at, and " +
@@ -639,6 +639,21 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
639
639
  .msg.assistant .mermaid-diagram:first-child { margin-top: 0; }
640
640
  .msg.assistant .mermaid-diagram:last-child { margin-bottom: 0; }
641
641
  .msg.assistant .mermaid-diagram svg { display: inline-block; max-width: none; height: auto; vertical-align: top; }
642
+ /* Mount point for a component the agent placed in its message (::view{…}, a
643
+ ```bivy fence). Layout only: what renders inside is an existing component
644
+ (.msg-attachment, .card), so this must not restyle its shell — it only gives
645
+ it the same vertical rhythm as a paragraph. Empty until the portal mounts,
646
+ and empty forever if a reference never resolves, so it carries no border or
647
+ background of its own that would leave a visible hole. */
648
+ .msg.assistant .md-component { margin: 0.75em 0; }
649
+ .msg.assistant .md-component:first-child { margin-top: 0; }
650
+ .msg.assistant .md-component:last-child { margin-bottom: 0; }
651
+ .msg.assistant .md-component:empty { margin: 0; }
652
+ /* A placed component is a block in the reading column, not a tile in a row: it
653
+ fills the message width so every kind lines up with every other one and with
654
+ the prose around it. The fixed chip width belongs to the composer's wrapping
655
+ attachment list, where several chips share a line. */
656
+ .msg.assistant .md-component > * { width: 100%; }
642
657
  /* Syntax highlighting for fenced code blocks (highlight.js token classes).
643
658
  Colors use light-dark() so one rule set tracks the app's light/dark scheme
644
659
  (color-scheme is set on :root). A GitHub-flavored palette that sits well on
package/dist/server.js CHANGED
@@ -8170,7 +8170,7 @@ function attachSessionListeners(record) {
8170
8170
  // resolvers are internally deduped, so checking twice costs nothing.
8171
8171
  if (event.type === "message_end" || event.type === "turn_end") {
8172
8172
  transcripts.resolveInlineImages(record);
8173
- transcripts.resolveWorkspaceImages(record, harnessDirFor(record));
8173
+ transcripts.resolveWorkspaceRefs(record, harnessDirFor(record));
8174
8174
  }
8175
8175
  // Durably persist the throttled sidecars at the turn boundary so a crash
8176
8176
  // loses at most the in-flight turn's UI detail, not the whole turn. A
@@ -19,7 +19,7 @@
19
19
  import dns from "node:dns/promises";
20
20
  import { hostnameIsLocal } from "../auth.js";
21
21
  import { sanitizeAttachmentName, sniffMime } from "./attach-to-chat.js";
22
- import { extractImageReferences, MAX_COMPONENTS_PER_MESSAGE } from "./message-components.js";
22
+ import { extractImageReferences, extractMessageReferences, MAX_COMPONENTS_PER_MESSAGE } from "./message-components.js";
23
23
  /** Bound how many distinct images a single message can trigger a resolve for —
24
24
  * a pathological/malicious message can't fan out into an unbounded number of
25
25
  * outbound requests or file reads. Re-exported from the shared grammar, which
@@ -41,10 +41,13 @@ const MAX_REDIRECTS = 5;
41
41
  export function extractInlineImageUrls(text) {
42
42
  return extractImageReferences(text).filter((i) => i.origin === "remote").map((i) => i.ref);
43
43
  }
44
- /** The distinct workspace-relative paths a message's raw markdown references,
45
- * in first-seen order, under the same per-message cap. */
46
- export function extractWorkspaceImagePaths(text) {
47
- return extractImageReferences(text).filter((i) => i.origin === "workspace").map((i) => i.ref);
44
+ /** The distinct workspace references a message makes — from image syntax or a
45
+ * `::view{src=…}` directive — in first-seen order, under the same per-message
46
+ * cap. `syntax` tells the resolver whether non-image bytes are acceptable. */
47
+ export function extractWorkspaceRefs(text) {
48
+ return extractMessageReferences(text)
49
+ .filter((r) => r.origin === "workspace")
50
+ .map(({ ref, syntax }) => ({ ref, syntax }));
48
51
  }
49
52
  /** Best-effort plain text for an assistant RuntimeMessage's `content`, which is
50
53
  * either a plain string or an array of typed blocks (`{type:"text", text}`
@@ -152,3 +152,172 @@ export function replaceImageReferences(text, render) {
152
152
  return render({ alt: alt ?? "", ref, origin });
153
153
  });
154
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
+ /** Leaf directives this grammar knows. `view` is the general one; a named leaf
180
+ * can be added later without touching the parser. */
181
+ const LEAF_NAMES = new Set(["view"]);
182
+ /**
183
+ * `{src=a/b.csv height=200 caption="Last quarter" #fig-1 .wide}` →
184
+ * `{ src: "a/b.csv", height: "200", caption: "Last quarter", id: "fig-1", class: "wide" }`
185
+ *
186
+ * Every repetition is bounded (see imagePattern's note — this runs on the same
187
+ * agent-authored text), and the whole list is length-capped, so a pathological
188
+ * attribute string is refused rather than parsed slowly.
189
+ */
190
+ export function parseAttrs(source) {
191
+ const out = {};
192
+ const text = String(source || "").trim().replace(/^\{|\}$/g, "");
193
+ if (!text || text.length > MAX_ATTRS_CHARS)
194
+ return out;
195
+ const re = /([#.]?[\w-]{1,64})(?:\s*=\s*(?:"([^"\n]{0,512})"|'([^'\n]{0,512})'|([^\s"'}]{1,512})))?/g;
196
+ for (const m of text.matchAll(re)) {
197
+ const key = m[1] ?? "";
198
+ const value = m[2] ?? m[3] ?? m[4];
199
+ if (key.startsWith("#"))
200
+ out.id = key.slice(1);
201
+ else if (key.startsWith("."))
202
+ out.class = out.class ? `${String(out.class)} ${key.slice(1)}` : key.slice(1);
203
+ else if (key)
204
+ out[key] = value === undefined ? true : value;
205
+ }
206
+ return out;
207
+ }
208
+ /**
209
+ * A whole line that is a leaf directive, or null. Block-level by construction:
210
+ * the directive must own its line, so a `::view{…}` written mid-sentence is
211
+ * prose and a half-streamed one cannot render until its line is complete.
212
+ *
213
+ * `src` is validated as a WORKSPACE reference. A directive is not a link — it
214
+ * names something Bivy has to read — so a remote URL is refused here even
215
+ * though `![](https://…)` accepts one.
216
+ */
217
+ export function parseComponentDirective(line) {
218
+ // Surrounding whitespace is trimmed rather than matched, leaving exactly ONE
219
+ // whitespace quantifier in the pattern. Two of them around an optional group
220
+ // (`::name[ \t]*{…}?[ \t]*$`) is ambiguous — a run of tabs can be split
221
+ // between them in as many ways as it is long, which is quadratic on a line
222
+ // like `::a` followed by whitespace, and this runs on agent-authored text.
223
+ // See imagePattern's note; the same rule applies to every pattern here.
224
+ const m = /^::([a-z][\w-]{0,31})[ \t]{0,32}(\{[^}\n]{0,512}\})?$/i.exec(String(line || "").trim());
225
+ if (!m || !LEAF_NAMES.has((m[1] ?? "").toLowerCase()))
226
+ return null;
227
+ const { src, ...attrs } = parseAttrs(m[2] ?? "");
228
+ const ref = typeof src === "string" ? unescapeComponentRef(src) : "";
229
+ if (!ref || classifyImageTarget(ref) !== "workspace")
230
+ return null;
231
+ return { ref, spec: null, attrs };
232
+ }
233
+ /** True for the info string of a component fence (```bivy). */
234
+ export function isComponentFence(info) {
235
+ return String(info || "").trim().toLowerCase() === COMPONENT_FENCE_LANG;
236
+ }
237
+ /**
238
+ * A component fence's body as a spec object, or null to leave the fence as an
239
+ * ordinary code block — which is what an agent gets for malformed JSON, and is
240
+ * strictly better than an error: the source stays readable and copyable.
241
+ */
242
+ export function parseComponentSpec(body) {
243
+ const text = String(body || "").trim();
244
+ if (!text || text.length > MAX_COMPONENT_SPEC_CHARS)
245
+ return null;
246
+ let parsed;
247
+ try {
248
+ parsed = JSON.parse(text);
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
254
+ return null;
255
+ return parsed;
256
+ }
257
+ /**
258
+ * How a component is rendered, from its spec's own `type` or from the file it
259
+ * points at. One table, so a new kind is a row here plus a row in the view
260
+ * layer's registry — never a branch in the parser.
261
+ *
262
+ * Returns a kind string even for a kind this build cannot render; the registry
263
+ * decides, and falls back visibly rather than dropping the component.
264
+ */
265
+ export function componentKind(opts) {
266
+ const declared = opts.spec?.type;
267
+ if (typeof declared === "string" && declared.trim())
268
+ return declared.trim().toLowerCase();
269
+ if (opts.spec)
270
+ return "unknown";
271
+ const mime = String(opts.mimeType || "").toLowerCase();
272
+ if (mime.startsWith("image/"))
273
+ return "image";
274
+ // Everything else is a file the reader can open or download. Rows for richer
275
+ // readings of a file — a CSV as a table, a notebook, a diff — belong here
276
+ // alongside the renderer that can actually draw them. A row added early would
277
+ // promise a kind the view layer has to decline, turning a useful download into
278
+ // "this version cannot show that yet".
279
+ return "file";
280
+ }
281
+ /**
282
+ * Every reference a message makes that Bivy has to resolve — from image syntax
283
+ * or from a directive — de-duplicated, in first-seen order, under ONE shared
284
+ * cap. One budget across both forms is deliberate: the cost being bounded is
285
+ * file reads and persisted bytes, which do not care which syntax asked.
286
+ */
287
+ export function extractMessageReferences(text, limit = MAX_COMPONENTS_PER_MESSAGE) {
288
+ if (!text)
289
+ return [];
290
+ const source = String(text);
291
+ const found = [];
292
+ // Images are matched against the WHOLE text, not line by line: alt text may
293
+ // contain a newline (an agent wrapping a long caption at 80 columns), and the
294
+ // renderer — which sees a paragraph with its lines already joined — would
295
+ // place a mount point the node then never resolved.
296
+ for (const match of source.matchAll(imagePattern())) {
297
+ const ref = unescapeComponentRef(match[2] ?? "");
298
+ const origin = classifyImageTarget(ref);
299
+ if (origin)
300
+ found.push({ ref, origin, syntax: "image", at: match.index ?? 0 });
301
+ }
302
+ // Directives own a whole line, so they are matched per line; the running
303
+ // offset keeps them in document order with the images above.
304
+ let at = 0;
305
+ for (const line of source.split("\n")) {
306
+ const directive = parseComponentDirective(line);
307
+ if (directive?.ref)
308
+ found.push({ ref: directive.ref, origin: "workspace", syntax: "view", at });
309
+ at += line.length + 1;
310
+ }
311
+ found.sort((a, b) => a.at - b.at);
312
+ const out = [];
313
+ const seen = new Set();
314
+ for (const { ref, origin, syntax } of found) {
315
+ if (seen.has(ref))
316
+ continue;
317
+ seen.add(ref);
318
+ out.push({ ref, origin, syntax });
319
+ if (out.length >= limit)
320
+ break;
321
+ }
322
+ return out;
323
+ }
@@ -17,7 +17,7 @@ import { createClientProjection } from "./client-projection.js";
17
17
  import { randomBytes } from "node:crypto";
18
18
  import { mergeBases } from "./event-log.js";
19
19
  import { thinkingTextFromContent } from "./transcript-merge.js";
20
- import { extractInlineImageUrls, extractWorkspaceImagePaths, fetchInlineImage, isFetchImageError, inlineImageDisplayName, assistantTextForImageScan } from "./inline-image-fetch.js";
20
+ import { extractInlineImageUrls, extractWorkspaceRefs, fetchInlineImage, isFetchImageError, inlineImageDisplayName, assistantTextForImageScan } from "./inline-image-fetch.js";
21
21
  import { planAttachment, isAttachPlanError } from "./attach-to-chat.js";
22
22
  import { historyDelta } from "../history-sync.js";
23
23
  import { withToolDetails } from "../runtime/tool-call-map.js";
@@ -73,12 +73,12 @@ function thinkingTextFromEvent(event) {
73
73
  return "";
74
74
  }
75
75
  const INLINE_IMAGE_RETRY_COOLDOWN_MS = 10 * 60 * 1000;
76
- /** Ceiling for one workspace image copied into the transcript. Matches the
76
+ /** Ceiling for one workspace file copied into the transcript. Matches the
77
77
  * remote cap (MAX_INLINE_IMAGE_BYTES) rather than the larger explicit-attach
78
78
  * one: a `![…](path)` is incidental illustration the agent spent no tool call
79
79
  * on, and every byte is persisted in the event log and replicated to phones. An
80
80
  * agent that means to send something big still has `bivy attach`. */
81
- const MAX_WORKSPACE_IMAGE_BYTES = 8 * 1024 * 1024;
81
+ const MAX_WORKSPACE_REF_BYTES = 8 * 1024 * 1024;
82
82
  export function createTranscriptPersistence(deps) {
83
83
  const { eventLog, attachmentStore } = deps;
84
84
  const project = createClientProjection(attachmentStore, eventLog);
@@ -112,11 +112,28 @@ export function createTranscriptPersistence(deps) {
112
112
  }
113
113
  return refs;
114
114
  }
115
+ /** The same, for workspace references, keeping which syntax asked for each.
116
+ * First mention wins: a path written as an image and as a directive is one
117
+ * file read, and the stricter image rule applies to it. */
118
+ function workspaceRefsInTranscript(record) {
119
+ const refs = new Map();
120
+ for (const m of record.session.getMessages()) {
121
+ if (m.role !== "assistant")
122
+ continue;
123
+ for (const { ref, syntax } of extractWorkspaceRefs(assistantTextForImageScan(m.content))) {
124
+ if (!refs.has(ref))
125
+ refs.set(ref, syntax);
126
+ }
127
+ }
128
+ return refs;
129
+ }
115
130
  /** Store resolved bytes under `ref` and tell every client, durably and live.
116
131
  * One place, so the remote and workspace resolvers below cannot drift in how
117
- * they record what they found. */
118
- function recordResolvedImage(sessionId, ref, bytes, meta) {
119
- const stored = attachmentStore.put(bytes, { name: meta.name, mimeType: meta.mimeType, kind: "image" });
132
+ * they record what they found. `kind` carries whether the bytes are an image,
133
+ * which is what lets the view layer pick a renderer for a `::view` that
134
+ * points at something other than a picture. */
135
+ function recordResolvedRef(sessionId, ref, bytes, meta) {
136
+ const stored = attachmentStore.put(bytes, { name: meta.name, mimeType: meta.mimeType, kind: meta.kind ?? "image" });
120
137
  eventLog.appendInlineImage(sessionId, { url: ref, ref: stored });
121
138
  eventLog.flush(sessionId);
122
139
  deps.broadcast(deps.stampSessionEvent({ type: "session.event", sessionId, event: { type: "inlineImage", url: ref, ref: stored } }));
@@ -143,37 +160,39 @@ export function createTranscriptPersistence(deps) {
143
160
  * Distinct paths are the workaround until componentRefs carry a per-message
144
161
  * anchor the way outbound attachments already do.
145
162
  */
146
- function resolveWorkspaceImages(record, workspaceDir) {
163
+ function resolveWorkspaceRefs(record, workspaceDir) {
147
164
  if (!workspaceDir)
148
165
  return;
149
- const paths = imageRefsInTranscript(record, extractWorkspaceImagePaths);
150
- if (!paths.size)
166
+ const refs = workspaceRefsInTranscript(record);
167
+ if (!refs.size)
151
168
  return;
152
169
  const alreadyResolved = new Set(eventLog.readInlineImages(record.id).map(([ref]) => ref));
153
- for (const rel of paths) {
170
+ for (const [rel, syntax] of refs) {
154
171
  if (alreadyResolved.has(rel))
155
172
  continue;
156
173
  const failedAt = inlineImageFailedAt.get(rel);
157
174
  if (failedAt !== undefined && Date.now() - failedAt < INLINE_IMAGE_RETRY_COOLDOWN_MS)
158
175
  continue;
159
- const plan = planAttachment({ workspaceDir, filePath: rel, maxBytes: MAX_WORKSPACE_IMAGE_BYTES });
176
+ const plan = planAttachment({ workspaceDir, filePath: rel, maxBytes: MAX_WORKSPACE_REF_BYTES });
160
177
  if (isAttachPlanError(plan)) {
161
- console.warn(`[inline-image] ${rel}: ${plan.error}`);
178
+ console.warn(`[component] ${rel}: ${plan.error}`);
162
179
  inlineImageFailedAt.set(rel, Date.now());
163
180
  continue;
164
181
  }
165
- // `![…](notes.md)` is a link the agent mistyped, not an image. Refusing
166
- // non-image bytes keeps a stray one from becoming an unopenable chip.
167
- if (plan.kind !== "image") {
168
- console.warn(`[inline-image] ${rel}: not an image (${plan.mimeType})`);
182
+ // `![…](notes.md)` is a link the agent mistyped, not an image, so refusing
183
+ // non-image bytes keeps a stray one from becoming an unopenable chip. A
184
+ // `::view{src=notes.md}` asked for the file on purpose, so it is kept and
185
+ // the view layer picks a renderer from the mime type.
186
+ if (syntax === "image" && plan.kind !== "image") {
187
+ console.warn(`[component] ${rel}: not an image (${plan.mimeType})`);
169
188
  inlineImageFailedAt.set(rel, Date.now());
170
189
  continue;
171
190
  }
172
191
  try {
173
- recordResolvedImage(record.id, rel, plan.bytes, { name: plan.name, mimeType: plan.mimeType });
192
+ recordResolvedRef(record.id, rel, plan.bytes, { name: plan.name, mimeType: plan.mimeType, kind: plan.kind });
174
193
  }
175
194
  catch (error) {
176
- console.warn(`[inline-image] ${rel}:`, error instanceof Error ? error.message : String(error));
195
+ console.warn(`[component] ${rel}:`, error instanceof Error ? error.message : String(error));
177
196
  inlineImageFailedAt.set(rel, Date.now());
178
197
  }
179
198
  }
@@ -197,7 +216,7 @@ export function createTranscriptPersistence(deps) {
197
216
  inlineImageFailedAt.set(url, Date.now());
198
217
  return;
199
218
  }
200
- recordResolvedImage(record.id, url, result.bytes, { name: inlineImageDisplayName(url, result.mimeType), mimeType: result.mimeType });
219
+ recordResolvedRef(record.id, url, result.bytes, { name: inlineImageDisplayName(url, result.mimeType), mimeType: result.mimeType });
201
220
  }
202
221
  catch (error) {
203
222
  console.warn(`[inline-image] ${url}:`, error instanceof Error ? error.message : String(error));
@@ -353,7 +372,7 @@ export function createTranscriptPersistence(deps) {
353
372
  persistToolActivityFromEvent,
354
373
  persistIntermediateFromEvent,
355
374
  resolveInlineImages,
356
- resolveWorkspaceImages,
375
+ resolveWorkspaceRefs,
357
376
  conversationMessages,
358
377
  forkMessages,
359
378
  buildHistoryEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.20.9-staging.20",
3
+ "version": "0.20.9-staging.21",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",