@bivy/bivy 0.20.9-staging.20 → 0.20.9-staging.22
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/dist/agent-instructions.js +6 -3
- package/dist/apps/styles.css +23 -0
- package/dist/server.js +1 -1
- package/dist/session/inline-image-fetch.js +8 -5
- package/dist/session/message-components.js +172 -0
- package/dist/session/transcript-persistence.js +39 -20
- package/package.json +1 -1
- package/web/assets/AutomationsView-Br7L0-Zg.js +51 -0
- package/web/assets/{ChangesCard-z3_ukf1u.js → ChangesCard-BfP_GgIK.js} +3 -3
- package/web/assets/LibraryView-CSe7yv6r.js +2 -0
- package/web/assets/{ProviderConnect-B3eBVETd.js → ProviderConnect-CoqDojZZ.js} +1 -1
- package/web/assets/SessionList-CAoE-3C7.js +1 -0
- package/web/assets/{Settings-BNot7mbn.js → Settings-DYSEhgqn.js} +5 -5
- package/web/assets/{Sheet-D6GzuZLS.js → Sheet-caCrA1GU.js} +1 -1
- package/web/assets/{Terminal-BsC5uIMV.js → Terminal-DTCSAu50.js} +1 -1
- package/web/assets/{VoiceSettings-ls5IroKf.js → VoiceSettings-Djk3abgx.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-DbgBwr4N.js → abnfDiagram-VCTEODGH-CSwey7Ok.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-BAYVSAaf.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-Bsmv18_k.js → architectureDiagram-5GKGNRK7-BBBVAzwA.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-qHhTUAop.js → blockDiagram-I7D4REHJ-_j19DyeC.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-2zjJl4E9.js → c4Diagram-7LVT6UL2-CaU3wOL1.js} +1 -1
- package/web/assets/channel-CrkPGiw9.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-BNe2pfM4.js → chunk-4HAMMTFA-GGND3e9k.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-CKl0NV3C.js → chunk-75Z2AOVW-vZMlOTli.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-B3P59NKh.js → chunk-DU6HZSFF-CQeyhcrS.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-CAWJsp2_.js → chunk-F27PBJKO-BnJUve12.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-BlDnxBjY.js → chunk-GMAD6QVW-CwjVaLcM.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-Bczr5jKx.js → chunk-GVQU2GXP-DGVcVilQ.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-CWeRsdUj.js → chunk-IMKFNOWR-DmMklOHd.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-BQPTEPpn.js → chunk-L3NEJ4N5-E7KhRxO_.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-DzGYbGz2.js → chunk-OSK3NFVY-i5KlTAtc.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-CSvykgBa.js → chunk-P2QGCYS3-B8UVjuAV.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-CSd2VGfh.js → chunk-POPQ4Y6H-TaAqMKg2.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-DR2HyrI3.js → chunk-PWAF6VOD-BWfwttll.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-CSMOeLrh.js → chunk-SHT3W25Y-YNrk1No8.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-BTIkXcEs.js → chunk-SVP7TREG-BCE8uZck.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-DS3eZA5z.js → chunk-TICWLB2K-C_LTum_p.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-D1-ZOLNO.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-D1-ZOLNO.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-qmnX5Drm.js → cynefin-OW5HDTMX-TmhXZJV5.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-Cuh1jGM6.js → cynefinDiagram-5FMLGOSQ-EyEJB-ZC.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-7GZbtRVA.js → dagre-GXQ25YYZ-CdJuIXlj.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-zQJ-kaiE.js → diagram-S7CK7UJ4-DdZqga1f.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-B5Ed0jaZ.js → diagram-UQ7AKVKN-X7YAkOc5.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-D5vZvTR7.js → diagram-VSXAHHWV-9Ryi0AUN.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-DEkORtmo.js → diagram-VX7I27RA-wACcvkTw.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-DPb9NWrB.js → diagram-Z3DM3KII-BgbwC53V.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-gVpp4zKg.js → ebnfDiagram-PWID7BFC-BX6g7HnR.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-4YNUcQlu.js → erDiagram-RLTQ6QDP-Cyxfj9K1.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-mMuWtYd9.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-CCEIJz0L.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-DUk_l5ka.js → ganttDiagram-EL5Y4UJY-Dd46tpxK.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-CGS2CP5_.js → gitGraph-4MIJSDKK-Deq0a4VU.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-C7rXPKT3.js → gitGraphDiagram-WWUBYQGX-Bf9ycAOa.js} +1 -1
- package/web/assets/index-Chhl9a2_.css +1 -0
- package/web/assets/{index-D_E_e1lg.js → index-ZOqk9szV.js} +2 -2
- package/web/assets/{info-A6RAGUB7-D2F_gpvF.js → info-A6RAGUB7-Bf1CVxOs.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-DdRRRHRV.js → infoDiagram-27XIBGKW-BVoZRQfT.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-Co_J8u-p.js → ishikawaDiagram-5VMMS53U-Brkk3cFP.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-CrJ7j8qZ.js → journeyDiagram-3NMN7TZE-DshlrscF.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-Csa-Hm-V.js → kanban-definition-UXKFOSKX-kuH25cu6.js} +1 -1
- package/web/assets/{line-CWxnZANW.js → line-CmdEFmHu.js} +1 -1
- package/web/assets/{mermaid-parser.core-DDL03Yeh.js → mermaid-parser.core-Dcz6l-rJ.js} +3 -3
- package/web/assets/{mermaid.core-CBFjpUnO.js → mermaid.core-BVFRzFTX.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-BIwMlu7Q.js → mindmap-definition-YA3MSWOX-C7ufUxV9.js} +1 -1
- package/web/assets/mount-DdC9Gakj.js +34 -0
- package/web/assets/{packet-AYTQ26CC-ClFWscdE.js → packet-AYTQ26CC-CSd1SCWR.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-Css6F3UD.js → pegDiagram-XKGWAZYB-CB7zMjKE.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-7l7k0PF3.js → pie-WAS4IAKB-J_cM_eH4.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-8IXxZkpM.js → pieDiagram-E7YTZNPT-BmvVGh5G.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-lAd65vKY.js → quadrantDiagram-AXDQQJYC-nv6OMi3_.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-CXOQDEjR.js → radar-RG4KPBEZ-DhuALA3v.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-DyAhTgOw.js → railroad-74A4TZTK-BYP2C3QZ.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-CIJwSYG3.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-Dz303c1z.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-Cgqq8lJB.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-B4xtr-GL.js → railroadDiagram-O6MQD6OU-BbMsM7jA.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-ZEB-t5Qu.js → requirementDiagram-BXWQKSXE-DwG8sVQH.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-QivhI6_D.js → sankeyDiagram-P5KCCOFB-CfPcKe8Z.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-DwJybUss.js → sequenceDiagram-WJ2MYXX4--w65lpJ0.js} +1 -1
- package/web/assets/{settingsRoute-DIPLWa9I.js → settingsRoute-Bn_MFt4p.js} +1 -1
- package/web/assets/{stateDiagram-D77RDMKH-CZKza48A.js → stateDiagram-D77RDMKH-DoBuP6CN.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-CSCu1o6C.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-BuSVwpr4.js → swimlanes-42K2YHIH-Bopp-0wx.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-B3XZR4ti.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-D5gldQJo.js → timeline-definition-24CTP7MA-CnWRZozB.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-DE5s4Bjg.js → treeView-Q6P3EWNA-Dc7knsDy.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-B3IETKms.js → treemap-WGGIJYW6-DTVyu1ky.js} +1 -1
- package/web/assets/useStore-KwfFQwKS.js +23 -0
- package/web/assets/{vennDiagram-4TSXK5OY-9OQdQ7F9.js → vennDiagram-4TSXK5OY-Cq1J6Fav.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-CSVfZgVH.js → wardley-WFR3VGLG-DmMuiXpP.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-BmOudRh6.js → wardleyDiagram-VM6X3IG4-BQYKmDum.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-CqyBA2yY.js → xychartDiagram-S5SC5T6Z-CSMDlTbC.js} +1 -1
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/web/assets/AutomationsView-CKGbGbUJ.js +0 -51
- package/web/assets/LibraryView-De-EIx0E.js +0 -2
- package/web/assets/architecture-7GRP2DOG-BbwsPIjP.js +0 -1
- package/web/assets/channel-Cb8DRt8H.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BfpJx-fl.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BfpJx-fl.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-D6hImpfS.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-Cm1vFAOu.js +0 -1
- package/web/assets/index-Cxa5QrGG.css +0 -1
- package/web/assets/mount-Co7Zfyst.js +0 -31
- package/web/assets/railroad-abnf-HS5TGJTU-D9nETWcm.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-C9tk_YfZ.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-DKIluUM5.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-D0QqHEXP.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-BGaunElN.js +0 -8
- package/web/assets/useStore-DcyF6rrO.js +0 -22
|
@@ -33,9 +33,12 @@ export const BIVY_AGENT_NOTE = [
|
|
|
33
33
|
"- Show an IMAGE (screenshot, chart, diagram) inside your reply: write it as markdown, " +
|
|
34
34
|
"``, 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
|
-
"-
|
|
37
|
-
'
|
|
38
|
-
"
|
|
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. Same workspace-only ' +
|
|
38
|
+
"rule. A `.csv`/`.tsv` renders as a table; anything else arrives as a download. For a single number worth " +
|
|
39
|
+
'reading at a glance, fence a spec instead: ```bivy {"type":"metric","value":"4 650 kWh","label":"per year"} ```.',
|
|
40
|
+
"- Send a file WITHOUT writing a reply (or from a script): run " +
|
|
41
|
+
'`bivy attach <path> [--caption "short note"]`, or call the `attach_to_chat` tool if you have it.',
|
|
39
42
|
"- Something with a UI: `bivy app publish <manifest.json>` (a web server's port, a static build, a terminal) or " +
|
|
40
43
|
"`bivy app run -- <command>` (a desktop app) gives the user a live preview; `bivy app shot` screenshots it so you " +
|
|
41
44
|
"can check your work; `bivy app present` tells the user a visible change is ready to look at, and " +
|
package/dist/apps/styles.css
CHANGED
|
@@ -639,6 +639,29 @@ 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%; }
|
|
657
|
+
/* A single number the agent wants read at a glance. Inherits .card's shell;
|
|
658
|
+
only the value's type scale is its own. */
|
|
659
|
+
.metric-component { text-align: center; }
|
|
660
|
+
.metric-value { font-size: var(--text-3xl); line-height: var(--lh-3xl); font-weight: var(--weight-semibold); }
|
|
661
|
+
/* A table from a CSV or a spec reuses .markdown-table-wrap, so it reads exactly
|
|
662
|
+
like a table the agent wrote as markdown. The caption is the only addition. */
|
|
663
|
+
.table-component { margin: 0; }
|
|
664
|
+
.table-component figcaption { margin-top: var(--space-2); }
|
|
642
665
|
/* Syntax highlighting for fenced code blocks (highlight.js token classes).
|
|
643
666
|
Colors use light-dark() so one rule set tracks the app's light/dark scheme
|
|
644
667
|
(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.
|
|
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
|
|
45
|
-
* in first-seen order, under the same per-message
|
|
46
|
-
|
|
47
|
-
|
|
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,175 @@ 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
|
+
// `` 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 `` 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
|
+
const ext = String(opts.path || "").toLowerCase().split(".").pop() ?? "";
|
|
275
|
+
if (ext === "csv" || ext === "tsv")
|
|
276
|
+
return "table";
|
|
277
|
+
// Everything else is a file the reader can open or download. Rows for richer
|
|
278
|
+
// readings of a file — a notebook, a diff — belong here alongside the renderer
|
|
279
|
+
// that can actually draw them. A row added early would promise a kind the view
|
|
280
|
+
// layer has to decline, turning a useful download into "this version cannot
|
|
281
|
+
// show that yet".
|
|
282
|
+
return "file";
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Every reference a message makes that Bivy has to resolve — from image syntax
|
|
286
|
+
* or from a directive — de-duplicated, in first-seen order, under ONE shared
|
|
287
|
+
* cap. One budget across both forms is deliberate: the cost being bounded is
|
|
288
|
+
* file reads and persisted bytes, which do not care which syntax asked.
|
|
289
|
+
*/
|
|
290
|
+
export function extractMessageReferences(text, limit = MAX_COMPONENTS_PER_MESSAGE) {
|
|
291
|
+
if (!text)
|
|
292
|
+
return [];
|
|
293
|
+
const source = String(text);
|
|
294
|
+
const found = [];
|
|
295
|
+
// Images are matched against the WHOLE text, not line by line: alt text may
|
|
296
|
+
// contain a newline (an agent wrapping a long caption at 80 columns), and the
|
|
297
|
+
// renderer — which sees a paragraph with its lines already joined — would
|
|
298
|
+
// place a mount point the node then never resolved.
|
|
299
|
+
for (const match of source.matchAll(imagePattern())) {
|
|
300
|
+
const ref = unescapeComponentRef(match[2] ?? "");
|
|
301
|
+
const origin = classifyImageTarget(ref);
|
|
302
|
+
if (origin)
|
|
303
|
+
found.push({ ref, origin, syntax: "image", at: match.index ?? 0 });
|
|
304
|
+
}
|
|
305
|
+
// Directives own a whole line, so they are matched per line; the running
|
|
306
|
+
// offset keeps them in document order with the images above.
|
|
307
|
+
let at = 0;
|
|
308
|
+
for (const line of source.split("\n")) {
|
|
309
|
+
const directive = parseComponentDirective(line);
|
|
310
|
+
if (directive?.ref)
|
|
311
|
+
found.push({ ref: directive.ref, origin: "workspace", syntax: "view", at });
|
|
312
|
+
at += line.length + 1;
|
|
313
|
+
}
|
|
314
|
+
found.sort((a, b) => a.at - b.at);
|
|
315
|
+
const out = [];
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
for (const { ref, origin, syntax } of found) {
|
|
318
|
+
if (seen.has(ref))
|
|
319
|
+
continue;
|
|
320
|
+
seen.add(ref);
|
|
321
|
+
out.push({ ref, origin, syntax });
|
|
322
|
+
if (out.length >= limit)
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
@@ -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,
|
|
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
|
|
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 `` 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
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
163
|
+
function resolveWorkspaceRefs(record, workspaceDir) {
|
|
147
164
|
if (!workspaceDir)
|
|
148
165
|
return;
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
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
|
|
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:
|
|
176
|
+
const plan = planAttachment({ workspaceDir, filePath: rel, maxBytes: MAX_WORKSPACE_REF_BYTES });
|
|
160
177
|
if (isAttachPlanError(plan)) {
|
|
161
|
-
console.warn(`[
|
|
178
|
+
console.warn(`[component] ${rel}: ${plan.error}`);
|
|
162
179
|
inlineImageFailedAt.set(rel, Date.now());
|
|
163
180
|
continue;
|
|
164
181
|
}
|
|
165
|
-
// `` is a link the agent mistyped, not an image
|
|
166
|
-
// non-image bytes keeps a stray one from becoming an unopenable chip.
|
|
167
|
-
|
|
168
|
-
|
|
182
|
+
// `` 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
|
-
|
|
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(`[
|
|
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
|
-
|
|
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
|
-
|
|
375
|
+
resolveWorkspaceRefs,
|
|
357
376
|
conversationMessages,
|
|
358
377
|
forkMessages,
|
|
359
378
|
buildHistoryEvent,
|
package/package.json
CHANGED