@bivy/bivy 0.20.8-staging.1 → 0.20.8-staging.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/apps/styles.css +41 -0
  2. package/dist/controllers/file-commands.js +208 -0
  3. package/dist/protocol/client-command-routes.js +1 -0
  4. package/dist/protocol/client-command-schemas.js +2 -0
  5. package/dist/server.js +10 -0
  6. package/package.json +1 -1
  7. package/web/assets/{AutomationsView-Dhv90Goz.js → AutomationsView-BlskfXsa.js} +1 -1
  8. package/web/assets/{ChangesCard-Bt_5dEhH.js → ChangesCard-wa7KQr_c.js} +1 -1
  9. package/web/assets/{LibraryView-BBfQ5hmx.js → LibraryView-kF61hq31.js} +1 -1
  10. package/web/assets/{ProviderConnect-DchJv4-X.js → ProviderConnect-CorJ3vAj.js} +1 -1
  11. package/web/assets/{Settings-DlwcGkm8.js → Settings-CU3VaG58.js} +3 -3
  12. package/web/assets/{Sheet-B7Z_5ode.js → Sheet-CyauvUYv.js} +1 -1
  13. package/web/assets/{Terminal-BEW9AsDD.js → Terminal-D5bUiUOK.js} +1 -1
  14. package/web/assets/{VoiceSettings-BuAH2DeI.js → VoiceSettings-CMtn4Ljo.js} +1 -1
  15. package/web/assets/{abnfDiagram-VCTEODGH-B1Ote64Y.js → abnfDiagram-VCTEODGH-iV_PfEvy.js} +1 -1
  16. package/web/assets/architecture-7GRP2DOG-Dzsx3Md6.js +1 -0
  17. package/web/assets/{architectureDiagram-5GKGNRK7-pSgLl4Ui.js → architectureDiagram-5GKGNRK7-BFJI1WJO.js} +1 -1
  18. package/web/assets/{blockDiagram-I7D4REHJ-DBhXiG_F.js → blockDiagram-I7D4REHJ-DzkfBPb4.js} +1 -1
  19. package/web/assets/{c4Diagram-7LVT6UL2-DfBBRGyu.js → c4Diagram-7LVT6UL2-DLhftvmM.js} +1 -1
  20. package/web/assets/channel-B-yXI3ZO.js +1 -0
  21. package/web/assets/{chunk-4HAMMTFA-CtkBsqfK.js → chunk-4HAMMTFA-BLReF9Dj.js} +1 -1
  22. package/web/assets/{chunk-75Z2AOVW-ZzwHWk5w.js → chunk-75Z2AOVW-AowCsqsA.js} +1 -1
  23. package/web/assets/{chunk-DU6HZSFF-zwkYVJLO.js → chunk-DU6HZSFF-BzxERZ0N.js} +1 -1
  24. package/web/assets/{chunk-F27PBJKO-D4Eh54NE.js → chunk-F27PBJKO-BvUtmLAT.js} +1 -1
  25. package/web/assets/{chunk-GMAD6QVW-CmxiJ48h.js → chunk-GMAD6QVW-BCwouWU7.js} +1 -1
  26. package/web/assets/{chunk-GVQU2GXP-Dv_yLEtA.js → chunk-GVQU2GXP-CKpKjCy0.js} +1 -1
  27. package/web/assets/{chunk-IMKFNOWR-D2iveAJn.js → chunk-IMKFNOWR-Ds4aOSFu.js} +1 -1
  28. package/web/assets/{chunk-L3NEJ4N5-BmYB9A51.js → chunk-L3NEJ4N5-BsA408fF.js} +1 -1
  29. package/web/assets/{chunk-OSK3NFVY-CGKN_LeZ.js → chunk-OSK3NFVY-DXhrIzuK.js} +1 -1
  30. package/web/assets/{chunk-P2QGCYS3-I-oHFTNl.js → chunk-P2QGCYS3-DEkdJ2Qf.js} +1 -1
  31. package/web/assets/{chunk-POPQ4Y6H-_-D-zxTt.js → chunk-POPQ4Y6H-AkkRDb7x.js} +1 -1
  32. package/web/assets/{chunk-PWAF6VOD-Cx0iGDr8.js → chunk-PWAF6VOD-B1lCFelI.js} +1 -1
  33. package/web/assets/{chunk-SHT3W25Y-DqbhuxyB.js → chunk-SHT3W25Y-Cwn0cdSV.js} +1 -1
  34. package/web/assets/{chunk-SVP7TREG-CwAPHe7-.js → chunk-SVP7TREG-BAHEWc7X.js} +1 -1
  35. package/web/assets/{chunk-TICWLB2K-CIAzV-iP.js → chunk-TICWLB2K-YNE_kWQV.js} +1 -1
  36. package/web/assets/classDiagram-ZZMXUADV-mb22ueVl.js +1 -0
  37. package/web/assets/classDiagram-v2-VYDZK3BY-mb22ueVl.js +1 -0
  38. package/web/assets/{cynefin-OW5HDTMX-JMSwTT4g.js → cynefin-OW5HDTMX-CgguoC6A.js} +1 -1
  39. package/web/assets/{cynefinDiagram-5FMLGOSQ-C8Q21R-1.js → cynefinDiagram-5FMLGOSQ-DKs9dFa3.js} +1 -1
  40. package/web/assets/{dagre-GXQ25YYZ-D30iJB_r.js → dagre-GXQ25YYZ-BNYgl_yY.js} +1 -1
  41. package/web/assets/{diagram-S7CK7UJ4-OTNRvDLZ.js → diagram-S7CK7UJ4-BQlqBaJT.js} +1 -1
  42. package/web/assets/{diagram-UQ7AKVKN-BAqmlxm6.js → diagram-UQ7AKVKN-1gzpuKVo.js} +1 -1
  43. package/web/assets/{diagram-VSXAHHWV-CJSHMEkJ.js → diagram-VSXAHHWV-CI9LReGZ.js} +1 -1
  44. package/web/assets/{diagram-VX7I27RA-B9Lmpo4y.js → diagram-VX7I27RA-3w6KNyYO.js} +1 -1
  45. package/web/assets/{diagram-Z3DM3KII-CitNedaL.js → diagram-Z3DM3KII-Cx5uvIoC.js} +1 -1
  46. package/web/assets/{ebnfDiagram-PWID7BFC-TB8DxF0V.js → ebnfDiagram-PWID7BFC-DDBBJ-un.js} +1 -1
  47. package/web/assets/{erDiagram-RLTQ6QDP-CFkLWND0.js → erDiagram-RLTQ6QDP-C0670g0G.js} +1 -1
  48. package/web/assets/eventmodeling-NTZA5JFV-DwWew4Zd.js +1 -0
  49. package/web/assets/flowDiagram-HODETNUW-BO0bxvrZ.js +1 -0
  50. package/web/assets/{ganttDiagram-EL5Y4UJY-e8c-70Pk.js → ganttDiagram-EL5Y4UJY-D-S7jV8O.js} +1 -1
  51. package/web/assets/{gitGraph-4MIJSDKK-Cyb_ZXpc.js → gitGraph-4MIJSDKK-n0qFNPnK.js} +1 -1
  52. package/web/assets/{gitGraphDiagram-WWUBYQGX-CFeII9_u.js → gitGraphDiagram-WWUBYQGX-BnMFT3Uc.js} +1 -1
  53. package/web/assets/index-D76QD5dl.css +1 -0
  54. package/web/assets/{index-Evgc33gP.js → index-Dbm2gkzB.js} +2 -2
  55. package/web/assets/{info-A6RAGUB7-DQUFoSGO.js → info-A6RAGUB7-BDQeI4cT.js} +1 -1
  56. package/web/assets/{infoDiagram-27XIBGKW-DAFCGMA_.js → infoDiagram-27XIBGKW-CJ6gMnKL.js} +1 -1
  57. package/web/assets/{ishikawaDiagram-5VMMS53U-BZ4DfAfj.js → ishikawaDiagram-5VMMS53U-DZCr3fBi.js} +1 -1
  58. package/web/assets/{journeyDiagram-3NMN7TZE-BdsTN6v9.js → journeyDiagram-3NMN7TZE-BiP96UIg.js} +1 -1
  59. package/web/assets/{kanban-definition-UXKFOSKX-C7RizPAY.js → kanban-definition-UXKFOSKX-D8pm9W-I.js} +1 -1
  60. package/web/assets/{line-BVaDN-UA.js → line-b61D4x2o.js} +1 -1
  61. package/web/assets/{mermaid-parser.core-SEQnuuIe.js → mermaid-parser.core-BcIWWwC3.js} +3 -3
  62. package/web/assets/{mermaid.core-Dccy9fCW.js → mermaid.core-CSi476uR.js} +4 -4
  63. package/web/assets/{mindmap-definition-YA3MSWOX-DHC8A7q0.js → mindmap-definition-YA3MSWOX-rzt2Hu0L.js} +1 -1
  64. package/web/assets/mount-ByTCLifz.js +33 -0
  65. package/web/assets/{packet-AYTQ26CC-C0-NhSJn.js → packet-AYTQ26CC-BmU95E1X.js} +1 -1
  66. package/web/assets/{pegDiagram-XKGWAZYB-BzSNtLmY.js → pegDiagram-XKGWAZYB-7Idq-T_t.js} +1 -1
  67. package/web/assets/{pie-WAS4IAKB-BqDKq9KC.js → pie-WAS4IAKB-Qra1MRYh.js} +1 -1
  68. package/web/assets/{pieDiagram-E7YTZNPT-Lr3IG5m5.js → pieDiagram-E7YTZNPT-C5axHfpv.js} +1 -1
  69. package/web/assets/{quadrantDiagram-AXDQQJYC-D9n9NvbZ.js → quadrantDiagram-AXDQQJYC-DLB9MoP_.js} +1 -1
  70. package/web/assets/{radar-RG4KPBEZ-BNB-ZzeF.js → radar-RG4KPBEZ-B4Gr9jet.js} +1 -1
  71. package/web/assets/{railroad-74A4TZTK-DIgKZfG_.js → railroad-74A4TZTK-D8PecAqL.js} +1 -1
  72. package/web/assets/railroad-abnf-HS5TGJTU-B9yclX1T.js +1 -0
  73. package/web/assets/railroad-ebnf-LZEXJU2U-l2PbXb-Q.js +1 -0
  74. package/web/assets/railroad-peg-WCYAUIDC-CCSfNwQP.js +1 -0
  75. package/web/assets/{railroadDiagram-O6MQD6OU-NT1UChuZ.js → railroadDiagram-O6MQD6OU-BIVI258e.js} +1 -1
  76. package/web/assets/{requirementDiagram-BXWQKSXE-DgFzZozC.js → requirementDiagram-BXWQKSXE-D5XGJyXn.js} +1 -1
  77. package/web/assets/{sankeyDiagram-P5KCCOFB-DMhWaXEB.js → sankeyDiagram-P5KCCOFB-DtzkvZdb.js} +1 -1
  78. package/web/assets/{sequenceDiagram-WJ2MYXX4-BTa9-awF.js → sequenceDiagram-WJ2MYXX4-DYL6uEwg.js} +1 -1
  79. package/web/assets/{settingsRoute-C1mvNJQN.js → settingsRoute-BiR5jLMZ.js} +1 -1
  80. package/web/assets/{stateDiagram-D77RDMKH-3eChesP_.js → stateDiagram-D77RDMKH-HyAyUrhk.js} +1 -1
  81. package/web/assets/stateDiagram-v2-MP3YSRHH-DgKKlaXv.js +1 -0
  82. package/web/assets/{swimlanes-42K2YHIH-DAzX0DdD.js → swimlanes-42K2YHIH-D-8UI2u5.js} +1 -1
  83. package/web/assets/swimlanesDiagram-VR7AAH4N-BRnl7x-I.js +8 -0
  84. package/web/assets/{timeline-definition-24CTP7MA-D7LAU60v.js → timeline-definition-24CTP7MA-Yd11ntOC.js} +1 -1
  85. package/web/assets/{treeView-Q6P3EWNA-BAinhKk_.js → treeView-Q6P3EWNA-Cd8gQl64.js} +1 -1
  86. package/web/assets/{treemap-WGGIJYW6-B6rVb2AN.js → treemap-WGGIJYW6-DC247xG-.js} +1 -1
  87. package/web/assets/{useStore-NbvMYl3_.js → useStore-Cft6t-I1.js} +3 -3
  88. package/web/assets/{vennDiagram-4TSXK5OY-INxRTqDL.js → vennDiagram-4TSXK5OY-MEj69bJp.js} +1 -1
  89. package/web/assets/{wardley-WFR3VGLG-d4ELXJs6.js → wardley-WFR3VGLG-2hDb2l-E.js} +1 -1
  90. package/web/assets/{wardleyDiagram-VM6X3IG4-E579TDWO.js → wardleyDiagram-VM6X3IG4-DObxcm8K.js} +1 -1
  91. package/web/assets/{xychartDiagram-S5SC5T6Z--ov5Tr63.js → xychartDiagram-S5SC5T6Z-Krd9coa1.js} +1 -1
  92. package/web/index.html +2 -2
  93. package/web/sw.js +1 -1
  94. package/web/assets/architecture-7GRP2DOG-DhKBlFGq.js +0 -1
  95. package/web/assets/channel-ByhPGa0t.js +0 -1
  96. package/web/assets/classDiagram-ZZMXUADV-FcYxpgDM.js +0 -1
  97. package/web/assets/classDiagram-v2-VYDZK3BY-FcYxpgDM.js +0 -1
  98. package/web/assets/eventmodeling-NTZA5JFV-CU3-mXbe.js +0 -1
  99. package/web/assets/flowDiagram-HODETNUW-CsBi0yZa.js +0 -1
  100. package/web/assets/index-DOCJQ99Q.css +0 -1
  101. package/web/assets/mount-C9BnP6cm.js +0 -30
  102. package/web/assets/railroad-abnf-HS5TGJTU-BmZJBR7z.js +0 -1
  103. package/web/assets/railroad-ebnf-LZEXJU2U-DiwlXmEA.js +0 -1
  104. package/web/assets/railroad-peg-WCYAUIDC-Lqo9rMHV.js +0 -1
  105. package/web/assets/stateDiagram-v2-MP3YSRHH-CZx4Pz8n.js +0 -1
  106. package/web/assets/swimlanesDiagram-VR7AAH4N-DGTSxofq.js +0 -8
