@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.
- package/README.md +1 -2
- package/dist/agent-instructions.js +14 -4
- package/dist/apps/remote-preview.js +7 -0
- package/dist/apps/service.js +6 -1
- package/dist/apps/styles.css +56 -70
- package/dist/automation-reseal.js +30 -0
- package/dist/config-cli.js +2 -0
- package/dist/controllers/credential-commands.js +3 -27
- package/dist/credentials/api.js +2 -43
- package/dist/credentials/document.js +6 -14
- package/dist/credentials/store.js +5 -6
- package/dist/device-registry.js +27 -0
- package/dist/harness/checkpoint.js +0 -0
- package/dist/node-config.js +4 -3
- package/dist/runtime/model-catalog.js +16 -0
- package/dist/runtime/tool-call-map.js +1 -1
- package/dist/runtime/tool-trace.js +1 -1
- package/dist/server.js +136 -244
- package/dist/session/event-log.js +136 -29
- package/dist/session/inline-image-fetch.js +24 -33
- package/dist/session/message-components.js +429 -0
- package/dist/session/run-terminal.js +9 -7
- package/dist/session/transcript-persistence.js +135 -10
- package/dist/work-instructions.js +50 -0
- package/package.json +2 -2
- package/web/assets/AutomationsView-HnUfeti_.js +51 -0
- package/web/assets/ChangesCard-Dnkguen5.js +4 -0
- package/web/assets/LibraryView-CSKKV8TI.js +2 -0
- package/web/assets/{ProviderConnect-BPwnaFGk.js → ProviderConnect-DNgxGmP9.js} +1 -1
- package/web/assets/SessionList-CYU9IGnd.js +1 -0
- package/web/assets/Settings-HQZtxiMV.js +10 -0
- package/web/assets/{Sheet-DOOJosQY.js → Sheet-DoPtOHBC.js} +1 -1
- package/web/assets/{StatusDot-dpbztK53.js → Spinner-CPdRfDcT.js} +1 -1
- package/web/assets/{Terminal-DVGXS6Yd.js → Terminal-DY6UB5U-.js} +1 -1
- package/web/assets/{VoiceSettings-DX-n0SFl.js → VoiceSettings-BoQ_Mhpm.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-CnJ4astd.js → abnfDiagram-VCTEODGH-n-P11_L4.js} +1 -1
- package/web/assets/arc-BNLueWOY.js +1 -0
- package/web/assets/architecture-7GRP2DOG-DyG-wgaH.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-B26-V_Xd.js → architectureDiagram-5GKGNRK7-C0b9IP5U.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-DuoZkDHs.js → blockDiagram-I7D4REHJ-iVx1FQBD.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-B81PZ-Vw.js → c4Diagram-7LVT6UL2-DIwEaAvJ.js} +1 -1
- package/web/assets/channel-HmCJmS4T.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-DmSrqg9X.js → chunk-4HAMMTFA-DSsyukSz.js} +1 -1
- package/web/assets/chunk-75Z2AOVW-zwLYo-XL.js +2 -0
- package/web/assets/{chunk-DU6HZSFF-jMBIr1yK.js → chunk-DU6HZSFF-PGuLkqeP.js} +15 -15
- package/web/assets/chunk-F27PBJKO-CpkyeGC9.js +1 -0
- package/web/assets/{chunk-GMAD6QVW-C5YdBKWY.js → chunk-GMAD6QVW-CZSmK3Ms.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-DOOAmJLJ.js → chunk-GVQU2GXP-C2XMkY6v.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-CISXYXGE.js → chunk-IMKFNOWR-DAIVz3j5.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-CvWtiEy-.js → chunk-L3NEJ4N5-C0geNTDK.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-yVAn5yky.js → chunk-OSK3NFVY-D-WN2fBr.js} +2 -2
- package/web/assets/{chunk-P2QGCYS3-D-VGoELb.js → chunk-P2QGCYS3-DYGhprKz.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-0Mj4Bp-N.js → chunk-POPQ4Y6H-Dlsu2a2Q.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-Dvvdg6s1.js → chunk-PWAF6VOD-DK0QWYdc.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-DcllnbDq.js → chunk-SHT3W25Y-BpQHIdUi.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-DnuOGeio.js → chunk-SVP7TREG-Cs5YWxpG.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-Dr76NyiT.js → chunk-TICWLB2K-BRsWLPXA.js} +1 -1
- package/web/assets/chunk-XXDRQBXY-CjF14Z7v.js +1 -0
- package/web/assets/classDiagram-ZZMXUADV-BmdAoNdO.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-BmdAoNdO.js +1 -0
- package/web/assets/colors-Cg5Vh3br.js +1 -0
- package/web/assets/{cose-bilkent-JH36ORCC-BIqBF2fY.js → cose-bilkent-JH36ORCC-CEUD3V3W.js} +1 -1
- package/web/assets/{cynefin-OW5HDTMX-DoAt_4E6.js → cynefin-OW5HDTMX-DoHWnu_-.js} +1 -1
- package/web/assets/cynefinDiagram-5FMLGOSQ-CgPeHmV5.js +62 -0
- package/web/assets/{dagre-GXQ25YYZ-DIAXxVMd.js → dagre-GXQ25YYZ-CrIlgwM7.js} +1 -1
- package/web/assets/{defaultLocale-BFoDCU3G.js → defaultLocale-CNPGWpMu.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-DA78JGp6.js → diagram-S7CK7UJ4-BM_l51zV.js} +1 -1
- package/web/assets/diagram-UQ7AKVKN-CfkxqgPI.js +41 -0
- package/web/assets/{diagram-VSXAHHWV-DCkvR_pJ.js → diagram-VSXAHHWV-0y5vCI9g.js} +1 -1
- package/web/assets/diagram-VX7I27RA-BMkMy7jn.js +24 -0
- package/web/assets/diagram-Z3DM3KII--0fxfDqG.js +24 -0
- package/web/assets/dist-kYfyPZP9.js +1 -0
- package/web/assets/{ebnfDiagram-PWID7BFC-CSCD0PAi.js → ebnfDiagram-PWID7BFC-6tPNzbJl.js} +1 -1
- package/web/assets/embed-BXV41wg7.js +201 -0
- package/web/assets/{erDiagram-RLTQ6QDP-M512qtMu.js → erDiagram-RLTQ6QDP-Bjqm_o3R.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-CoCZxN1n.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-CznfWzEq.js +1 -0
- package/web/assets/ganttDiagram-EL5Y4UJY-92X7KOOI.js +292 -0
- package/web/assets/{gitGraph-4MIJSDKK-CwjwFpZb.js → gitGraph-4MIJSDKK-B5F-u7Vq.js} +1 -1
- package/web/assets/gitGraphDiagram-WWUBYQGX-Ct2hVG0b.js +106 -0
- package/web/assets/index-BKSbod42.css +1 -0
- package/web/assets/{index-CNs5i7tU.js → index-CnjJQpgZ.js} +2 -2
- package/web/assets/{info-A6RAGUB7-D6nXu5V2.js → info-A6RAGUB7-D3xmYWyC.js} +1 -1
- package/web/assets/infoDiagram-27XIBGKW-lzqZ7xaA.js +2 -0
- package/web/assets/init-ZlXS9mIS.js +1 -0
- package/web/assets/{ishikawaDiagram-5VMMS53U-4bsiyG2x.js → ishikawaDiagram-5VMMS53U-CsVZHXR0.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-o3U3L2rt.js → journeyDiagram-3NMN7TZE-DN9AdyVy.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-CQOPj0NZ.js → kanban-definition-UXKFOSKX-gvkBaOFe.js} +1 -1
- package/web/assets/linear-CeYXFQza.js +1 -0
- package/web/assets/{mermaid-parser.core-Ba2-DGvb.js → mermaid-parser.core-CPwOm7-a.js} +3 -3
- package/web/assets/{mermaid.core-BxdMVbm9.js → mermaid.core-F09Wr40d.js} +6 -6
- package/web/assets/{mindmap-definition-YA3MSWOX-1Eu4-zLM.js → mindmap-definition-YA3MSWOX-Qzc2J9Pm.js} +1 -1
- package/web/assets/mount-Bvl-yslT.js +36 -0
- package/web/assets/ordinal-BdBDtYTt.js +1 -0
- package/web/assets/{packet-AYTQ26CC-BTZ7CbVd.js → packet-AYTQ26CC-Unxh5mP2.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-DNdK3axo.js → pegDiagram-XKGWAZYB-BIqcRp3m.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-DidjfqgW.js → pie-WAS4IAKB-BipHZKbc.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-Dme68TUA.js → pieDiagram-E7YTZNPT-gaTNuzPD.js} +2 -2
- package/web/assets/{quadrantDiagram-AXDQQJYC-z0xeSJSB.js → quadrantDiagram-AXDQQJYC-3BEmlbuE.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-BV50NO32.js → radar-RG4KPBEZ-Cm1_Q0XJ.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-BOkd4wGS.js → railroad-74A4TZTK-CJ4K8tjT.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-BVKI4cmM.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-UMcDSelb.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-C00zpjQU.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-2eK1mPm_.js → railroadDiagram-O6MQD6OU-Nzyn7Xx6.js} +1 -1
- package/web/assets/range-EplFIxc7.js +1 -0
- package/web/assets/{requirementDiagram-BXWQKSXE-DvlVp433.js → requirementDiagram-BXWQKSXE-9zXuiai_.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-C6CsEyCg.js → sankeyDiagram-P5KCCOFB-Cnw1rQun.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-1wwxxN6P.js → sequenceDiagram-WJ2MYXX4-BSLLRV-U.js} +3 -3
- package/web/assets/{settingsRoute-Dxrvnqih.js → settingsRoute-C2Up46o6.js} +1 -1
- package/web/assets/src-CFHfg5gg.js +1 -0
- package/web/assets/stateDiagram-D77RDMKH-D07b4cJB.js +1 -0
- package/web/assets/stateDiagram-v2-MP3YSRHH-DaG9Yzhk.js +1 -0
- package/web/assets/step-BDqHAzds.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-XLThGmTf.js → swimlanes-42K2YHIH-RpDl0CG9.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-7003ldWX.js +8 -0
- package/web/assets/time-Dzw-YSCF.js +1 -0
- package/web/assets/{timeline-definition-24CTP7MA-BEOV0lDT.js → timeline-definition-24CTP7MA-BgQOP12r.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-Dm4FCZVW.js → treeView-Q6P3EWNA-BiSBsrlo.js} +1 -1
- package/web/assets/treemap-BfjIY-m6.js +1 -0
- package/web/assets/{treemap-WGGIJYW6-DEZGmkE8.js → treemap-WGGIJYW6-CNdjKqUG.js} +1 -1
- package/web/assets/useStore-CxjHbFPA.js +16 -0
- package/web/assets/{vennDiagram-4TSXK5OY-CDRUEJ_c.js → vennDiagram-4TSXK5OY-DLzOr3Yv.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-CjZ5JazT.js → wardley-WFR3VGLG-LCC8FWVb.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-COsxLnU6.js → wardleyDiagram-VM6X3IG4-WrwzM3YW.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-BfwzNIP-.js → xychartDiagram-S5SC5T6Z-B_HuoAnl.js} +3 -3
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/dist/ephemeral-exec.js +0 -108
- package/dist/hosted-custody.js +0 -30
- package/web/assets/AutomationsView-Dk1aHviY.js +0 -51
- package/web/assets/ChangesCard-Bn37dRew.js +0 -4
- package/web/assets/LibraryView-CGkpNOgR.js +0 -2
- package/web/assets/Settings-DJjEVwmu.js +0 -10
- package/web/assets/arc-Pry-7G1_.js +0 -1
- package/web/assets/architecture-7GRP2DOG-kbFZ_7eU.js +0 -1
- package/web/assets/array-BifhSqXX.js +0 -1
- package/web/assets/channel-CpJ3OYta.js +0 -1
- package/web/assets/chunk-75Z2AOVW-CGlA2AB-.js +0 -2
- package/web/assets/chunk-F27PBJKO-Cto7X88Z.js +0 -1
- package/web/assets/chunk-XXDRQBXY-DqAYeljN.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BSqiC-6r.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BSqiC-6r.js +0 -1
- package/web/assets/cynefinDiagram-5FMLGOSQ-B2jZZ_es.js +0 -62
- package/web/assets/diagram-UQ7AKVKN-D7BIW4QU.js +0 -41
- package/web/assets/diagram-VX7I27RA-qmkf9rFQ.js +0 -24
- package/web/assets/diagram-Z3DM3KII-BZf2h-T8.js +0 -24
- package/web/assets/dist-v5Q1xZ2K.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-DHYpZdDY.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-BdNS-1c7.js +0 -1
- package/web/assets/ganttDiagram-EL5Y4UJY-DGMQzJ8U.js +0 -292
- package/web/assets/gitGraphDiagram-WWUBYQGX-DNfbdhPa.js +0 -106
- package/web/assets/index-kWXUrhG8.css +0 -1
- package/web/assets/infoDiagram-27XIBGKW-QcpEFiaQ.js +0 -2
- package/web/assets/init-C-OQMol4.js +0 -1
- package/web/assets/line-Cq9nN3X2.js +0 -1
- package/web/assets/linear-SzVmGSw4.js +0 -1
- package/web/assets/mount-C9EzYXqA.js +0 -31
- package/web/assets/ordinal-BDEzSJ7C.js +0 -1
- package/web/assets/path-fybaL0A-.js +0 -1
- package/web/assets/railroad-abnf-HS5TGJTU-DiQLjR_J.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-D4bSTzXb.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-DDhl4AMh.js +0 -1
- package/web/assets/src-B6xuSHsQ.js +0 -1
- package/web/assets/stateDiagram-D77RDMKH-_fkXrLZe.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-Cuo-j5c1.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-DOpCt5KW.js +0 -8
- 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(/</g, "<")
|
|
76
|
+
.replace(/>/g, ">")
|
|
77
|
+
.replace(/&/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
|
+
// `` 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 `` 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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|