@bivy/bivy 0.17.3-staging.22 → 0.17.3-staging.24

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.
@@ -42,14 +42,27 @@ const hover=e=>{
42
42
  const el=under(e);if(!el)return;const r=el.getBoundingClientRect();
43
43
  Object.assign(box.style,{left:r.left+'px',top:r.top+'px',width:r.width+'px',height:r.height+'px'});
44
44
  };
45
- const pick=e=>{
46
- e.preventDefault();
47
- const el=under(e);if(!el)return;stop();
45
+ const report=(el,hold)=>{
48
46
  const r=el.getBoundingClientRect();
49
47
  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),
50
48
  rect:{x:Math.round(r.left),y:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height)},
51
- viewport:{width:innerWidth,height:innerHeight},path:location.pathname+location.search+location.hash});
49
+ viewport:{width:innerWidth,height:innerHeight},path:location.pathname+location.search+location.hash,hold});
50
+ };
51
+ // A long press picks at once and says so (the shell starts listening); the
52
+ // layer stays until the finger lifts, so the release never reaches the app.
53
+ let press=null,held=false;
54
+ const pick=e=>{
55
+ e.preventDefault();
56
+ if(held){held=false;stop();return;}
57
+ const el=under(e);if(!el)return;stop();report(el,false);
58
+ };
59
+ const down=e=>{
60
+ hover(e);clearTimeout(press?.timer);held=false;
61
+ const x=e.clientX,y=e.clientY;
62
+ press={x,y,timer:setTimeout(()=>{const el=under({clientX:x,clientY:y});if(!el)return;held=true;box?.remove();report(el,true);},450)};
52
63
  };
64
+ const move=e=>{hover(e);if(press&&Math.hypot(e.clientX-press.x,e.clientY-press.y)>10)clearTimeout(press.timer);};
65
+ const up=()=>{clearTimeout(press?.timer);press=null;if(held){post({type:'release'});setTimeout(()=>{if(held){held=false;stop();}},0);}};
53
66
  const key=e=>{if(e.key==='Escape'){stop();onPicked({type:'picked',cancelled:true});}};
54
67
  function startPointing(){
55
68
  stop();
@@ -60,13 +73,15 @@ function startPointing(){
60
73
  layer=document.createElement('div');
61
74
  layer.setAttribute('aria-hidden','true');
62
75
  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);
76
+ layer.addEventListener('pointermove',move);layer.addEventListener('pointerdown',down);layer.addEventListener('pointerup',up);layer.addEventListener('pointercancel',up);layer.addEventListener('click',pick);
77
+ layer.addEventListener('contextmenu',e=>e.preventDefault());
64
78
  document.documentElement.append(layer,box);
65
79
  addEventListener('keydown',key,true);
66
80
  }
67
81
  addEventListener('message',e=>{
68
82
  if(!framed||e.origin!==SHELL||e.source!==parent||e.data?.type!=='bivy:point')return;
69
- if(e.data.on)startPointing();else stop();
83
+ // Held: the layer goes when the finger lifts (see up).
84
+ if(e.data.on)startPointing();else if(!held)stop();
70
85
  });
