@bivy/bivy 0.17.3-staging.13 → 0.17.3-staging.15

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>'
@@ -1271,6 +1271,7 @@ button.sheet-action:last-child { border-bottom: 0; }
1271
1271
  .fork-select-field select { width: 100%; padding: 10px 12px; border: 1px solid var(--line-strong); border-radius: var(--radius-md); background: var(--surface); color: var(--ink); }
1272
1272
  .fork-toggle { display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-size: 13px; color: var(--ink); }
1273
1273
  .fork-toggle input { width: 16px; height: 16px; }
1274
+ .fork-note { margin: 0; padding: var(--space-2) var(--space-3); color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); }
1274
1275
  .fork-error { margin: 4px 12px; padding: 8px 10px; border-radius: var(--radius-md); background: var(--surface-2); color: var(--danger); font-size: 12px; }
1275
1276
  .fork-warn { margin: 4px 12px; padding: 8px 10px; border-radius: var(--radius-md); background: var(--surface-2); color: var(--muted); font-size: 12px; }
1276
1277
  .fork-submit { width: calc(100% - 24px); margin: var(--space-2) var(--space-3); }
@@ -3202,7 +3203,7 @@ select.picker-search { appearance: auto; }
3202
3203
  .automation-history-view .autom-section-head > strong { min-width: 0; line-height: var(--lh-lg); }
3203
3204
  }
3204
3205
 
3205
- /* Device handoff (HandoffBanner): sits above the composer like the composer
3206
- itself, with one line of the other device's draft. */
3206
+ /* Handoff notes above the composer (HandoffBanner, StandbyNotice): shaped like
3207
+ the composer itself, with one line of another device's draft. */
3207
3208
  .handoff-banner { border-radius: var(--radius-md); }
3208
3209
  .handoff-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
package/dist/server.js CHANGED
@@ -1834,6 +1834,20 @@ const replication = new ReplicationService({
1834
1834
  },
1835
1835
  log: (m) => console.error(`[replication] ${m}`),
1836
1836
  });
1837
+ /** Take over a replicated session on this node (manual promotion). Once it
1838
+ * wins the epoch it is this node's session, not a standby copy: retag it so
1839
+ * every client stops offering "Continue here", and push the new list. */
1840
+ async function promoteReplicaHere(sessionId) {
1841
+ const epoch = await replication.promote(sessionId, identity.nodeId);
1842
+ if (epoch === undefined)
1843
+ return undefined;
1844
+ const source = metadata.getSession(sessionId)?.source;
1845
+ if (source?.startsWith("replica"))
1846
+ metadata.upsertSession({ id: sessionId, source: source.replace(/^replica/, "promoted") });
1847
+ broadcastSessionsList();
1848
+ scheduleAdvertise();
1849
+ return epoch;
1850
+ }
1837
1851
  // Retire the legacy overlay sidecars. The append-only log is the sole overlay
1838
1852
  // store now and every session was migrated into it by the prior release's boot
1839
1853
  // migration (.migrated-overlays-v1), verified complete on-disk before this landed.
@@ -1973,6 +1987,12 @@ const appService = new AppService(appRegistry, appGateway ?? remotePreview, {
1973
1987
  has: (id) => terminals.has(id),
1974
1988
  close: (id) => { terminals.close(id); },
1975
1989
  }, { screenshots: { enabled: appScreenshotsEnabled }, displays: appDisplays });
1990
+ // A reviewer's note shows up in an open Apps sheet without reopening it.
1991
+ appRegistry.on("notes", (viewId) => {
1992
+ const entry = appRegistry.getView(viewId);
1993
+ if (entry)
1994
+ broadcast({ type: "apps.changed", sessionId: entry.app.sessionId });
1995
+ });
1976
1996
  const RELAY_COMMANDS = {
1977
1997
  ...createAppCommands(appService, (id) => { const record = resolveSession(id); return record ? harnessDirFor(record) : undefined; }, (app) => {
1978
1998
  const record = resolveSession(app.sessionId);
@@ -2779,10 +2799,9 @@ const RELAY_COMMANDS = {
2779
2799
  const sessionId = String(msg.sessionId ?? "").trim();
2780
2800
  if (!sessionId)
2781
2801
  return ctx.reply({ type: "session.promote.result", requestId, ok: false, error: "Missing sessionId" });
2782
- const epoch = await replication.promote(sessionId, identity.nodeId);
2802
+ const epoch = await promoteReplicaHere(sessionId);
2783
2803
  if (epoch === undefined)
2784
2804
  return ctx.reply({ type: "session.promote.result", requestId, ok: false, error: "Promotion lost the epoch race" });
2785
- scheduleAdvertise();
2786
2805
  ctx.reply({ type: "session.promote.result", requestId, ok: true, sessionId, epoch });
2787
2806
  },
2788
2807
  // Ephemeral provisioning transport (node-broker path). A remote device that
@@ -11265,10 +11284,9 @@ app.post("/api/session/promote", async (req, res, next) => {
11265
11284
  const sessionId = String(req.body?.sessionId ?? "").trim();
11266
11285
  if (!sessionId)
11267
11286
  return res.status(400).json({ error: "Missing sessionId" });
11268
- const epoch = await replication.promote(sessionId, identity.nodeId);
11287
+ const epoch = await promoteReplicaHere(sessionId);
11269
11288
  if (epoch === undefined)
11270
11289
  return res.status(409).json({ error: "Promotion lost the epoch race (another node owns it, or it isn't replicated)" });
11271
- scheduleAdvertise();
11272
11290
  res.json({ ok: true, epoch });
11273
11291
  }
11274
11292
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.17.3-staging.13",
3
+ "version": "0.17.3-staging.15",
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.",