@bivy/bivy 0.17.3-staging.14 → 0.17.3-staging.16

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.
@@ -32,35 +32,20 @@ const selector=el=>{
32
32
  }
33
33
  return parts.join(' > ')||el.localName;
34
34
  };
35
- let box=null,target=null,onPicked=post,host=null;
36
- // Picks on pointerup: iOS never fires click for a tap on a non-clickable
37
- // element when only window listens, so click would miss most of the page.
38
- // While pointing, the app sees none of the gesture, including the click after.
39
- const SWALLOW=['pointerdown','mousedown','mouseup','touchend','click','dblclick','contextmenu'];
40
- const swallow=e=>{e.preventDefault();e.stopPropagation();};
41
- // A second listener, so stopping (which the shell does right after a pick)
42
- // doesn't also drop the guard on the rest of the tap. The tap's click ends it.
43
- const aftermath=e=>{swallow(e);if(e.type==='click')listen(false,aftermath);};
44
- const listen=(on,handler=swallow)=>{
45
- const method=on?addEventListener:removeEventListener;
46
- if(handler===swallow){method('pointermove',hover,true);method('pointerup',pick,true);method('keydown',key,true);}
47
- for(const type of SWALLOW)method(type,handler,true);
48
- };
49
- const stop=()=>{box?.remove();box=null;target=null;listen(false);};
35
+ let box=null,layer=null,onPicked=post,host=null;
36
+ // While pointing, a clear layer over the page takes every tap itself, so the
37
+ // app never sees one and no browser's event order matters (iOS sends no click
38
+ // to a window listener for most taps). The element is looked up beneath it.
39
+ const stop=()=>{box?.remove();layer?.remove();box=layer=null;removeEventListener('keydown',key,true);};
40
+ const under=e=>document.elementsFromPoint(e.clientX,e.clientY).find(el=>el!==layer&&el!==box&&el!==host);
50
41
  const hover=e=>{
51
- const el=document.elementFromPoint(e.clientX,e.clientY);
52
- if(!el||el===box||el===host)return;target=el;const r=el.getBoundingClientRect();
42
+ const el=under(e);if(!el)return;const r=el.getBoundingClientRect();
53
43
  Object.assign(box.style,{left:r.left+'px',top:r.top+'px',width:r.width+'px',height:r.height+'px'});
54
44
  };
55
45
  const pick=e=>{
56
- swallow(e);
57
- // The event's own target, not a coordinate lookup: iOS gets elementFromPoint
58
- // wrong in a frame of a zoomed page. Found nothing? Keep pointing.
59
- const el=e.target instanceof Element?e.target:document.elementFromPoint(e.clientX,e.clientY)||target;
60
- if(!el)return;stop();
61
- // Keep eating the click (and iOS's delayed mouse events) this tap still fires.
62
- listen(true,aftermath);setTimeout(()=>listen(false,aftermath),500);
63
- if(el===host){onPicked({type:'picked',cancelled:true});return;}const r=el.getBoundingClientRect();
46
+ e.preventDefault();
47
+ const el=under(e);if(!el)return;stop();
48
+ const r=el.getBoundingClientRect();
64
49
  onPicked({type:'picked',selector:selector(el),tag:el.localName,text:(el.innerText||el.getAttribute('aria-label')||el.getAttribute('alt')||'').trim().replace(/\\s+/g,' ').slice(0,200),
65
50
  rect:{x:Math.round(r.left),y:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height)},
66
51
  viewport:{width:innerWidth,height:innerHeight},path:location.pathname+location.search+location.hash});