@@ -300,6 +300,7 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
300
300
  }
301
301
  .side-pane-tabs { min-width: 0; overflow-x: auto; }
302
302
  .side-pane-tab { display: inline-flex; align-items: center; gap: var(--space-1); }
303
+ .side-pane-tabs .side-pane-tab { padding-inline: var(--space-2); }
303
304
  .seg-count { color: var(--muted); font-variant-numeric: tabular-nums; font-weight: var(--weight-normal); }
304
305
  .side-pane-close { flex: none; color: var(--muted); }
305
306
  .side-pane-close:hover { color: var(--ink); }
@@ -330,6 +331,46 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
330
331
  .main.changes-section > :not(.topbar):not(.banner):not(.main-changes) { display: none; }
331
332
  .main-changes { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--surface); }
332
333
  .main-changes .pane-panel-content { padding-bottom: max(var(--space-3), env(safe-area-inset-bottom)); }
334
+
335
+ /* ---- Files: read-only browser over the session's workspace ---- */
336
+ .files-browser, .files-viewer { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
337
+ .files-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding-bottom: var(--space-2); border-bottom: 1px solid var(--line); }
338
+ .files-toolbar-meta { font-size: var(--text-xs); color: var(--muted); }
339
+ .files-tree { display: flex; flex-direction: column; }
340
+ .files-row {
341
+ display: flex; align-items: center; gap: var(--space-1); width: 100%; min-height: 32px;
342
+ padding: var(--space-1) var(--space-2) var(--space-1) var(--space-1);
343
+ background: transparent; border: 0; border-radius: var(--radius-sm); color: var(--ink);
344
+ font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer;
345
+ }
346
+ .files-row:hover:not(:disabled) { background: var(--surface-2); }
347
+ .files-row:disabled { cursor: default; color: var(--muted); }
348
+ .files-row[data-status="deleted"] .files-name { text-decoration: line-through; }
349
+ .files-row-note { cursor: default; color: var(--muted); font-size: var(--text-xs); gap: var(--space-2); }
350
+ .files-row-note:hover { background: transparent; }
351
+ .files-row-error { color: var(--danger); }
352
+ @media (pointer: coarse) { .files-row { min-height: 40px; } }
353
+ .files-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); font-size: var(--text-xs); }
354
+ .files-meta { flex: none; font-size: var(--text-xs); color: var(--muted); }
355
+ .files-changed { color: var(--accent); }
356
+ .files-mark { flex: none; width: 1.25em; text-align: center; font-family: var(--font-mono); font-weight: var(--weight-bold); }
357
+ .files-mark[data-status="added"] { color: var(--add); }
358
+ .files-mark[data-status="modified"] { color: var(--accent); }
359
+ .files-mark[data-status="deleted"] { color: var(--del); }
360
+ .files-viewer-head { display: flex; align-items: center; gap: var(--space-1); }
361
+ .files-viewer-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
362
+ .files-viewer-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3); font-size: var(--text-xs); color: var(--muted); }
363
+ .files-code {
364
+ display: flex; min-width: 0; overflow: auto; max-height: none;
365
+ border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2);
366
+ font-family: var(--font-mono); font-size: var(--text-xs); line-height: var(--lh-xs);
367
+ }
368
+ .files-code pre { margin: 0; padding: var(--space-2) 0; background: none; border: 0; font: inherit; white-space: pre; }
369
+ .files-code .files-gutter { flex: none; position: sticky; left: 0; padding-inline: var(--space-2); text-align: right; color: var(--muted); background: var(--surface-2); border-right: 1px solid var(--line); user-select: none; }
370
+ .files-code .files-source { flex: 1; padding-inline: var(--space-3); }
371
+ .files-source code { display: block; background: none; padding: 0; color: var(--ink); }
372
+ .files-image { display: flex; justify-content: center; padding: var(--space-3); border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); }
373
+ .files-image img { max-width: 100%; height: auto; }
333
374
  .topbar-actions .side-pane-toggle[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); }