71
86
  if(REVIEWER&&!framed){
72
87
  // A shadow root keeps the app's CSS off these controls (and ours off the app).
@@ -43,6 +43,15 @@ nav .btn[aria-pressed="true"] { background:var(--accent-soft); color:var(--accen
43
43
  #entries li { display:flex; gap:var(--space-2); align-items:baseline; padding:var(--space-1) 0; border-top:thin solid var(--line); overflow-wrap:anywhere; }
44
44
  /* Keeps .field's 16px: smaller, and iOS zooms the shell on focus and stays zoomed. */
45
45
  #draft-text { width:100%; min-height:5em; box-sizing:border-box; resize:vertical; }
46
+ #draft-row { display:flex; align-items:flex-start; gap:var(--space-2); }
47
+ #draft-row #draft-text { flex:1; min-width:0; }
48
+ /* Hold to talk: a phone-sized target that never selects text or opens a callout. */
49
+ #mic { flex:none; inline-size:44px; block-size:44px; padding:0; border-radius:var(--radius-full); touch-action:none; user-select:none; -webkit-user-select:none; -webkit-touch-callout:none; }
50
+ #mic[aria-pressed="true"] { background:var(--accent); border-color:var(--accent); color:var(--accent-contrast); }
51
+ #voice-status { font-size:var(--text-xs); margin:var(--space-2) 0 0; }
52
+ #voice-status:empty { display:none; }
53
+ /* The panel scrolls on a phone; its actions stay in reach at the bottom. */
54
+ #draft .panel-actions { position:sticky; bottom:calc(-1 * var(--space-3)); margin:var(--space-2) calc(-1 * var(--space-3)) calc(-1 * var(--space-3)); padding:var(--space-2) var(--space-3) var(--space-3); background:var(--surface); border-top:thin solid var(--line); }
46
55
  #draft-hint { font-size:var(--text-xs); margin:var(--space-2) 0 var(--space-1); }
47
56
  #draft-context { margin:0; padding:var(--space-2); background:var(--surface-2); border-radius:var(--radius-md); font-family:var(--font-mono); font-size:var(--text-xs); white-space:pre-wrap; overflow-wrap:anywhere; color:var(--muted); }
48
57
  #show { position:fixed; right:var(--space-3); bottom:calc(var(--space-3) + env(safe-area-inset-bottom)); z-index:var(--z-sticky); border-radius:var(--radius-full); box-shadow:var(--shadow-lg); }
@@ -67,12 +76,14 @@ nav .btn[aria-pressed="true"] { background:var(--accent-soft); color:var(--accen
67
76
  </section>
68
77
  <section class="panel" id="draft" hidden aria-labelledby="draft-title">
69
78
  <h2 id="draft-title">Draft for the agent</h2>
70
- <textarea class="field" id="draft-text" aria-label="What should change?" placeholder="What should change?"></textarea>
79
+ <div id="draft-row"><textarea class="field" id="draft-text" aria-label="What should change?" placeholder="What should change?"></textarea>
80
+ <button class="btn" id="mic" type="button" hidden aria-pressed="false" aria-label="Speak" title="Hold to speak, or tap to start and stop"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15.5a3 3 0 003-3V6a3 3 0 10-6 0v6.5a3 3 0 003 3z"/><path d="M6 11.5v1a6 6 0 0012 0v-1M12 18.5V21M9 21h6"/></svg></button></div>
81
+ <p class="muted" id="voice-status" role="status"></p>
71
82
  <p class="muted" id="draft-hint">Sent with this context from the preview. Nothing is sent until you send it in the chat.</p>
72
83
  <pre id="draft-context"></pre>
73
84
  <div class="panel-actions"><button class="btn sm ghost" id="draft-cancel">Cancel</button><button class="btn sm primary" id="draft-add">Add to chat</button></div>
74
85
  </section>
75
- <p class="banner inline" data-tone="accent" id="pointing" role="status" hidden>Tap anything in the app to point at it. Press Escape or Point again to stop.</p>
86
+ <p class="banner inline" data-tone="accent" id="pointing" role="status" hidden>Tap anything in the app to point at it. <span id="pointing-voice" hidden>Hold to point and speak. </span>Press Escape or Point again to stop.</p>
76
87
  <nav aria-label="Bivy preview controls">
77
88
  <button class="btn sm ghost" id="back" aria-label="Back to chat">‹ <span class="label-wide">Back to chat</span><span class="label-narrow">Chat</span></button>
78
89
  <span id="name"><span id="title">App preview</span><span id="stamp" class="muted" role="status"></span></span>
@@ -115,6 +126,8 @@ function show(data,launch){
115
126
  for(const b of [reload,$('point'),$('errors')])b.disabled=false;
116
127
  status.textContent='Loading app…';
117
128
  frame.onload=()=>{status.hidden=true;void loadCompare();if(revision===null){revision=-1;watch();}else if(wake)wake();};
129
+ // A Bivy client framing the shell says whether it can take dictation.
130
+ if(embedded&&data.returnTo)toBivy({type:'hello'});
118
131
  // Store navigation metadata only, never tickets or cookies.
119
132
  try{sessionStorage.setItem(storageKey,JSON.stringify(data));}catch{}
120
133
  }
@@ -143,15 +156,49 @@ function toChat(text){
143
156
  const to=new URL(metadata.returnTo),session=to.pathname.split('/').pop();
144
157
  location.assign(to.origin+'/share?session='+encodeURIComponent(session)+'&text='+encodeURIComponent(text));
145
158
  }
146
- function draft(context){
159
+ function draft(context,listen){
147
160
  panels(null);
148
161
  $('draft').hidden=false;
149
162
  $('draft-add').textContent=metadata.returnTo?'Add to chat':'Copy';
150
163
  $('draft-context').textContent=context;
151
- const box=$('draft-text');box.value='';box.focus();
164
+ const box=$('draft-text');box.value='';
165
+ // Speaking right away: don't raise the keyboard over the draft.
166
+ if(listen)startListening();else box.focus();
152
167
  }
153
- $('draft-cancel').onclick=()=>{$('draft').hidden=true;};
168
+ // Point and speak. The Bivy client framing this shell does the listening:
169
+ // audio never reaches this origin, and only the transcript comes back, into
170
+ // the draft box, where it stays editable. Hold to talk, or tap to start and
171
+ // tap again to stop.
172
+ const mic=$('mic'),voiceStatus=$('voice-status');
173
+ let voice=false,listening=false,heldAt=0;
174
+ const sayVoice=text=>{voiceStatus.textContent=text;};
175
+ function setListening(on){listening=on;mic.setAttribute('aria-pressed',String(on));mic.setAttribute('aria-label',on?'Stop and add what you said':'Speak');}
176
+ function startListening(){if(!voice||listening)return;setListening(true);sayVoice('Listening… let go, or tap the mic again, to add what you said.');toBivy({type:'listen',state:'start'});}
177
+ function stopListening(){if(!listening)return;sayVoice('Turning speech into text…');toBivy({type:'listen',state:'stop'});}
178
+ function cancelListening(){if(listening)toBivy({type:'listen',state:'cancel'});setListening(false);sayVoice('');}
179
+ function addSpoken(text){
180
+ const box=$('draft-text'),said=String(text||'').trim().slice(0,4000);
181
+ if(!said)return;
182
+ box.value=box.value.trim()?box.value.trimEnd()+' '+said:said;
183
+ sayVoice('Added what you said. Edit it, or Add to chat.');
184
+ }
185
+ mic.onpointerdown=e=>{
186
+ e.preventDefault();
187
+ if(listening){stopListening();heldAt=0;return;}
188
+ heldAt=Date.now();try{mic.setPointerCapture(e.pointerId);}catch{}
189
+ startListening();
190
+ };
191
+ // A short tap keeps listening until the next tap; a hold stops on release.
192
+ mic.onpointerup=mic.onpointercancel=()=>{if(heldAt&&Date.now()-heldAt>400)stopListening();heldAt=0;};
193
+ mic.onclick=e=>{if(e.detail===0){if(listening)stopListening();else startListening();}};
194
+ function fromBivy(d){
195
+ if(d.type==='voice'){voice=d.available===true;mic.hidden=!voice;$('pointing-voice').hidden=!voice;if(!voice)cancelListening();}
196
+ else if(d.type==='transcript'){setListening(false);if(typeof d.error==='string')sayVoice(d.error.slice(0,200));else addSpoken(d.text);}
197
+ else if(d.type==='listening'&&d.on===false&&listening){setListening(false);if(voiceStatus.textContent.startsWith('Listening'))sayVoice('');}
198
+ }
199
+ $('draft-cancel').onclick=()=>{cancelListening();$('draft').hidden=true;};
154
200
  $('draft-add').onclick=async()=>{
201
+ cancelListening();
155
202
  const note=$('draft-text').value.trim(),text=(note?note+'\\n\\n':'')+$('draft-context').textContent;
156
203
  if(metadata.returnTo)return toChat(text);
157
204
  try{await navigator.clipboard.writeText(text);$('draft-add').textContent='Copied';}catch{$('draft-text').select();}
@@ -171,23 +218,26 @@ $('clear').onclick=resetConsole;
171
218
  $('send-errors').onclick=()=>draft('Console output in the app preview "'+metadata.name+'" (page '+currentPath+'):\\n'+entries.slice(-20).map(e=>'- '+e.level+': '+e.text).join('\\n'));
172
219
  // Point and tell
173
220
  const point=$('point');
174
- function pointing(on){point.setAttribute('aria-pressed',String(on));$('pointing').hidden=!on;frame.contentWindow?.postMessage({type:'bivy:point',on},metadata.origin);}
221
+ // Pointing again starts a new draft: panels over the app step aside.
222
+ function pointing(on){if(on)panels(null);point.setAttribute('aria-pressed',String(on));$('pointing').hidden=!on;frame.contentWindow?.postMessage({type:'bivy:point',on},metadata.origin);}
175
223
  point.onclick=()=>pointing(point.getAttribute('aria-pressed')!=='true');
176
224
  function picked(d){
177
225
  pointing(false);
178
226
  if(d.cancelled)return;
227
+ // A long press: the draft opens listening, and letting go stops.
228
+ if(d.hold&&voice)heldAt=Date.now();
179
229
  const r=d.rect||{},v=d.viewport||{};
180
230
  const errors=entries.filter(e=>e.level==='error').slice(-5);
181
231
  draft('In the app preview "'+metadata.name+'" (page '+safePath(d.path)+', viewport '+v.width+'×'+v.height+'):\\n'
182
232
  +'Element: '+String(d.selector).slice(0,300)+(d.text?' ("'+String(d.text).slice(0,200)+'")':'')+', '+r.width+'×'+r.height+' at '+r.x+','+r.y
183
- +(errors.length?'\\nRecent errors:\\n'+errors.map(e=>'- '+e.text).join('\\n'):''));
233
+ +(errors.length?'\\nRecent errors:\\n'+errors.map(e=>'- '+e.text).join('\\n'):''),d.hold===true&&voice);
184
234
  }
185
235
  // Compare: screenshots around the agent's last change (agent screenshots on).
186
236
  const compareBtn=$('compare-btn');let shots=[],urls=[];
187
237
  async function loadCompare(){
188
238
  try{const r=await fetch(metadata.origin+'/__bivy/compare',{credentials:'include',cache:'no-store'});if(!r.ok)return;shots=(await r.json()).shots||[];compareBtn.hidden=shots.length<2;}catch{}
189
239
  }
190
- function panels(open){for(const [id,btn] of [['console','errors'],['compare','compare-btn']]){$(id).hidden=id!==open;$(btn).setAttribute('aria-pressed',String(id===open));}$('draft').hidden=true;}
240
+ function panels(open){if(listening)cancelListening();for(const [id,btn] of [['console','errors'],['compare','compare-btn']]){$(id).hidden=id!==open;$(btn).setAttribute('aria-pressed',String(id===open));}$('draft').hidden=true;}
191
241
  compareBtn.onclick=async()=>{
192
242
  if(!$('compare').hidden)return panels(null);
193
243
  panels('compare');
@@ -212,6 +262,8 @@ for(const b of $('lens').querySelectorAll('button'))b.onclick=()=>{
212
262
  const down=$('down'),downText=$('down-text'),ask=$('ask');
213
263
  let downPort=0;
214
264
  addEventListener('message',e=>{
265
+ // The Bivy client framing this shell (Peek): voice only.
266
+ if(embedded&&metadata?.returnTo&&e.source===parent&&e.origin===new URL(metadata.returnTo).origin){if(e.data?.source==='bivy')fromBivy(e.data);return;}
215
267
  if(!metadata||e.origin!==metadata.origin||e.source!==frame.contentWindow)return;
216
268
  const d=e.data||{};
217
269
  if(d.type==='bivy:access'&&d.state==='blocked'&&embedded&&metadata.returnTo)toBivy({type:'blocked'});
@@ -224,10 +276,11 @@ addEventListener('message',e=>{
224
276
  if(d.type==='route')currentPath=safePath(d.path);
225
277
  else if(d.type==='console'&&(d.level==='error'||d.level==='warn')){entries.push({level:d.level,text:String(d.text).slice(0,500)});if(entries.length>50)entries.shift();renderConsole();}
226
278
  else if(d.type==='picked')picked(d);
279
+ else if(d.type==='release'&&heldAt){heldAt=0;stopListening();}
227
280
  }
228
281
  });
229
282
  ask.onclick=()=>toChat('The app preview "'+metadata.name+'" isn’t loading: nothing is answering on port '+downPort+'. Please find out why the server stopped, restart it, and tell me when it’s back.');
230
- addEventListener('keydown',e=>{if(e.key==='Escape'){if(point.getAttribute('aria-pressed')==='true')pointing(false);else panels(null);}});
283
+ addEventListener('keydown',e=>{if(e.key==='Escape'){if(listening)cancelListening();else if(point.getAttribute('aria-pressed')==='true')pointing(false);else panels(null);}});
231
284
  back.onclick=()=>{if(metadata?.returnTo){window.close();setTimeout(()=>location.replace(metadata.returnTo),100);}else{window.close();status.hidden=false;status.textContent='You can close this tab to return to Bivy.';}};
232
285
  reload.onclick=()=>{if(metadata)open(currentPath,'Reloading app…');};
233
286
  (async()=>{
@@ -3088,6 +3088,10 @@ select.picker-search { appearance: auto; }
3088
3088
  .apps-foot { margin: 0; padding: 0 var(--space-1); color: var(--muted); font-size: var(--text-xs); line-height: var(--lh-xs); }
3089
3089
  /* Peek: an app preview framed in a drawer over the chat (PreviewPeek.tsx). */
3090
3090
  .preview-peek { display: block; inline-size: 100%; block-size: 100%; border: 0; border-radius: var(--radius-md); background: var(--bg); }
3091
+ .preview-peek-stage { position: relative; block-size: 100%; }
3092
+ /* Point and speak: the recording bar floats over the top of the preview while
3093
+ the finger stays on the shell's mic, so nothing under it moves. */
3094
+ .preview-voice { position: absolute; inset-inline: var(--space-2); top: var(--space-2); z-index: var(--z-sticky); padding: var(--space-1) var(--space-2); border: 1px solid var(--line-strong); border-radius: var(--radius-full); background: var(--surface); box-shadow: var(--shadow-lg); }
3091
3095
  .app-terminal-status { padding: var(--space-2) var(--space-4); margin: 0; color: var(--muted); font-size: var(--text-sm); }
3092
3096
  .artifacts-group { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
3093
3097
  .artifacts-group:last-child { margin-bottom: 0; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.17.3-staging.22",
3
+ "version": "0.17.3-staging.24",
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.",