@@ -71,8 +56,13 @@ function startPointing(){
71
56
  box=document.createElement('div');
72
57
  box.setAttribute('aria-hidden','true');
73
58
  Object.assign(box.style,{position:'fixed',zIndex:2147483647,pointerEvents:'none',outline:'2px solid Highlight',outlineOffset:'2px',borderRadius:'4px',transition:'all 60ms ease-out'});
74
- document.documentElement.append(box);
75
- listen(true);
59
+ // One below the reviewer dock, so its controls stay usable while pointing.
60
+ layer=document.createElement('div');
61
+ layer.setAttribute('aria-hidden','true');
62
+ Object.assign(layer.style,{position:'fixed',inset:0,zIndex:2147483646,cursor:'crosshair',background:'transparent',touchAction:'manipulation',userSelect:'none',webkitUserSelect:'none',webkitTouchCallout:'none'});
63
+ layer.addEventListener('pointermove',hover);layer.addEventListener('pointerdown',hover);layer.addEventListener('click',pick);
64
+ document.documentElement.append(layer,box);
65
+ addEventListener('keydown',key,true);
76
66
  }
77
67
  addEventListener('message',e=>{
78
68
  if(!framed||e.origin!==SHELL||e.source!==parent||e.data?.type!=='bivy:point')return;
@@ -87,7 +77,8 @@ if(REVIEWER&&!framed){
87
77
  +'button{border:1px solid ButtonBorder;background:ButtonFace;color:ButtonText;border-radius:999px;padding:10px 14px;font-weight:600;cursor:pointer;min-height:44px}'
88
78
  +'.primary{background:CanvasText;color:Canvas;border-color:CanvasText}'
89
79
  +'form{width:min(340px,calc(100vw - 24px));background:Canvas;color:CanvasText;border:1px solid GrayText;border-radius:14px;padding:12px;box-shadow:0 8px 30px rgb(0 0 0/.25)}'
90
- +'textarea{width:100%;min-height:5em;margin:8px 0;border:1px solid GrayText;border-radius:8px;padding:8px;background:Canvas;color:CanvasText;resize:vertical}'
80
+ // 16px: smaller, and iOS zooms the page in when the note box is focused.
81
+ +'textarea{width:100%;min-height:5em;margin:8px 0;border:1px solid GrayText;border-radius:8px;padding:8px;background:Canvas;color:CanvasText;resize:vertical;font-size:16px}'
91
82
  +'.row{display:flex;justify-content:flex-end;gap:8px}.muted{color:GrayText;font-size:12px;overflow-wrap:anywhere}[hidden]{display:none!important}</style>'
92
83
  +'<div class="dock"><p class="muted" role="status" id="status" hidden></p>'
93
84
  +'<form id="form" hidden><label for="note">Your note</label><div class="muted" id="about"></div><textarea id="note" required maxlength="1000"></textarea>'
@@ -198,7 +198,7 @@ export class AppRegistry extends EventEmitter {
198
198
  return structuredClone(app);
199
199
  }
200
200
  list(sessionId) {
201
- return structuredClone([...this.apps.values()].filter((app) => app.sessionId === sessionId));
201
+ return structuredClone([...this.apps.values()].filter((app) => sessionId === undefined || app.sessionId === sessionId));
202
202
  }
203
203
  getView(id) { return this.views.get(id); }
204
204
  /** An agent turn changed files: re-take changed static snapshots and bump
@@ -35,6 +35,7 @@ export class AppService {
35
35
  this.screenshots = { enabled: options.screenshots?.enabled ?? (() => false), take: options.screenshots?.take ?? takeShots };
36
36
  this.displays = options.displays ?? NO_DISPLAYS;
37
37
  }
38
+ /** A session's apps, or every app on this machine when no session is given. */
38
39
  list(sessionId) {
39
40
  const available = Boolean(this.gateway) && this.gateway?.available !== false;
40
41
  const apps = this.registry.list(sessionId);
@@ -588,6 +588,12 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
588
588
  discoverability pattern as .row-menu-btn — and a touch-friendly baseline
589
589
  opacity on devices with no hover. */
590
590
  .assistant-row { align-self: stretch; width: 100%; max-width: 680px; }
591
+ /* The message "Show in chat" landed on (Artifacts / Apps pages). */
592
+ .is-jump-target { border-radius: var(--radius-md); animation: jump-target 2.4s var(--ease-standard); }
593
+ @keyframes jump-target {
594
+ 0%, 50% { background: color-mix(in srgb, var(--accent) 10%, transparent); box-shadow: 0 0 0 var(--space-2) color-mix(in srgb, var(--accent) 10%, transparent); }
595
+ 100% { background: transparent; box-shadow: 0 0 0 var(--space-2) transparent; }
596
+ }
591
597
  .msg-actions { display: flex; align-items: center; gap: 2px; margin-top: var(--space-1); margin-left: -6px; }
592
598
  .msg-copy-btn, .msg-speak-btn {
593
599
  border: 0; background: transparent; color: var(--muted);
@@ -1412,6 +1418,62 @@ button.sheet-action:last-child { border-bottom: 0; }
1412
1418
  .automations-view-head-actions > .btn.primary { padding: 9px 18px; }
1413
1419
  .automations-view-body { padding: 8px 24px max(48px, env(safe-area-inset-bottom)); }
1414
1420
  }
1421
+ /* Artifacts / Apps pages (LibraryView.tsx) share the Automations page shell:
1422
+ a quiet image grid for files, a plain list for apps. */
1423
+ .library-view:focus { outline: none; }
1424
+ .library-view .automations-view-head { width: 100%; max-width: 1080px; margin: 0 auto; box-sizing: border-box; }
1425
+ .library-body { max-width: 1080px; }
1426
+ .library-toolbar { display: flex; margin: var(--space-2) 0 var(--space-4); }
1427
+ .library-loading { display: flex; justify-content: center; padding: var(--space-8) 0; }
1428
+ .library-body > .banner { margin: var(--space-2) 0; }
1429
+ .library-note { margin: var(--space-2) 0; font-size: var(--text-sm); color: var(--muted); }
1430
+ .library-grid {
1431
+ list-style: none; margin: var(--space-2) 0 0; padding: 0;
1432
+ display: grid; gap: var(--space-5) var(--space-3);
1433
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
1434
+ }
1435
+ .library-tile { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
1436
+ .library-tile-preview {
1437
+ width: 100%; aspect-ratio: 4 / 3; padding: 0; overflow: hidden;
1438
+ display: grid; place-items: center;
1439
+ border: 1px solid var(--line); border-radius: var(--radius-lg);
1440
+ background: var(--surface-2); color: var(--muted); font: inherit; cursor: pointer;
1441
+ transition: border-color var(--dur-fast) var(--ease-standard);
1442
+ }
1443
+ .library-tile-preview:hover:not(:disabled) { border-color: var(--line-strong); }
1444
+ .library-tile-preview:disabled { cursor: progress; }
1445
+ .library-tile-preview img { display: block; width: 100%; height: 100%; object-fit: cover; }
1446
+ .library-tile-file { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); }
1447
+ .library-tile-ext { font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: 0.04em; }
1448
+ .library-tile-caption { display: flex; flex-direction: column; gap: 2px; min-width: 0; padding: 0 2px; }
1449
+ .library-tile-name, .library-row-name {
1450
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1451
+ font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--ink);
1452
+ }
1453
+ .library-tile-note { font-size: var(--text-xs); color: var(--danger); }
1454
+ .library-source {
1455
+ align-self: flex-start; display: inline-flex; align-items: center; gap: var(--space-1);
1456
+ max-width: 100%; min-width: 0; min-height: 28px; padding: 0; border: 0; background: transparent;
1457
+ color: var(--muted); font: inherit; font-size: var(--text-xs); text-align: left; cursor: pointer;
1458
+ }
1459
+ .library-source:hover { color: var(--ink); }
1460
+ .library-source svg, .library-source-when { flex: none; }
1461
+ .library-source-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1462
+ .library-list { list-style: none; margin: 0; padding: 0; }
1463
+ .library-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) 0; border-bottom: 1px solid var(--line); }
1464
+ .library-row:last-child { border-bottom: 0; }
1465
+ .library-row-icon {
1466
+ flex: none; width: 40px; height: 40px; display: grid; place-items: center;
1467
+ border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-2);
1468
+ color: var(--ink); font-weight: var(--weight-semibold);
1469
+ }
1470
+ .library-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
1471
+ .library-row-name { font-size: var(--text-base); }
1472
+ .library-row-kind { color: var(--muted); font-weight: var(--weight-normal); }
1473
+ @media (min-width: 640px) {
1474
+ .library-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--space-6) var(--space-4); }
1475
+ }
1476
+
1415
1477
  .autom-section { margin-top: 26px; }