334
375
 
335
376
  /* ---- Header session actions (dot-menu: rename / open PR / delete) ---- */
@@ -0,0 +1,208 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ //
4
+ // Read-only file browser over a session's workspace (its worktree, or the
5
+ // directory it runs in): list one directory, read one file. Lets a person look
6
+ // at what the agent is working on before anything is committed or pushed. Every
7
+ // path is confined to the workspace — `..`, absolute paths and symlinks that
8
+ // resolve outside it are refused — and nothing here ever writes.
9
+ import { execFile } from "node:child_process";
10
+ import fs from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { promisify } from "node:util";
13
+ const exec = promisify(execFile);
14
+ /** Text past this is cut (and says so); a viewer, not an editor. */
15
+ export const MAX_TEXT_BYTES = 1024 * 1024;
16
+ export const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
17
+ /** A directory this big is cut so one listing can't flood the link. */
18
+ export const MAX_DIR_ENTRIES = 2000;
19
+ /** Never listed: git's own store says nothing about the work. */
20
+ const HIDDEN = new Set([".git"]);
21
+ const IMAGE_TYPES = {
22
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif",
23
+ ".webp": "image/webp", ".avif": "image/avif", ".bmp": "image/bmp", ".ico": "image/x-icon",
24
+ };
25
+ export class WorkspacePathError extends Error {
26
+ }
27
+ /** Normalise a client path to workspace-relative `a/b`, or throw if it climbs out. */
28
+ export function relativeWorkspacePath(input) {
29
+ const raw = (input ?? "").replace(/\\/g, "/").trim();
30
+ if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw))
31
+ throw new WorkspacePathError("Paths are relative to the workspace.");
32
+ const parts = [];
33
+ for (const part of raw.split("/")) {
34
+ if (!part || part === ".")
35
+ continue;
36
+ if (part === "..")
37
+ throw new WorkspacePathError("That path is outside the workspace.");
38
+ parts.push(part);
39
+ }
40
+ if (parts.some((part) => HIDDEN.has(part)))
41
+ throw new WorkspacePathError("That path is not browsable.");
42
+ return parts.join("/");
43
+ }
44
+ /** Resolve `rel` inside `root`, following symlinks, refusing anything that lands outside. */
45
+ async function confined(root, rel) {
46
+ const realRoot = await fs.realpath(root);
47
+ const abs = path.join(realRoot, rel);
48
+ const real = await fs.realpath(abs).catch((error) => {
49
+ if (error.code === "ENOENT")
50
+ throw new WorkspacePathError("That file no longer exists.");
51
+ throw error;
52
+ });
53
+ if (real !== realRoot && !real.startsWith(realRoot + path.sep))
54
+ throw new WorkspacePathError("That path is outside the workspace.");
55
+ return { abs, real, realRoot };
56
+ }
57
+ /** Uncommitted files by workspace-relative path; null when not a git checkout. */
58
+ export async function gitStatus(root) {
59
+ try {
60
+ const opts = { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 10_000 };
61
+ // Porcelain paths are repo-relative; the workspace may be a subdirectory.
62
+ const [{ stdout: prefix }, { stdout }] = await Promise.all([
63
+ exec("git", ["rev-parse", "--show-prefix"], opts),
64
+ exec("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "."], opts),
65
+ ]);
66
+ return parseGitStatus(stdout, prefix.trim());
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** `git status --porcelain=v1 -z` → path → status. A rename shows as its new path, added. */
73
+ export function parseGitStatus(stdout, prefix = "") {
74
+ const out = new Map();
75
+ const records = stdout.split("\0");
76
+ for (let i = 0; i < records.length; i++) {
77
+ const record = records[i];
78
+ if (record.length < 4)
79
+ continue;
80
+ const code = record.slice(0, 2);
81
+ const file = record.slice(3);
82
+ // Renames and copies carry the source path as the next record.
83
+ if (code.includes("R") || code.includes("C"))
84
+ i++;
85
+ if (code === "!!" || !file.startsWith(prefix))
86
+ continue;
87
+ out.set(file.slice(prefix.length), code === "??" || code.includes("A") || code.includes("R") || code.includes("C") ? "added" : code.includes("D") ? "deleted" : "modified");
88
+ }
89
+ return out;
90
+ }
91
+ export async function listWorkspaceDir(root, input) {
92
+ const rel = relativeWorkspacePath(input);
93
+ const { real } = await confined(root, rel);
94
+ const stat = await fs.stat(real);
95
+ if (!stat.isDirectory())
96
+ throw new WorkspacePathError("That path is not a folder.");
97
+ const [dirents, status] = await Promise.all([fs.readdir(real, { withFileTypes: true }), gitStatus(root)]);
98
+ const prefix = rel ? `${rel}/` : "";
99
+ const entries = [];
100
+ for (const dirent of dirents) {
101
+ if (HIDDEN.has(dirent.name))
102
+ continue;
103
+ const entryPath = prefix + dirent.name;
104
+ let isDir = dirent.isDirectory();
105
+ let size;
106
+ let mtime;
107
+ try {
108
+ const s = await fs.stat(path.join(real, dirent.name));
109
+ isDir = s.isDirectory();
110
+ size = isDir ? undefined : s.size;
111
+ mtime = s.mtimeMs;
112
+ }
113
+ catch { /* a dangling symlink: list it, reading it says why */ }
114
+ entries.push({
115
+ name: dirent.name,
116
+ path: entryPath,
117
+ type: isDir ? "dir" : "file",
118
+ ...(size !== undefined ? { size } : {}),
119
+ ...(mtime !== undefined ? { mtime: Math.round(mtime) } : {}),
120
+ ...(dirent.isSymbolicLink() ? { symlink: true } : {}),
121
+ });
122
+ }
123
+ // Deleted files are gone from disk but are part of what changed: show them here.
124
+ const present = new Set(entries.map((entry) => entry.name));
125
+ for (const [file, state] of status ?? []) {
126
+ if (state !== "deleted" || !file.startsWith(prefix))
127
+ continue;
128
+ const name = file.slice(prefix.length);
129
+ if (!name.includes("/") && !present.has(name))
130
+ entries.push({ name, path: file, type: "file", status: "deleted" });
131
+ }
132
+ for (const entry of entries) {
133
+ if (!status)
134
+ break;
135
+ if (entry.type === "file") {
136
+ const state = status.get(entry.path);
137
+ if (state)
138
+ entry.status = state;
139
+ }
140
+ else {
141
+ let changed = 0;
142
+ for (const file of status.keys())
143
+ if (file.startsWith(`${entry.path}/`))
144
+ changed++;
145
+ if (changed)
146
+ entry.changed = changed;
147
+ }
148
+ }
149
+ entries.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1));
150
+ return { path: rel, entries: entries.slice(0, MAX_DIR_ENTRIES), git: status !== null };
151
+ }
152
+ function looksBinary(bytes) {
153
+ const head = bytes.subarray(0, 8000);
154
+ return head.includes(0);
155
+ }
156
+ export async function readWorkspaceFile(root, input) {
157
+ const rel = relativeWorkspacePath(input);
158
+ if (!rel)
159
+ throw new WorkspacePathError("Choose a file.");
160
+ const { real } = await confined(root, rel);
161
+ const stat = await fs.stat(real);
162
+ if (!stat.isFile())
163
+ throw new WorkspacePathError("That path is not a file.");
164
+ const status = (await gitStatus(root))?.get(rel);
165
+ const base = { path: rel, size: stat.size, mtime: Math.round(stat.mtimeMs), ...(status ? { status } : {}) };
166
+ const mimeType = IMAGE_TYPES[path.extname(rel).toLowerCase()];
167
+ if (mimeType) {
168
+ if (stat.size > MAX_IMAGE_BYTES)
169
+ return { ...base, kind: "too-large" };
170
+ return { ...base, kind: "image", mimeType, data: (await fs.readFile(real)).toString("base64") };
171
+ }
172
+ const handle = await fs.open(real, "r");
173
+ try {
174
+ const length = Math.min(stat.size, MAX_TEXT_BYTES);
175
+ const bytes = Buffer.alloc(length);
176
+ await handle.read(bytes, 0, length, 0);
177
+ if (looksBinary(bytes))
178
+ return { ...base, kind: "binary" };
179
+ return { ...base, kind: "text", content: bytes.toString("utf8"), truncated: stat.size > MAX_TEXT_BYTES };
180
+ }
181
+ finally {
182
+ await handle.close();
183
+ }
184
+ }
185
+ /** `workspaceFor`: the directory a session works in, if this machine has it. */
186
+ export function createFileCommands(workspaceFor) {
187
+ const operations = {
188
+ "files.list": (root, msg) => listWorkspaceDir(root, typeof msg.path === "string" ? msg.path : undefined),
189
+ "files.read": async (root, msg) => ({ file: await readWorkspaceFile(root, String(msg.path ?? "")) }),
190
+ };
191
+ return Object.fromEntries(Object.entries(operations).map(([kind, run]) => [kind, async (msg, ctx) => {
192
+ try {
193
+ const root = workspaceFor(String(msg.sessionId ?? ""));
194
+ if (!root)
195
+ throw new WorkspacePathError("This session's workspace isn't on this machine.");
196
+ ctx.reply({ type: `${kind}.ok`, requestId: msg.requestId, ...await run(root, msg) });
197
+ }
198
+ catch (error) {
199
+ // Raw filesystem errors carry host paths; say what happened without them.
200
+ const code = error?.code;
201
+ const message = error instanceof WorkspacePathError ? error.message
202
+ : code === "ENOENT" ? "That file no longer exists."
203
+ : code === "EACCES" || code === "EPERM" ? "Permission denied."
204
+ : "Could not read that path.";
205
+ ctx.reply({ type: `${kind}.error`, requestId: msg.requestId, httpStatus: 400, error: message });
206
+ }
207
+ }]));
208
+ }
@@ -5,6 +5,7 @@
5
5
  export const CLIENT_COMMAND_ROUTES = [
6
6
  ...["list", "publish", "offers", "adopt", "open", "logs", "requests", "request", "runRequest", "data", "serverLog", "shot", "input", "menu", "present", "scenarios", "showMe", "mute", "reviewMode", "annotate", "clearNotes", "agentNotes", "notes", "share", "unshare", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
7
7
  { method: "post", path: "/api/artifacts/list", kind: "artifacts.list" },
8
+ ...["list", "read"].map((action) => ({ method: "post", path: `/api/files/${action}`, kind: `files.${action}` })),
8
9
  { method: "post", path: "/api/access/get", kind: "access.get" },
9
10
  { method: "post", path: "/api/tailnet/machines", kind: "tailnet.machines" },
10
11
  { method: "post", path: "/api/auth/credentials/native-preview", kind: "credentials.native.preview" },
@@ -13,6 +13,8 @@ export const CLIENT_COMMAND_SCHEMAS = {
13
13
  ping: Type.Object(request),
14
14
  "apps.list": Type.Object({ ...request, ...optionalSession }),
15
15
  "artifacts.list": Type.Object(request),
16
+ "files.list": Type.Object({ ...request, ...session, path: Type.Optional(Type.String({ maxLength: 4096 })) }),
17
+ "files.read": Type.Object({ ...request, ...session, path: Type.String({ minLength: 1, maxLength: 4096 }) }),
16
18
  "access.get": Type.Object(request),
17
19
  "tailnet.machines": Type.Object(request),
18
20
  "session.fork.local": Type.Object({ ...request, ...session, model: Type.Optional(Type.Object({ provider: Type.Optional(Type.String({ maxLength: 200 })), id: Type.String({ maxLength: 200 }) })) }),
package/dist/server.js CHANGED
@@ -41,6 +41,7 @@ import { AGENT_PROFILES } from "./agents/profiles.js";
41
41
  import { pngSize, reviewHint } from "./apps/review.js";
42
42
  import { createDisplayHost } from "./apps/display.js";
43
43
  import { createAppCommands } from "./controllers/app-commands.js";
44
+ import { createFileCommands } from "./controllers/file-commands.js";
44
45
  import { bindClientCommandRoutes } from "./http/client-command-routes.js";
45
46
  import { collectDiscoveredSessions, planNativeAdoption } from "./runtime/native-session-discovery.js";
46
47
  import { aggregateModelCatalog, catalogReplyEvent, mergeProviderCatalog } from "./runtime/model-catalog.js";
@@ -2385,6 +2386,15 @@ const RELAY_COMMANDS = {
2385
2386
  const meta = attachmentStore.readMeta(hash);
2386
2387
  ctx.reply({ type: "attachment.data", requestId, hash, mimeType: meta?.mimeType ?? "application/octet-stream", name: meta?.name, data: bytes.toString("base64") });
2387
2388
  },
2389
+ // Read-only browsing of a session's workspace: an open session's worktree,
2390
+ // else the one metadata remembers, else a live terminal run's directory.
2391
+ ...createFileCommands((id) => {
2392
+ const record = resolveSession(id);
2393
+ if (record)
2394
+ return harnessDirFor(record);
2395
+ const meta = metadata.getSession(id);
2396
+ return meta?.worktree || meta?.workspace || runTerms.liveRun(id)?.workspace;
2397
+ }),
2388
2398
  ...createArtifactCommands({
2389
2399
  sessionIds: () => new Set([...metadata.listSessions().map((session) => session.id), ...[...openSessions.values()].map((record) => record.id)]),
2390
2400
  scan: (id) => eventLog.scan(id, ["outbound-attachment"]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.20.8-staging.1",
3
+ "version": "0.20.8-staging.3",
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.",