1416
1478
  .autom-section-head {
1417
1479
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
@@ -6,7 +6,7 @@ export function createAppCommands(service, workspaceFor, published = () => { })
6
6
  return workspace;
7
7
  };
8
8
  const operations = {
9
- "apps.list": (msg) => service.list(String(msg.sessionId)),
9
+ "apps.list": (msg) => service.list(typeof msg.sessionId === "string" ? msg.sessionId : undefined),
10
10
  "apps.publish": (msg) => {
11
11
  const sessionId = String(msg.sessionId);
12
12
  const app = service.publish(sessionId, workspaceOf(sessionId), msg.manifest);
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ //
4
+ // The machine-wide Artifacts index behind the sidebar's Artifacts page: every
5
+ // file an agent sent into a chat (`bivy attach` / `attach_to_chat`), across all
6
+ // of this machine's sessions. Built from the same event-log records attachment
7
+ // GC already sweeps, so there is no second index to keep in sync. Only what can
8
+ // still be opened is listed: the session exists and the blob is still stored.
9
+ /** Newest first, bounded like the per-session projection. */
10
+ export const MAX_MACHINE_ARTIFACTS = 500;
11
+ /** One entry per content hash — the newest send wins, so the same report sent
12
+ * to two sessions links to where it was sent last. */
13
+ export function listMachineArtifacts(deps) {
14
+ const byHash = new Map();
15
+ for (const sessionId of deps.sessionIds()) {
16
+ for (const record of deps.scan(sessionId)) {
17
+ if (record.bivyKind !== "outbound-attachment")
18
+ continue;
19
+ const { ref } = record;
20
+ const existing = byHash.get(ref.hash);
21
+ if (existing && existing.createdAt >= record.createdAt)
22
+ continue;
23
+ byHash.set(ref.hash, {
24
+ sessionId, hash: ref.hash, name: ref.name, mimeType: ref.mimeType, kind: ref.kind, size: ref.size,
25
+ ...(record.caption ? { caption: record.caption } : {}),
26
+ createdAt: record.createdAt,
27
+ artifact: Boolean(record.artifact || existing?.artifact),
28
+ });
29
+ }
30
+ }
31
+ return [...byHash.values()]
32
+ .filter((item) => deps.stored(item.hash))
33
+ .sort((a, b) => b.createdAt - a.createdAt)
34
+ .slice(0, MAX_MACHINE_ARTIFACTS);
35
+ }
36
+ export function createArtifactCommands(deps) {
37
+ return {
38
+ "artifacts.list"(msg, ctx) {
39
+ ctx.reply({ type: "artifacts.list.ok", requestId: msg.requestId, artifacts: listMachineArtifacts(deps) });
40
+ },
41
+ };
42
+ }
@@ -4,6 +4,7 @@
4
4
  * client command registry. Relay and HTTP now differ only in framing. */
5
5
  export const CLIENT_COMMAND_ROUTES = [
6
6
  ...["list", "publish", "offers", "adopt", "open", "logs", "shot", "clearNotes", "share", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
7
+ { method: "post", path: "/api/artifacts/list", kind: "artifacts.list" },
7
8
  { method: "post", path: "/api/auth/credentials/native-preview", kind: "credentials.native.preview" },
8
9
  { method: "post", path: "/api/auth/credentials/native-import", kind: "credentials.native.import" },
9
10
  { method: "post", path: "/api/session/pause", kind: "session.pause" },
@@ -8,7 +8,8 @@ const optionalSession = { sessionId: Type.Optional(Type.String()) };
8
8
  * this table remains migration-compatible; adding validation is one data row. */
9
9
  export const CLIENT_COMMAND_SCHEMAS = {
10
10
  ping: Type.Object(request),
11
- "apps.list": Type.Object({ ...request, ...session }),
11
+ "apps.list": Type.Object({ ...request, ...optionalSession }),
12
+ "artifacts.list": Type.Object(request),
12
13
  "apps.publish": Type.Object({ ...request, ...session, manifest: Type.Unknown() }),
13
14
  "apps.offers": Type.Object({ ...request, ...session }),
14
15
  "apps.adopt": Type.Object({ ...request, ...session, port: Type.Integer({ minimum: 1024, maximum: 65535 }) }),
package/dist/server.js CHANGED
@@ -115,6 +115,7 @@ import { createCapabilitiesController } from "./controllers/capabilities.js";
115
115
  import { createAccessDeviceController, createLinkedDeviceController } from "./controllers/devices.js";
116
116
  import { createSessionControlCommands } from "./controllers/session-control.js";
117
117
  import { createPresenceCommands } from "./controllers/presence-commands.js";
118
+ import { createArtifactCommands } from "./controllers/artifact-commands.js";
118
119
  import { PresenceBook, deviceFrom } from "./session/presence.js";
119
120
  import { createForkCommands } from "./controllers/fork-commands.js";
120
121
  import { createGithubCommands } from "./controllers/github-commands.js";
@@ -1987,6 +1988,12 @@ const appService = new AppService(appRegistry, appGateway ?? remotePreview, {
1987
1988
  has: (id) => terminals.has(id),
1988
1989
  close: (id) => { terminals.close(id); },
1989
1990
  }, { screenshots: { enabled: appScreenshotsEnabled }, displays: appDisplays });
1991
+ // A reviewer's note shows up in an open Apps sheet without reopening it.
1992
+ appRegistry.on("notes", (viewId) => {
1993
+ const entry = appRegistry.getView(viewId);
1994
+ if (entry)
1995
+ broadcast({ type: "apps.changed", sessionId: entry.app.sessionId });
1996
+ });
1990
1997
  const RELAY_COMMANDS = {
1991
1998
  ...createAppCommands(appService, (id) => { const record = resolveSession(id); return record ? harnessDirFor(record) : undefined; }, (app) => {
1992
1999
  const record = resolveSession(app.sessionId);
@@ -2029,6 +2036,11 @@ const RELAY_COMMANDS = {
2029
2036
  const meta = attachmentStore.readMeta(hash);
2030
2037
  ctx.reply({ type: "attachment.data", requestId, hash, mimeType: meta?.mimeType ?? "application/octet-stream", name: meta?.name, data: bytes.toString("base64") });
2031
2038
  },
2039
+ ...createArtifactCommands({
2040
+ sessionIds: () => new Set([...metadata.listSessions().map((session) => session.id), ...[...openSessions.values()].map((record) => record.id)]),
2041
+ scan: (id) => eventLog.scan(id, ["outbound-attachment"]),
2042
+ stored: (hash) => attachmentStore.getPath(hash) !== null,
2043
+ }),
2032
2044
  ...createPresenceCommands(presenceBook, publishPresence),
2033
2045
  ...createSessionControlCommands({
2034
2046
  resolve: (sessionId) => resolveSession(sessionId),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.17.3-staging.14",
3
+ "version": "0.17.3-staging.16",
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.",