symbiot 0.61.0 → 0.62.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/CHANGELOG.md +25 -0
- package/README.md +80 -34
- package/browser.html +2 -1
- package/core.mjs +3 -1
- package/guard.mjs +30 -1
- package/handover.mjs +1 -1
- package/headless.mjs +83 -8
- package/home.mjs +48 -12
- package/index.mjs +31 -17
- package/links.mjs +40 -5
- package/mdns.mjs +135 -0
- package/mind.mjs +1 -0
- package/package.json +5 -3
- package/phone.mjs +525 -78
- package/post.mjs +29 -12
- package/qr.mjs +160 -0
- package/server.mjs +26 -13
- package/tasks.mjs +12 -2
- package/ui.mjs +173 -35
package/ui.mjs
CHANGED
|
@@ -951,6 +951,27 @@ body.lq-light #lqaway{color:#151A21;text-shadow:0 1px 12px rgba(255,255,255,.8)}
|
|
|
951
951
|
body.lq-light #lqaway .aws b{color:#9A5200}
|
|
952
952
|
|
|
953
953
|
/* Setup: the first run, over everything, until it's done */
|
|
954
|
+
/* Your phone: the QR that pairs it, the line saying how fresh the copy is, an agent's question answered from the phone */
|
|
955
|
+
.pqr{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin:10px 0}
|
|
956
|
+
.pqr .qrimg{flex:none;width:188px;height:188px;border-radius:14px;overflow:hidden;background:#fff;box-shadow:0 8px 26px -12px rgba(0,0,0,.55)}
|
|
957
|
+
.pqr .qrimg svg{display:block;width:100%;height:100%}
|
|
958
|
+
.pqr .qrtxt{flex:1;min-width:190px;font-size:13.5px;line-height:1.55;color:var(--text)}
|
|
959
|
+
.pqr .qrtxt .muted{display:block;margin-top:6px;font-size:12.5px}
|
|
960
|
+
.pqr .code{letter-spacing:3px;margin-right:-3px;white-space:nowrap}
|
|
961
|
+
#pcline{font-size:13.5px;line-height:1.5;color:var(--faint);margin:2px 0 8px}
|
|
962
|
+
#pcline b{color:var(--amber);font-weight:600}
|
|
963
|
+
.pcwait{font-size:11px;color:var(--amber);white-space:nowrap}
|
|
964
|
+
.pcq{padding:12px 13px;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--ink2)}
|
|
965
|
+
.pcq .qn{font-size:12px;color:var(--faint)}
|
|
966
|
+
.pcq .qq{font-size:13.5px;line-height:1.5;color:var(--text);margin:4px 0 8px;overflow-wrap:anywhere}
|
|
967
|
+
.pcq .opts{display:flex;flex-direction:column;gap:6px;align-items:stretch}
|
|
968
|
+
.pcq .opts button{text-align:left;white-space:normal;line-height:1.4;height:auto;padding:8px 12px}
|
|
969
|
+
.pcq form{display:flex;gap:8px;margin-top:8px}.pcq form input{flex:1;min-width:0}
|
|
970
|
+
.pcq ul{margin:6px 0 8px;padding-left:18px;font-size:13px;line-height:1.5;color:var(--text)}
|
|
971
|
+
.pcadd{display:flex;gap:8px;margin:8px 0 2px;flex-wrap:wrap}.pcadd input{flex:1;min-width:150px}.pcadd select{flex:0 1 160px;min-width:0}
|
|
972
|
+
.pcfound{display:flex;flex-direction:column;gap:6px;margin-top:8px}
|
|
973
|
+
body.lq-hline:not(.lq-work):not(.lq-pooled):not(.lq-chat):not(.lq-onb) #lqline{display:block;position:absolute;left:50%;top:auto;bottom:100px;transform:translateX(-50%);z-index:3;max-width:calc(100% - 32px);text-align:center;font:400 13.5px/1.4 var(--sans);color:#A1A8B1;text-shadow:0 1px 10px rgba(0,0,0,.7)}
|
|
974
|
+
body.lq-hline.lq-light:not(.lq-work):not(.lq-pooled):not(.lq-chat):not(.lq-onb) #lqline{color:#2E3743;text-shadow:none}
|
|
954
975
|
.onb{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:rgba(5,6,8,.74);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}
|
|
955
976
|
.onb.hidden{display:none}
|
|
956
977
|
.onbin{width:min(700px,calc(100vw - 32px));max-height:calc(100vh - 40px);overflow:auto;padding:30px 32px 26px;border-radius:28px;background:rgba(19,21,25,.9);border:1px solid rgba(255,255,255,.08);box-shadow:0 30px 80px -30px rgba(0,0,0,.9);color:var(--text)}
|
|
@@ -1141,6 +1162,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1141
1162
|
<div id="tasklist"></div>
|
|
1142
1163
|
<div id="panel-agents" class="wdagents">
|
|
1143
1164
|
<div class="row"><span class="muted" style="flex:1"><span class="tgroup" style="margin-right:8px">Agents and handovers</span>What each agent is doing, live, and the work handed between them. Questions, options and ideas an agent leaves for you show up on its block, whichever model it runs.</span><button class="iconbtn" id="agentsrefresh" title="Refresh" aria-label="Refresh"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M13 8a5 5 0 1 1-1.5-3.6M13 2.5V5h-2.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>
|
|
1165
|
+
<div id="phoneappr"></div>
|
|
1144
1166
|
<div id="agentsmsg"></div>
|
|
1145
1167
|
<div id="laneslist"></div>
|
|
1146
1168
|
<div id="agentslist"></div>
|
|
@@ -1161,6 +1183,11 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1161
1183
|
<div id="mkreplies"></div>
|
|
1162
1184
|
<div id="mktest"></div>
|
|
1163
1185
|
</section>
|
|
1186
|
+
<section id="panel-computer" class="hidden">
|
|
1187
|
+
<div id="pcline" aria-live="polite"></div>
|
|
1188
|
+
<div id="pcmsg"></div>
|
|
1189
|
+
<div id="pcbody"></div>
|
|
1190
|
+
</section>
|
|
1164
1191
|
<section id="panel-drift" class="hidden">
|
|
1165
1192
|
<div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
|
|
1166
1193
|
<label class="muted swl" style="margin-left:auto"><input type="checkbox" class="sw" id="driftfetch"> Fetch latest</label>
|
|
@@ -1172,7 +1199,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1172
1199
|
<div id="firststeps" class="sset hidden" aria-live="polite"></div>
|
|
1173
1200
|
<div class="sset" id="langset"><h3 class="ssh">Language</h3><div id="langbox"><span class="muted">…</span></div></div>
|
|
1174
1201
|
<div class="sset" id="licset"><h3 class="ssh">Symbiot Pro</h3><div id="licbox"><span class="muted">Checking…</span></div></div>
|
|
1175
|
-
<div class="sset"><h3 class="ssh">Setup</h3><div class="row"><span class="muted" style="flex:1">Walk through setup again: your AI, your work, your agent and your
|
|
1202
|
+
<div class="sset"><h3 class="ssh">Setup</h3><div class="row"><span class="muted" style="flex:1">Walk through setup again: your AI, your work, your agent, your apps and your phone.</span><button class="ghost" id="rerunsetup">Run setup again</button></div></div>
|
|
1176
1203
|
<div class="sset"><h3 class="ssh">Your AI</h3>
|
|
1177
1204
|
<label>Which AI should Symbiot write with?</label>
|
|
1178
1205
|
<select id="provider">
|
|
@@ -1228,6 +1255,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1228
1255
|
<div class="sset">
|
|
1229
1256
|
<div class="row" id="sbrow" style="margin-bottom:12px"><span style="flex:1"><b>The Symbiot Browser</b><br><span class="muted">Symbiot’s own browser window, where you sign in to your sites once so Symbiot can read them for you.</span></span><button class="act" id="sbopen">Open the Symbiot Browser</button></div>
|
|
1230
1257
|
<div id="links"></div>
|
|
1258
|
+
<div class="row" style="margin-top:8px"><input id="addsite" placeholder="Add a site: the page you see once signed in, e.g. www.domains.co.za/client/dashboard" aria-label="add a site to sign in to" style="flex:1"><button class="act" id="addsitego">Add and sign in</button></div>
|
|
1231
1259
|
<div id="linksmsg"></div>
|
|
1232
1260
|
<div class="note muted" id="linksnote">For a whole team, one <b>links.json</b> in Symbiot's config folder adds your company's own sites and hides the ones you don't use, so everyone gets the same buttons.</div>
|
|
1233
1261
|
</div>
|
|
@@ -1240,7 +1268,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1240
1268
|
<h3 class="ssh">Trusted sites for Screens <span class="muted">(experimental)</span></h3>
|
|
1241
1269
|
<div id="trustedsites"></div>
|
|
1242
1270
|
<div class="row" style="margin-top:6px"><input id="newtrusted" placeholder="a site, e.g. mail.google.com or github.com" style="flex:1"><button class="ghost" id="addtrusted">Trust site</button></div>
|
|
1243
|
-
<div class="note muted" id="trustednote">On a mapped page from one of these sites, <b>Press</b> and <b>Type</b> go ahead without asking, for you and for agents (<b>symbiot screens press</b> / <b>type</b>), signed in as you. A site covers its subdomains: google.com covers mail.google.com. Everywhere else, each one asks first. Only you add sites, here: Symbiot gives agents no command for it.</div>
|
|
1271
|
+
<div class="note muted" id="trustednote">To sign in to a site for agents, use <b>Sign in</b> next to it, or <b>Add a site</b> under Connections. On a mapped page from one of these sites, <b>Press</b> and <b>Type</b> go ahead without asking, for you and for agents (<b>symbiot screens press</b> / <b>type</b>), signed in as you. A site covers its subdomains: google.com covers mail.google.com. Everywhere else, each one asks first. Only you add sites, here: Symbiot gives agents no command for it.</div>
|
|
1244
1272
|
</div>
|
|
1245
1273
|
<div class="sset">
|
|
1246
1274
|
<h3 class="ssh">Email — add what you sent to Week and Standup <span class="muted">(experimental)</span></h3>
|
|
@@ -1259,7 +1287,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1259
1287
|
<div class="note muted" id="desktopnote"></div>
|
|
1260
1288
|
</div>
|
|
1261
1289
|
<div class="sset">
|
|
1262
|
-
<h3 class="ssh">
|
|
1290
|
+
<h3 class="ssh">Your phone <span class="muted">(experimental)</span></h3>
|
|
1263
1291
|
<div id="phonelink"></div>
|
|
1264
1292
|
</div>
|
|
1265
1293
|
<div class="sset">
|
|
@@ -1321,6 +1349,7 @@ $('panel-tasks').classList.toggle('hidden',!isTasks);
|
|
|
1321
1349
|
$('panel-drift').classList.toggle('hidden',!isDrift);
|
|
1322
1350
|
$('panel-reports').classList.toggle('hidden',tab!=='reports');if(tab==='reports')loadReports();
|
|
1323
1351
|
$('panel-marketing').classList.toggle('hidden',tab!=='marketing');if(tab==='marketing')loadMarketing();
|
|
1352
|
+
$('panel-computer').classList.toggle('hidden',tab!=='computer');if(tab==='computer')loadComputer();
|
|
1324
1353
|
if(isRun){$('what').textContent=tab;wuHead();WUTEXT='';$('out').textContent=WUEMPTY;$('out').classList.add('muted');$('copy').classList.add('hidden');$('outfoot').style.display='none';if(tab==='week')showLatestWeek();}
|
|
1325
1354
|
if(isMap&&!mapLoaded)loadMap();
|
|
1326
1355
|
if(isTasks)loadTasks();
|
|
@@ -1332,7 +1361,8 @@ lqTitle();if(isTasks&&TFILTER.repo)loadParked(lqTitle);}
|
|
|
1332
1361
|
// (optional, or skipped). Each ticks itself as it's done anywhere in Settings; Go
|
|
1333
1362
|
// takes you to its part, lit up. The block goes once all are done.
|
|
1334
1363
|
var FSKIP='symbiot-skip-company';
|
|
1335
|
-
function
|
|
1364
|
+
function fsKey(id){return id==='company'?FSKIP:'symbiot-skip-'+id;}
|
|
1365
|
+
function fsSkipped(id){try{return !!(window.localStorage&&window.localStorage.getItem(fsKey(id||'company')));}catch(e){return false;}}
|
|
1336
1366
|
// The app in your language (lang.mjs). Its own words on screen are swapped for their
|
|
1337
1367
|
// translation (your AI makes each once; they're kept, so it's instant after that).
|
|
1338
1368
|
// Numbers stay out of what's translated: "3 at work" is "{#} at work". Your content
|
|
@@ -1401,12 +1431,12 @@ var sv=$('licsave');if(sv)sv.addEventListener('click',function(){var k=($('licke
|
|
|
1401
1431
|
var cl=$('licclear');if(cl)cl.addEventListener('click',function(){api('/api/licence/clear',{}).then(function(r){licRender(r);}).catch(function(){});});
|
|
1402
1432
|
el.querySelectorAll('.licproj button[data-i]').forEach(function(b){b.addEventListener('click',function(){var p=st.projects[+b.getAttribute('data-i')];if(!p)return;api('/api/licence/project/free',{path:p.path}).then(function(r){licRender(r);}).catch(function(){});});});}
|
|
1403
1433
|
function loadFirstSteps(){api('/api/firststeps').then(function(f){var el=$('firststeps');if(!el||!f||!f.steps)return;
|
|
1404
|
-
var st=f.steps.map(function(s){return s.
|
|
1434
|
+
var st=f.steps.map(function(s){return s.optional&&!s.done&&fsSkipped(s.id)?Object.assign({},s,{done:true,skipped:true}):s;}),next=st.filter(function(s){return !s.done;})[0];
|
|
1405
1435
|
if(!next){el.innerHTML='';el.classList.add('hidden');return;}el.classList.remove('hidden');
|
|
1406
|
-
el.innerHTML="<h3 class='ssh'>First steps</h3><div class='note muted' style='margin-top:2px'>In this order: each one ticks itself once it's done.</div><ol>"+st.map(function(s,i){return "<li class='"+(s.done?'done':s===next?'next':'')+"'><span class='fsn' aria-hidden='true'>"+(s.done?'✓':(i+1))+"</span><span class='fst'><b>"+esc(s.title)+(s.optional?" <span class='muted' style='font-weight:400'>(optional)</span>":"")+"</b><span>"+(s.skipped?'skipped':esc(s.sub))+"</span></span>"+(s.done?"<span class='sr' style='position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)'>done</span>":"<button type='button' class='"+(s===next?'act':'ghost')+" fsgo' data-id='"+s.id+"'>"+(s===next?'Do it now':'Go')+"</button>"+(s.optional?"<button type='button' class='ghost fsskip'>Skip</button>":""))+"</li>";}).join('')+"</ol>";
|
|
1436
|
+
el.innerHTML="<h3 class='ssh'>First steps</h3><div class='note muted' style='margin-top:2px'>In this order: each one ticks itself once it's done.</div><ol>"+st.map(function(s,i){return "<li class='"+(s.done?'done':s===next?'next':'')+"'><span class='fsn' aria-hidden='true'>"+(s.done?'✓':(i+1))+"</span><span class='fst'><b>"+esc(s.title)+(s.optional?" <span class='muted' style='font-weight:400'>(optional)</span>":"")+"</b><span>"+(s.skipped?'skipped':esc(s.sub))+"</span></span>"+(s.done?"<span class='sr' style='position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)'>done</span>":"<button type='button' class='"+(s===next?'act':'ghost')+" fsgo' data-id='"+s.id+"'>"+(s===next?'Do it now':'Go')+"</button>"+(s.optional?"<button type='button' class='ghost fsskip' data-id='"+s.id+"'>Skip</button>":""))+"</li>";}).join('')+"</ol>";
|
|
1407
1437
|
el.querySelectorAll('.fsgo').forEach(function(b){b.addEventListener('click',function(){var id=b.getAttribute('data-id'),a=$(SETFOCUS[id==='site'?'links':id]||''),s=a&&a.closest?a.closest('.sset'):null;if(!s)return;
|
|
1408
1438
|
document.querySelectorAll('#panel-settings .lit').forEach(function(x){x.classList.remove('lit');});s.classList.add('lit');if(s.scrollIntoView)s.scrollIntoView({block:'start',behavior:'smooth'});var c=s.querySelector('select,input,button');if(c&&c.focus){try{c.focus({preventScroll:true});}catch(e){c.focus();}}});});
|
|
1409
|
-
|
|
1439
|
+
el.querySelectorAll('.fsskip').forEach(function(sk){sk.addEventListener('click',function(){try{if(window.localStorage)window.localStorage.setItem(fsKey(sk.getAttribute('data-id')),'1');}catch(e){}loadFirstSteps();});});});}
|
|
1410
1440
|
var fsTimer=null;(function(){var ps=$('panel-settings');if(ps&&ps.addEventListener){var again=function(){if(fsTimer)clearTimeout(fsTimer);fsTimer=setTimeout(function(){if(current==='settings')whenFree($('firststeps'),'firststeps',loadFirstSteps);},1800);};ps.addEventListener('click',again);ps.addEventListener('change',again);}})();
|
|
1411
1441
|
var tabPicked=false;tabs().forEach(function(t){t.addEventListener('click',function(){tabPicked=true;setTab(t.dataset.tab);});});
|
|
1412
1442
|
// Watching a page (your inbox, GitHub)? The app opens on the Dashboard instead of
|
|
@@ -1835,7 +1865,10 @@ var todoBox=boxes.length&&(w.todos&&w.todos.length||pg)?boxes.shift():'';
|
|
|
1835
1865
|
if(todoBox)h+="<div class='wk'>"+todoBox+"</div>";
|
|
1836
1866
|
h+="<details class='wkraw'><summary>details: steps, files, cost</summary><div class='wkstats'>"+stats.join('')+"</div>"+(boxes.length?"<div class='wk'>"+boxes.join('')+"</div>":"")+(!run&&w.final?"<div class='wkfinal'>"+esc(w.final)+"</div>":"")+"</details>";
|
|
1837
1867
|
return h;}
|
|
1838
|
-
function loadAgents(){loadLanes();loadParked(agentsDraw);}
|
|
1868
|
+
function loadAgents(){loadLanes();loadParked(agentsDraw);phoneApprovedNote();}
|
|
1869
|
+
// What was approved from your phone today (phone.mjs), above the agents: it wasn't you at this desk.
|
|
1870
|
+
function phoneApprovedNote(){var el=$('phoneappr');if(!el)return;var ap=(LQ.home&&LQ.home.phoneApproved)||[];
|
|
1871
|
+
el.innerHTML=ap.map(function(a){return "<div class='note "+(a.status==='failed'?'err':'ok')+"'>"+(a.status==='failed'?"Approving <b>"+esc(a.repo)+"</b> from "+esc(a.phone)+" failed: "+esc(a.error||''):(a.status==='approving'?"Approving ":"✓ Approved ")+"<b>"+esc(a.repo)+"</b> from "+esc(a.phone)+", "+agoTxt(a.at)+(a.pr?" · <a href='"+escQ(a.pr)+"' target='_blank' rel='noopener'>its PR</a>":""))+"</div>";}).join('');}
|
|
1839
1872
|
// Agents, in order: what needs you (a question, a step of yours), what's at work, and
|
|
1840
1873
|
// what's finished, folded away (open stays open across refreshes).
|
|
1841
1874
|
var AGDONEOPEN=false;
|
|
@@ -1962,30 +1995,93 @@ function phoneTermux(){var A=window.SymbiotAndroid,t={};if(A&&A.termux){try{t=JS
|
|
|
1962
1995
|
function loadPhone(){var t=phoneTermux();if(!t.installed&&!t.on)return;$('phonebox').classList.remove('hidden');
|
|
1963
1996
|
$('phonenote').innerHTML=t.on?"This window shows the Symbiot running in Termux, so it sees the projects in Termux's home folder and runs your agents there. The app's own Symbiot sees only shared storage.":"Termux keeps its home folder (<code>~</code>) private, so the app's own Symbiot can't see the projects there. Symbiot running in Termux can, and this app can show it. <b>Open Termux</b> copies the command that starts it (it installs Node and Symbiot there first if they're missing). Paste it in Termux, and Termux opens it here.";
|
|
1964
1997
|
var b=$('phonebtn');b.textContent=t.on?"Use the app's own Symbiot":"Open Termux";b.onclick=function(){if(t.on)SymbiotAndroid.builtIn();else SymbiotAndroid.openTermux();};}
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
1998
|
+
// Your phone (phone.mjs). On the computer: switch it on and it shows a QR (and a code)
|
|
1999
|
+
// to pair the app on your phone with; the phones paired, when each last asked and how
|
|
2000
|
+
// (your Wi-Fi, or the relay away from home), and the relay's switch. On the phone:
|
|
2001
|
+
// pair by scanning (the app opens the QR's link), or by the address and code typed in,
|
|
2002
|
+
// with the computers it finds on this network listed; then Check now and Forget.
|
|
1967
2003
|
function loadPhoneLink(msg){api('/api/phone').then(function(d){renderPhoneLink(d,msg);});}
|
|
1968
|
-
function
|
|
2004
|
+
function phoneQr(d,where){var ad=(d.addresses||[]).map(function(a){return a+':'+d.port;});
|
|
2005
|
+
return "<div class='pqr'><div class='qrimg' role='img' aria-label='QR code to pair your phone'>"+(d.qr||'')+"</div><div class='qrtxt'><b>Scan it with your phone’s camera.</b> Symbiot’s app on your phone opens and pairs with this computer."+
|
|
2006
|
+
"<span class='muted'>No camera, or Symbiot in Termux? On the phone, in "+where+", type "+(ad.length?"the address <b>"+esc(ad[0])+"</b>"+(ad.length>1?" (or "+esc(ad.slice(1).join(', '))+")":"")+" and ":"this computer’s address and ")+"the code <b class='code'>"+esc(d.code)+"</b>. It works for 10 minutes.</span></div></div>";}
|
|
2007
|
+
function phoneSeen(p){return p.old?"<span class='err'>paired before the link was sealed: pair it again</span>":(p.seen?"asked "+agoTxt(p.seen)+(p.via==='relay'?" through the relay":p.via==='wifi'?" on your Wi-Fi":""):"hasn’t asked yet");}
|
|
2008
|
+
function renderPhoneLink(d,msg){if(!d||!d.role)return;var box=$('phonelink'),h='';if(!box)return;
|
|
1969
2009
|
if(d.role==='phone'){
|
|
1970
|
-
if(d.paired){h+="<div class='task'><span class='t'>Paired with <b>"+esc(d.name)+"</b> <span class='muted' style='font-size:12px'>"+esc(d.url)+(d.last?" · asked "+agoTxt(d.last):"")+"</span>"+(d.error?"<br><span class='err' style='font-size:12px'>"+esc(d.error)+"</span>":"")+"</span><button class='ghost' id='pcheck' title='ask your computer now'>Check now</button><button class='rm' id='pforget' title='
|
|
1971
|
-
h+="<div class='note muted'>While Symbiot runs on this phone
|
|
1972
|
-
else
|
|
2010
|
+
if(d.paired&&!d.old){h+="<div class='task'><span class='t'>Paired with <b>"+esc(d.name)+"</b> <span class='muted' style='font-size:12px'>"+esc(d.url||'')+(d.last?" · asked "+agoTxt(d.last)+(d.via==='relay'?" through the relay":""):"")+"</span>"+(d.error?"<br><span class='err' style='font-size:12px'>"+esc(d.error)+"</span>":"")+"</span><button class='ghost' id='pcheck' title='ask your computer now'>Check now</button><button class='rm' id='pforget' title='forget it: its copy here goes too'>×</button></div>";
|
|
2011
|
+
h+="<div class='note muted'>Its tasks, what needs you and its agents’ questions are on Home, in <b>On "+esc(d.name)+"</b>: tick, add, answer and approve from there. While Symbiot runs on this phone it asks every 2 minutes (and as soon as this phone’s network changes), and shows what Watch found there as a notification.</div>";}
|
|
2012
|
+
else{if(d.old)h+="<div class='note err'>"+esc(d.error||'')+"</div>";
|
|
2013
|
+
h+="<div class='note muted' style='margin-top:2px'>"+(window.SymbiotAndroid?"<b>Scan the code your computer shows:</b> in Symbiot on your computer, open Settings → <b>Your phone</b> and switch it on. Point this phone’s camera at the QR, and tap its link. Or type its address and code here:":"In Symbiot on your computer, open Settings → <b>Your phone</b> and switch it on: it shows its address and a 6-digit code. Type both here.")+"</div>"+
|
|
2014
|
+
"<div class='row'><input id='paddr' placeholder='your computer’s address, e.g. 192.168.8.50:7392' style='flex:1'><input id='pcode' placeholder='code' inputmode='numeric' style='flex:0 0 90px'><button class='act' id='ppair'>Pair</button></div>"+
|
|
2015
|
+
"<div class='row' style='margin-top:4px'><button class='ghost' id='pfind'>Look for it on this network</button></div><div id='pfound' class='pcfound'></div>";}
|
|
1973
2016
|
if(!d.notify)h+="<div class='note err'>Notifications from Termux need the Termux:API app: install it, then run <b>pkg install termux-api</b> in Termux.</div>";}
|
|
1974
|
-
else{h+="<label class='check'><input type='checkbox' id='plinkon'"+(d.on?" checked":"")+"> Let the Symbiot app on my phone
|
|
2017
|
+
else{h+="<label class='check'><input type='checkbox' id='plinkon'"+(d.on?" checked":"")+"> Let the Symbiot app on my phone show my work here, and send back what I do on it</label>";
|
|
1975
2018
|
if(d.on&&d.error)h+="<div class='note err'>"+esc(d.error)+"</div>";
|
|
1976
|
-
else if(d.on&&d.listening){
|
|
1977
|
-
h+=d.
|
|
1978
|
-
|
|
1979
|
-
h+=
|
|
1980
|
-
h+="<div class='note muted'>"+(d.on?"Symbiot listens on port "+d.port+" of your network for this alone: the pairing code, then what’s new for a phone that paired. Nothing can be changed from there, and the rest of Symbiot stays on this computer. What’s new (your mail’s senders and subjects) crosses your Wi-Fi unencrypted, so use it on a network you trust. Pairing fails? A firewall here may need to allow port "+d.port+".":"New mail, review requests and failed CI runs that Watch finds here, as notifications on your phone, through the Symbiot app there. It asks this computer every 2 minutes while both are running, on the same Wi-Fi.")+"</div>";}
|
|
2019
|
+
else if(d.on&&d.listening){h+=d.code&&d.qr?phoneQr(d,"Settings → <b>Your phone</b>"):"<div class='row' style='margin-top:4px'><button class='ghost' id='pnewcode'>Pair a phone</button></div>";
|
|
2020
|
+
h+=(d.phones||[]).map(function(p){return "<div class='task' data-id='"+escQ(p.id)+"'><span class='t'>"+esc(p.name)+" <span class='muted' style='font-size:12px'>paired "+agoTxt(p.added)+" · "+phoneSeen(p)+"</span></span><button class='rm punpair' title='unpair it: it gets nothing more, and what it queued (an approve too) is refused'>×</button></div>";}).join('');
|
|
2021
|
+
var rl=d.relay||{};if((d.phones||[]).length)h+="<label class='check'><input type='checkbox' id='prelay'"+(rl.on!==false?" checked":"")+"> Away from home too, through symbiot.co.za</label><div class='note muted' style='margin-top:0'>"+(rl.on===false?"Off: your phone reaches this computer only on your Wi-Fi.":rl.connected?"Connected. Away from home, what your phone and this computer send each other passes through symbiot.co.za, sealed: it can’t read it, and keeps none of it. At home nothing goes through it.":"Not connected yet"+(rl.error?": "+esc(rl.error):"")+". Away from home, what they send passes through symbiot.co.za, sealed: it can’t read it.")+"</div>";}
|
|
2022
|
+
h+="<div class='note muted'>"+(d.on?"Symbiot listens on port "+d.port+" of your network for this alone: pairing, then sealed requests from a phone that paired. Everything between them is encrypted with a key the two agree when they pair, and a phone can only add and tick tasks, answer your agents and approve. The rest of Symbiot stays on this computer. Pairing fails? A firewall here may need to allow port "+d.port+".":"Your tasks, what needs you, your agents’ questions and what Watch finds, on your phone through the Symbiot app there: tick, add, answer and approve from it, sealed between the two.")+"</div>";}
|
|
1981
2023
|
if(msg)h+="<div class='note "+(msg.ok?"ok":"err")+"'>"+esc(msg.text)+"</div>";
|
|
1982
2024
|
box.innerHTML=h;
|
|
1983
2025
|
var on=$('plinkon');if(on)on.addEventListener('change',function(){on.disabled=true;api('/api/phone/link',{on:on.checked}).then(function(x){renderPhoneLink(x);});});
|
|
2026
|
+
var rlb=$('prelay');if(rlb)rlb.addEventListener('change',function(){rlb.disabled=true;api('/api/phone/relay',{on:rlb.checked}).then(function(x){renderPhoneLink(x);});});
|
|
1984
2027
|
var nc=$('pnewcode');if(nc)nc.addEventListener('click',function(){api('/api/phone/code',{}).then(function(x){renderPhoneLink(x);});});
|
|
1985
|
-
box.querySelectorAll('.punpair').forEach(function(b){b.addEventListener('click',function(){if(typeof confirm==='function'&&!confirm('Unpair this phone? It gets
|
|
1986
|
-
var pp=$('ppair');if(pp)pp.addEventListener('click',function(){var a=$('paddr').value,cd=$('pcode').value;pp.disabled=true;pp.textContent='Pairing...';api('/api/phone/pair',{address:a,code:cd}).then(function(x){renderPhoneLink(x,x&&x.error?{text:x.error}:{ok:true,text:'Paired. What
|
|
2028
|
+
box.querySelectorAll('.punpair').forEach(function(b){b.addEventListener('click',function(){if(typeof confirm==='function'&&!confirm('Unpair this phone? It gets nothing more from here, and what it queued (an approve too) is refused.'))return;api('/api/phone/unpair',{id:b.closest('.task').getAttribute('data-id')}).then(function(x){renderPhoneLink(x);});});});
|
|
2029
|
+
var pp=$('ppair');if(pp)pp.addEventListener('click',function(){var a=$('paddr').value,cd=$('pcode').value;pp.disabled=true;pp.textContent='Pairing...';api('/api/phone/pair',{address:a,code:cd}).then(function(x){renderPhoneLink(x,x&&x.error?{text:x.error}:{ok:true,text:'Paired. What needs you on '+x.name+' is on Home now.'});if(x&&x.error&&$('paddr')){$('paddr').value=a;$('pcode').value=cd;}if(x&&!x.error){LQ.fresh=true;lqLoad(true);}});});
|
|
2030
|
+
var pf0=$('pfind');if(pf0)pf0.addEventListener('click',function(){phoneFind(pf0,$('pfound'),function(u){var i=$('paddr');if(i)i.value=u.replace(/^http:[/][/]/,'');var c0=$('pcode');if(c0&&c0.focus)c0.focus();});});
|
|
1987
2031
|
var pc=$('pcheck');if(pc)pc.addEventListener('click',function(){pc.disabled=true;api('/api/phone/check',{}).then(function(x){renderPhoneLink(x,x&&!x.error?{ok:true,text:x.shown?'Showed '+x.shown+' notification(s).':'Asked: nothing new.'}:null);});});
|
|
1988
|
-
var pf=$('pforget');if(pf)pf.addEventListener('click',function(){if(typeof confirm==='function'&&!confirm('Forget your computer? Its notifications stop.'))return;api('/api/phone/forget',{}).then(function(x){renderPhoneLink(x);});});}
|
|
2032
|
+
var pf=$('pforget');if(pf)pf.addEventListener('click',function(){if(typeof confirm==='function'&&!confirm('Forget your computer? Its notifications stop, and its copy here goes, with any change not sent yet.'))return;api('/api/phone/forget',{}).then(function(x){renderPhoneLink(x);LQ.fresh=true;lqLoad(true);});});}
|
|
2033
|
+
// On your computer, on your phone (phone.mjs computerView): the copy of your computer's
|
|
2034
|
+
// work this phone keeps, so it shows with the computer off. First a line saying how
|
|
2035
|
+
// fresh it is (out of reach: since when), then what needs you there (its agents'
|
|
2036
|
+
// questions, its work waiting for your Approve), its tasks (tick, or add one), and
|
|
2037
|
+
// what Watch found. What you do here shows at once, marked "waiting to send" until the
|
|
2038
|
+
// computer has it; what it couldn't apply is said here, in plain words.
|
|
2039
|
+
var PCV=null;
|
|
2040
|
+
function loadComputer(){api('/api/phone').then(function(d){PCV=d;renderComputer();}).catch(function(){});}
|
|
2041
|
+
function pcDo(op,args,then){api('/api/phone/do',{op:op,args:args}).then(function(d){if(d&&d.refused){$('pcmsg').innerHTML="<div class='note err'>"+esc(d.refused)+"</div>";}else $('pcmsg').innerHTML='';if(d&&d.role){PCV=d;renderComputer();}if(then)then(d);LQ.fresh=true;});}
|
|
2042
|
+
function pcTime(t){var d=new Date(t),n=new Date();return d.toDateString()===n.toDateString()?d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}):d.toLocaleString([],{weekday:'short',hour:'2-digit',minute:'2-digit'});}
|
|
2043
|
+
function renderComputer(){var d=PCV,el=$('pcbody'),ln=$('pcline');if(!el||!ln)return;
|
|
2044
|
+
if(d&&d.name&&LQNAMES.computer!=='On '+d.name){LQNAMES.computer='On '+d.name;lqTitle();}
|
|
2045
|
+
if(!d||d.role!=='phone'||!d.paired||d.old){ln.innerHTML=d&&d.old?"<b>"+esc(d.error||'')+"</b>":'';el.innerHTML="<div class='note muted'>"+(d&&d.role==='computer'?"This is your computer: your phone shows this, there.":"Not paired with a computer yet.")+" <button class='ghost' id='pcsettings'>Your phone, in Settings</button></div>";var g=$('pcsettings');if(g)g.addEventListener('click',function(){lqFocus({kind:'setup',focus:'phone'});lqPool('settings');});return;}
|
|
2046
|
+
var c=d.copy,wait=d.queued?" · "+d.queued+" change"+(d.queued>1?"s":"")+" waiting to send":"";
|
|
2047
|
+
ln.innerHTML=d.away?"<b>"+esc(d.name)+" is out of reach.</b> Showing what it sent "+(d.heard?"at "+esc(pcTime(d.heard)):"last")+"."+wait:(c?"From "+esc(d.name)+", "+(d.heard?agoTxt(d.heard):"just now")+(d.via==='relay'?" through the relay":"")+"."+wait:"Asking "+esc(d.name)+" for its work…");
|
|
2048
|
+
if(!c){el.innerHTML="<div class='row'><button class='ghost' id='pcnow'>Ask it now</button></div>";var n0=$('pcnow');if(n0)n0.addEventListener('click',function(){n0.disabled=true;api('/api/phone/check',{}).then(loadComputer);});return;}
|
|
2049
|
+
var h='',W="<span class='pcwait'> waiting to send</span>";
|
|
2050
|
+
(c.notes||[]).forEach(function(n){h+="<div class='task' data-note='"+escQ(n.id)+"'><span class='t err'>"+esc(n.error)+(n.what?" <span class='muted'>("+esc(n.what)+")</span>":"")+"</span><button class='ghost pcnote'>OK</button></div>";});
|
|
2051
|
+
var asks=c.asks||[],aps=c.approves||[],needs=c.needs||[];
|
|
2052
|
+
if(asks.length||aps.length||needs.length){h+="<div class='mkneeds lit'><div class='pshead'><span>Needs you · "+(asks.length+aps.length+needs.length)+"</span><small>on "+esc(d.name)+"</small></div>";
|
|
2053
|
+
asks.forEach(function(a,i){var q=(a.questions||[])[0]||{};h+="<div class='pcq' data-ask='"+i+"'><div class='qn'>"+esc(a.name)+" asks"+((a.questions||[]).length>1?" · 1 of "+a.questions.length:"")+"</div><div class='qq'>"+esc(q.q||'')+"</div>"+
|
|
2054
|
+
(a.waiting?"<div class='note' style='margin:0'>You answered: "+esc((a.answered||[])[0]||'')+W+"</div>":"<div class='opts'>"+(q.options||[]).map(function(o,j){return "<button class='"+(j?'ghost':'act')+" pcopt' data-j='"+j+"'>"+esc(o)+"</button>";}).join('')+"</div><form class='pcown'><input placeholder='Or say it in your own words' maxlength='2000'><button class='ghost' type='submit'>Send</button></form>")+"</div>";});
|
|
2055
|
+
aps.forEach(function(a,i){h+="<div class='pcq' data-ap='"+i+"'><div class='qn'>"+esc(a.repo)+" · waiting for your Approve</div><div class='qq'>"+(a.tasks.length?a.tasks.length+" task"+(a.tasks.length>1?"s":"")+" done":"Changes without a task")+" · <span class='muted'>"+esc(a.stat||(a.files+" file"+(a.files===1?"":"s")+" changed"))+"</span></div>"+(a.tasks.length?"<ul>"+a.tasks.map(function(t){return "<li>"+esc(t.text)+"</li>";}).join('')+"</ul>":"")+
|
|
2056
|
+
(a.waiting?"<div class='note' style='margin:0'>Approved here"+W+"</div>":"<div class='row' style='margin:0'><button class='act pcapprove'>Approve</button><span class='muted' style='font-size:12px'>exactly this: if the work changes before your computer gets it, it isn’t approved</span></div>")+"</div>";});
|
|
2057
|
+
needs.forEach(function(n){h+="<div class='task'><span class='t'>"+esc(n.title)+" <span class='muted' style='font-size:12px'>"+esc(n.sub)+" · on your computer</span></span></div>";});
|
|
2058
|
+
h+="</div>";}
|
|
2059
|
+
(c.recent||[]).slice(-3).forEach(function(r){h+="<div class='note "+(r.status==='failed'?'err':'ok')+"'>"+(r.status==='approving'?"Approving "+esc(r.repo)+" (from "+esc(r.phone)+")…":r.status==='failed'?"Approving "+esc(r.repo)+" from "+esc(r.phone)+" failed: "+esc(r.error||''):"✓ "+esc(r.repo)+" approved from "+esc(r.phone)+(r.pr?" · <a href='"+escQ(r.pr)+"' target='_blank' rel='noopener'>its PR</a>":""))+"</div>";});
|
|
2060
|
+
var open=(c.tasks||[]).filter(function(t){return !t.done;}),done=(c.tasks||[]).filter(function(t){return t.done;}),repos=[];(c.tasks||[]).forEach(function(t){if(t.repo&&repos.indexOf(t.repo)<0)repos.push(t.repo);});
|
|
2061
|
+
h+="<div class='pshead' style='margin-top:14px'><span>Tasks · "+open.length+" open</span><small>on "+esc(d.name)+"</small></div>";
|
|
2062
|
+
h+="<form class='pcadd'><input id='pctask' placeholder='Add a task' maxlength='4000' autocomplete='off'><select id='pcrepo' aria-label='Which project'><option value=''>No project</option>"+repos.sort().map(function(r){return "<option value='"+escQ(r)+"'>"+esc(r)+"</option>";}).join('')+"</select><button class='act' type='submit'>Add</button></form>";
|
|
2063
|
+
h+=open.concat(done.slice(0,8)).map(function(t){return "<div class='task"+(t.done?" done":"")+"' data-id='"+escQ(t.id)+"'><input type='checkbox' class='pctick'"+(t.done?" checked":"")+(String(t.id).indexOf('q:')===0?" disabled":"")+" aria-label='done'><span class='t'>"+esc(t.text)+(t.repo?" <span class='rp'>"+esc(t.repo)+"</span>":"")+(t.review?" <span class='muted' style='font-size:11px'>done by its agent, waits for your Approve</span>":"")+(t.waiting?W:"")+"</span></div>";}).join('');
|
|
2064
|
+
if((c.watch||[]).length)h+="<div class='pshead' style='margin-top:14px'><span>What Watch found</span><small>on "+esc(d.name)+", today</small></div>"+c.watch.map(function(w){return "<div class='task'><span class='t'><b>"+esc(w.name)+"</b> <span class='muted' style='font-size:12px'>"+esc(w.label)+"</span>"+(w.items||[]).map(function(i){return "<br><span style='font-size:13px'>"+esc(i.text)+"</span>";}).join('')+"</span></div>";}).join('');
|
|
2065
|
+
var typed=$('pctask')?[$('pctask').value,$('pcrepo').value,document.activeElement&&document.activeElement.id]:null;
|
|
2066
|
+
el.innerHTML=h;if(typed){$('pctask').value=typed[0]||'';$('pcrepo').value=typed[1]||'';if(typed[2]==='pctask')$('pctask').focus();}
|
|
2067
|
+
el.querySelectorAll('.pcnote').forEach(function(b){b.addEventListener('click',function(){api('/api/phone/note',{id:b.closest('.task').getAttribute('data-note')}).then(function(x){PCV=x;renderComputer();});});});
|
|
2068
|
+
el.querySelectorAll('.pcq[data-ask]').forEach(function(box){var a=asks[+box.getAttribute('data-ask')],q=(a.questions||[])[0]||{};if(a.waiting)return;
|
|
2069
|
+
var send=function(t){if(!t)return;pcDo('agent.answer',{path:a.path,answers:[{q:q.q,a:t}]});};
|
|
2070
|
+
box.querySelectorAll('.pcopt').forEach(function(b){b.addEventListener('click',function(){send((q.options||[])[+b.getAttribute('data-j')]);});});
|
|
2071
|
+
var f=box.querySelector('form');if(f)f.addEventListener('submit',function(ev){ev.preventDefault();send(f.querySelector('input').value.trim());});});
|
|
2072
|
+
el.querySelectorAll('.pcq[data-ap]').forEach(function(box){var a=aps[+box.getAttribute('data-ap')],b=box.querySelector('.pcapprove');if(!b)return;b.addEventListener('click',function(){if(typeof confirm==='function'&&!confirm('Approve '+a.repo+'? It goes out as Approve on your computer does: committed, a PR opened, and a release if the version changed.'))return;b.disabled=true;pcDo('pending.approve',{repo:a.repo,sum:a.sum});});});
|
|
2073
|
+
el.querySelectorAll('.pctick').forEach(function(cb){cb.addEventListener('change',function(){pcDo('task.setDone',{id:cb.closest('.task').getAttribute('data-id'),done:cb.checked});});});
|
|
2074
|
+
var fm=el.querySelector('.pcadd');if(fm)fm.addEventListener('submit',function(ev){ev.preventDefault();var t=$('pctask');if(!t.value.trim()){t.focus();return;}pcDo('task.add',{text:t.value,repo:$('pcrepo').value},function(x){if(x&&!x.refused){var i=$('pctask');if(i)i.value='';}});});}
|
|
2075
|
+
// A pairing link the Android app was opened with (the QR, scanned): pair with it here.
|
|
2076
|
+
// In Setup's "Your computer" step it shows there; anywhere else, the computer's droplet opens.
|
|
2077
|
+
window.symbiotPairLink=function(link){api('/api/phone/pair',{link:String(link||'')}).then(function(x){
|
|
2078
|
+
if(ONB&&ONB.pending){var show=function(){if(x&&x.error){var m=$('onbpairmsg');if(m)m.textContent=x.error;}}; // Setup goes to "Your computer", where it shows paired, or why not
|
|
2079
|
+
if((ONB.steps||[]).indexOf('computer')>=0&&ONB.step!=='computer')api('/api/onboarding/set',{step:'computer'}).then(function(){return onbLoad();}).then(show).catch(show);else{var r=onbLoad();if(r&&r.then)r.then(show);else show();}return;}
|
|
2080
|
+
renderPhoneLink(x,x&&x.error?{text:x.error}:{ok:true,text:'Paired with '+x.name+'.'});if(x&&!x.error){LQ.fresh=true;lqLoad(true);lqPool('computer');}else lqPool('settings');});};
|
|
2081
|
+
// The computers saying they're here on this network (phone.mjs findComputers): a tap fills the address in.
|
|
2082
|
+
function phoneFind(btn,el,pick){if(!el)return;btn.disabled=true;btn.textContent='Looking...';api('/api/phone/find').then(function(r){btn.disabled=false;btn.textContent='Look again';var f=(r&&r.found)||[];
|
|
2083
|
+
el.innerHTML=f.length?f.map(function(x,i){return "<button class='ghost' data-i='"+i+"' style='text-align:left'>"+esc(x.name)+" <span class='muted'>"+esc(x.url.replace(/^http:[/][/]/,''))+"</span></button>";}).join(''):"<div class='note muted' style='margin:0'>None found. Is Symbiot running on your computer with Your phone switched on, and is this phone on the same Wi-Fi?</div>";
|
|
2084
|
+
el.querySelectorAll('button').forEach(function(b){b.addEventListener('click',function(){pick(f[+b.getAttribute('data-i')].url);});});}).catch(function(){btn.disabled=false;btn.textContent='Look again';});}
|
|
1989
2085
|
// Link your work (links.mjs): one button per standard site. A click opens it in
|
|
1990
2086
|
// Symbiot's browser to sign in, trusts it and watches it. Its dot: grey not
|
|
1991
2087
|
// linked, amber waiting for you to sign in (or signed out), green linked. Shown
|
|
@@ -2026,9 +2122,17 @@ el.innerHTML=ns.length?ns.slice(0,60).map(function(n){return "<div class='task'
|
|
|
2026
2122
|
el.querySelectorAll('.forgetone').forEach(function(b){b.addEventListener('click',function(){api('/api/mind/forget',{id:b.closest('.task').getAttribute('data-id')}).then(loadMind);});});
|
|
2027
2123
|
var fa=document.getElementById('forgetall');if(fa)fa.addEventListener('click',function(){api('/api/mind/forget',{id:'all'}).then(loadMind);});});}
|
|
2028
2124
|
function loadLinks(){api('/api/links').then(function(d){if(d&&d.items){LINKS=d;renderLinks();}});}
|
|
2125
|
+
// Sign in to a site for agents: the Symbiot Browser opens on it (links.mjs addSite:
|
|
2126
|
+
// added under Connections → Your sites, trusted, checked once you click Done there).
|
|
2127
|
+
// From Settings, a trusted site, a card or a reply that asks you to sign in. s: { site } or { id }.
|
|
2128
|
+
function signInSaid(nm){return 'Opened the Symbiot Browser with '+nm+'. Sign in there as you normally do, then click Done in the Symbiot Browser: Settings → Connections shows when it’s signed in.';}
|
|
2129
|
+
function openSignIn(s,say){if(!s||(!s.site&&!s.id))return;api('/api/links/site',s.id?{id:s.id}:{site:s.site}).then(function(r){var nm=(r&&r.item&&r.item.name)||s.name||s.site||'it';
|
|
2130
|
+
if(!r||r.error){say(nm+': '+((r&&r.error)||'it didn’t open'),false);return;}say(signInSaid(nm),true);loadLinks();loadTrusted();[20000,60000,120000].forEach(function(ms){setTimeout(loadLinks,ms);});}).catch(function(e){say(String((e&&e.message)||e),false);});}
|
|
2131
|
+
function addSiteUI(){var i=document.getElementById('addsite'),v=((i&&i.value)||'').trim();if(!v)return;openSignIn({site:v},function(t,ok){if(ok)i.value='';linksOut("<div class='note "+(ok?'ok':'err')+"'>"+esc(t)+"</div>");});}
|
|
2029
2132
|
// Trusted sites: where Screens' Press and Type don't ask first (headless.mjs).
|
|
2030
2133
|
function loadTrusted(){api('/api/screens/trusted').then(function(d){var box=document.getElementById('trustedsites');var sites=(d&&d.sites)||[];
|
|
2031
|
-
box.innerHTML=sites.length?sites.map(function(h){return "<div class='task' data-h='"+esc(h)+"'><span class='t' style='font-family:ui-monospace,monospace;font-size:12px'>"+esc(h)+"</span><button class='rm rmtrusted' title='stop trusting it: press and type ask first again'>×</button></div>";}).join(""):"<div class='muted' style='font-size:12px'>None yet: Press and Type ask first on every site.</div>";
|
|
2134
|
+
box.innerHTML=sites.length?sites.map(function(h){return "<div class='task' data-h='"+esc(h)+"'><span class='t' style='font-family:ui-monospace,monospace;font-size:12px'>"+esc(h)+"</span><button class='ghost tsign' title='open "+esc(h)+" in the Symbiot Browser, to sign in for agents'>Sign in</button><button class='rm rmtrusted' title='stop trusting it: press and type ask first again'>×</button></div>";}).join(""):"<div class='muted' style='font-size:12px'>None yet: Press and Type ask first on every site.</div>";
|
|
2135
|
+
box.querySelectorAll('.tsign').forEach(function(btn){btn.addEventListener('click',function(){var h=btn.closest('.task').getAttribute('data-h');openSignIn({site:h},function(t,ok){var n=document.getElementById('trustednote');if(n)n.innerHTML="<span class='"+(ok?'ok':'err')+"'>"+esc(t)+"</span>";});});});
|
|
2032
2136
|
box.querySelectorAll('.rmtrusted').forEach(function(btn){btn.addEventListener('click',function(){api('/api/screens/trusted/remove',{site:btn.closest('.task').getAttribute('data-h')}).then(function(){loadTrusted();loadScreensUI();});});});});}
|
|
2033
2137
|
function addTrustedUI(){var i=document.getElementById('newtrusted');var v=(i.value||'').trim();if(!v)return;var n=document.getElementById('trustednote');
|
|
2034
2138
|
api('/api/screens/trusted/add',{site:v}).then(function(r){if(!r||r.error){n.innerHTML="<span class='err'>"+esc((r&&r.error)||'failed')+"</span>";return;}i.value='';loadTrusted();loadScreensUI();});}
|
|
@@ -2639,6 +2743,8 @@ document.getElementById('newroot').addEventListener('keydown',function(e){if(e.k
|
|
|
2639
2743
|
$('addknow').addEventListener('click',addKnowUI);
|
|
2640
2744
|
['newknow','newknowex'].forEach(function(id){$(id).addEventListener('keydown',function(e){if(e.key==='Enter')addKnowUI();});});
|
|
2641
2745
|
document.getElementById('addtrusted').addEventListener('click',addTrustedUI);
|
|
2746
|
+
document.getElementById('addsitego').addEventListener('click',addSiteUI);
|
|
2747
|
+
document.getElementById('addsite').addEventListener('keydown',function(e){if(e.key==='Enter')addSiteUI();});
|
|
2642
2748
|
document.getElementById('newtrusted').addEventListener('keydown',function(e){if(e.key==='Enter')addTrustedUI();});
|
|
2643
2749
|
document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
|
|
2644
2750
|
document.getElementById('agentsrefresh').addEventListener('click',loadAgents);
|
|
@@ -2756,7 +2862,7 @@ if(!ps.length){el.innerHTML='';return;}
|
|
|
2756
2862
|
el.innerHTML=ps.map(function(p,i){var nm=p.name||p.repo,q=(p.qs||[])[0],st=p.parked?'parked':(p.qs&&p.qs.length)?'asks you':p.ready?'ready for your OK':p.state||'';
|
|
2757
2863
|
var h="<section class='rly"+((p.qs&&p.qs.length)||p.ready?' lit':'')+"' data-i='"+i+"'><div class='rlh'><b>"+esc(nm)+"</b>"+(st?"<span class='rls'>"+esc(st)+"</span>":"")+"</div>";
|
|
2758
2864
|
if(p.summary)h+="<p class='rlsum' title='"+escQ(p.summary)+"'>"+esc(p.summary)+"</p>";
|
|
2759
|
-
if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,fix:q.fix,id:q.id,sub:q.sub,draft:q.draft,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
|
|
2865
|
+
if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,fix:q.fix,id:q.id,sub:q.sub,draft:q.draft,signInTo:q.signInTo,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
|
|
2760
2866
|
if(p.ready)h+="<button type='button' class='act rlok'>Review and approve</button>";
|
|
2761
2867
|
if(p.ideas&&p.ideas.length)h+="<div class='rlk'>Extra tasks it suggests</div>"+p.ideas.map(function(d,j){return "<div class='rli' data-j='"+j+"'><span title='"+escQ(d.full)+"'>"+esc(d.text)+"</span><span class='rlia'><button type='button' class='rladd'>+ task</button><button type='button' class='rlskip'>Skip</button></span></div>";}).join('');
|
|
2762
2868
|
return h+"<div class='rlf'><button type='button' class='rlopen'>Its tasks ›</button><button type='button' class='rlag'>Its agents ›</button></div></section>";}).join('');
|
|
@@ -2827,7 +2933,7 @@ LQ.talk.push({me:false,text:t});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.r
|
|
|
2827
2933
|
// comes out of the core (mass is conserved). Your colours come from the system,
|
|
2828
2934
|
// live: light or dark, contrast, transparency, forced colours, your accent, night.
|
|
2829
2935
|
var LQ={drops:[],btns:[],core:{x:0,y:0,cr:0},mode:'aware',last:'',lastAct:Date.now(),talk:[],talking:false,theme:{},ripple:[0.5,0.5,-10],t:0,pointer:null,touch:false,frame:0};
|
|
2830
|
-
var LQNAMES={board:'Dashboard',map:'Map',tasks:'Workdesk',agents:'Workdesk',week:'Week',standup:'Standup',todo:'Todo',drift:'Drift',settings:'Settings',reports:'Reports',marketing:'Marketing'};
|
|
2936
|
+
var LQNAMES={board:'Dashboard',map:'Map',tasks:'Workdesk',agents:'Workdesk',week:'Week',standup:'Standup',todo:'Todo',drift:'Drift',settings:'Settings',reports:'Reports',marketing:'Marketing',computer:'Your computer'};
|
|
2831
2937
|
var LQ_REST=60000,LQ_MAX=11;
|
|
2832
2938
|
function lqMM(q){try{return !!(window.matchMedia&&window.matchMedia(q).matches);}catch(e){return false;}}
|
|
2833
2939
|
function lqLookGet(){var l='';try{l=window.localStorage&&window.localStorage.getItem('symbiot-look')||'';}catch(e){}return l==='glass'||l==='pearl'?l:'ferro';}
|
|
@@ -2932,6 +3038,13 @@ function runWarn(x){return x.quiet?'no progress in '+x.quiet+' min · ':x.loopin
|
|
|
2932
3038
|
function lqLinks(list,near){var at={},links=[],same=function(l){return lqPara(list[l[0]])===lqPara(list[l[1]]);};list.forEach(function(d,i){if(d.kind==='shape')at[d.shape]=i;});
|
|
2933
3039
|
list.forEach(function(d,i){if(d.kind==='shape')((near||{})[d.shape]||[]).forEach(function(n){var j=at[n.id];if(j!=null&&j>i)links.push([i,j,n.w]);else if(j!=null&&j<i&&!links.some(function(l){return l[0]===j&&l[1]===i;}))links.push([j,i,n.w]);});
|
|
2934
3040
|
else if((d.kind==='you'||d.kind==='feed')&&at[d.shape]!=null)links.push([at[d.shape],i,0.5]);});return links.filter(same);}
|
|
3041
|
+
// One calm line on Home, never a pop-up: on a phone, that your computer is out of reach
|
|
3042
|
+
// and what you see is its copy, from when; on the computer, what was approved from your phone today.
|
|
3043
|
+
function lqHomeLine(h){var el=$('lqline'),b=document.body,t='',pc=h.computer,ap=(h.phoneApproved||[]).slice(-1)[0];
|
|
3044
|
+
if(pc&&pc.away)t="<b>"+esc(pc.name)+" is out of reach.</b> Showing what it sent "+(pc.heard?"at "+esc(pcTime(pc.heard)):"last")+(pc.queued?" · "+pc.queued+" change"+(pc.queued>1?"s":"")+" waiting to send":"")+".";
|
|
3045
|
+
else if(pc&&pc.old)t="<b>Pair this phone with "+esc(pc.name)+" again:</b> the link between them is sealed now.";
|
|
3046
|
+
else if(ap)t=ap.status==='failed'?"<b>Approving "+esc(ap.repo)+" from "+esc(ap.phone)+" failed:</b> "+esc(ap.error||''):(ap.status==='approving'?"Approving ":"Approved ")+esc(ap.repo)+" from "+esc(ap.phone)+" at "+esc(pcTime(ap.at))+".";
|
|
3047
|
+
if(b&&b.classList)b.classList.toggle('lq-hline',!!t);if(el&&LQ.scene!=='work'&&el.innerHTML!==t)el.innerHTML=t;}
|
|
2935
3048
|
function lqBuild(){var a=LQ.adapt||{},h=LQ.home||{},S=lqSize(),list=[];LQ.bw=S.w;LQ.bh=S.h;
|
|
2936
3049
|
// the band first: everything else sits under it, round a core centred in what's left
|
|
2937
3050
|
var need=lqNeeds(h,S),band=lqRunBand(h,S,need.bottom),top=band.bottom||need.bottom,cx=S.w/2,cy=top?Math.max(S.h*0.47,(top+60+S.h-200)/2):S.h*0.47;LQ.cy=cy;LQ.top=top;LQ.need=need;
|
|
@@ -2948,6 +3061,10 @@ lay.slice(room).forEach(function(it){if(!vis.some(function(x){return x.id===it.i
|
|
|
2948
3061
|
var mkh=h.marketing||{},mkSub=mkh.needs?mkh.needs+(mkh.needs>1?' need you':' needs you'):mkh.working?'its agent is at work':'';
|
|
2949
3062
|
lay.slice(0,room).forEach(function(it){var mko=it.id==='marketing';list.push({id:'shape:'+it.id,kind:'shape',shape:it.id,title:LQNAMES[it.id]||it.id,sub:it.id==='reports'&&repOut?repNew.count+' new':mko?mkSub:'',ask:mko&&!!mkh.needs,orbit:mko&&!!mkh.working,r:it.r*rs,tx:cx+Math.cos(it.angle)*it.d*vx,ty:cy+Math.sin(it.angle)*it.d*vy});});
|
|
2950
3063
|
feeds.forEach(function(f,i){var ang=Math.PI/2+(i-(feeds.length-1)/2)*0.7;var fn=bsName({name:f.title});list.push({id:f.id,kind:'feed',shape:f.shape,title:fn.n+(fn.sub?' · '+fn.sub.split('@').pop():''),sub:f.sub,r:(28+5*Math.min(f.count||1,5))*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*300*vy});});
|
|
3064
|
+
// on a phone paired with your computer: its droplet, lit when something there needs you (phone.mjs)
|
|
3065
|
+
var pc=h.computer;if(pc&&!pc.old){var pn=pc.asks+pc.approves,pang=Math.PI/2+((feeds.length+1)/2)*0.7;list.push({id:'computer',kind:'feed',shape:'computer',title:'On '+pc.name,sub:pc.away?'out of reach · its copy':pn?pn+(pn>1?' need you':' needs you'):pc.tasks+' open task'+(pc.tasks===1?'':'s'),ask:pn>0,r:(32+4*Math.min(pn,5))*rs,tx:cx+Math.cos(pang)*300*vx,ty:cy+Math.sin(pang)*300*vy});}
|
|
3066
|
+
// on the computer: your phone, and when it last asked (a droplet only while one's paired)
|
|
3067
|
+
var phs=h.phones||[];if(phs.length){var p0=phs.slice().sort(function(x,y){return (y.seen||0)-(x.seen||0);})[0],gang=Math.PI/2+((feeds.length+1)/2)*0.7;list.push({id:'phones',kind:'feed',shape:'settings',title:phs.length>1?phs.length+' phones':p0.name,sub:p0.old?'pair it again':p0.seen?'seen '+agoTxt(p0.seen)+(p0.via==='relay'?', away':''):'hasn’t asked yet',ask:!!p0.old,r:28*rs,tx:cx+Math.cos(gang)*300*vx,ty:cy+Math.sin(gang)*300*vy});}
|
|
2951
3068
|
list.push({id:'archive',kind:'archive',shape:'tasks',title:'Archive',sub:'what’s done: archived tasks',r:26*rs,tx:cx+Math.cos(PARA_ANG.x)*330*vx,ty:cy+Math.sin(PARA_ANG.x)*330*vy});
|
|
2952
3069
|
list.forEach(function(d){var g=lqPara(d);d.live=PARA_LIVE[g];if(g==='r')d.r*=0.82;if(g==='x')d.r*=0.85;if(d.shape==='tasks'&&d.kind==='shape'&&h.working)d.orbit=true;});
|
|
2953
3070
|
var mr=more.length?[{id:'more',kind:'more',title:'more',sub:more.map(function(m){return LQNAMES[m]||m;}).join(' · '),more:more.slice(),r:24*S.s}]:[],zl=list.concat(mr),zc=lqZones(zl,S,top,92*S.s);
|
|
@@ -2966,6 +3083,7 @@ list=list.concat(need.items,band.items);var old={};LQ.drops.forEach(function(d){
|
|
|
2966
3083
|
LQ.drops=list.map(function(d){var o=old[d.id];d.x=o?o.x:d.blob?d.tx:cx;d.y=o?o.y:d.blob?d.ty:cy;d.vx=o?o.vx:0;d.vy=o?o.vy:0;d.cr=o?o.cr:0;return d;});
|
|
2967
3084
|
var n=(h.you||[]).length,w=h.working||0,qt=(h.runs||[]).filter(function(x){return x.quiet||x.looping;}).length,wk=w?w+' agent'+(w>1?'s':'')+' working'+(qt?' ('+qt+' stalled)':''):'';
|
|
2968
3085
|
LQ.coreText=n?(n+(n>1?' things need':' thing needs')+' only you'+(w?' · '+wk:'')):(w?wk+' · nothing needs you':'Nothing needs you right now');
|
|
3086
|
+
lqHomeLine(h);
|
|
2969
3087
|
var b=document.body;if(b&&b.classList){b.classList.toggle('lq-touch',!!(a.modes&&a.modes.touch)||LQ.touch);b.classList.toggle('lq-keys',!!(a.modes&&a.modes.keyboard));}
|
|
2970
3088
|
LQ.talkWeight=(a.modes&&a.modes.talkWeight)||0.35;
|
|
2971
3089
|
lqLabels();}
|
|
@@ -3058,6 +3176,7 @@ if(d.kind==='you'&&d.item){if(d.x!=null){var S1=lqSize();LQ.ripple=[d.x/S1.w,d.y
|
|
|
3058
3176
|
if(d.kind==='wait'&&d.focus){lqFocus({kind:'proj',repo:d.repo,path:d.focus});return;}
|
|
3059
3177
|
if(d.kind==='proj'){if(d.lit||d.repo==='ops'){lqFocus({kind:'proj',repo:d.repo});return;}TFILTER.repo=d.repo;TFILTER.type='';lqPool('tasks');if(typeof loadTasks==='function')loadTasks();return;}
|
|
3060
3178
|
// where your files disagree: the checks, lit up in Settings, and these clashes reached you (it dissolves)
|
|
3179
|
+
if(d.id==='phones'){lqUse('settings',ev);lqFocus({kind:'setup',focus:'phone'});return;}
|
|
3061
3180
|
if(d.id==='feed:clash'){api('/api/knowledge/checks/seen',{}).catch(function(){});if(LQ.home)LQ.home.feeds=(LQ.home.feeds||[]).filter(function(f){return f.id!=='feed:clash';});lqUse('settings',ev);loadChecks();lqFocus({kind:'setup',focus:'checks'});return;}
|
|
3062
3181
|
var S=lqSize();LQ.ripple=[d.x/S.w,d.y/S.h,LQ.t];if(LQ.scene==='work'){lqPool(d.shape||'agents');return;}lqUse(d.shape||'board',ev);lqGo(d.shape||'board');}
|
|
3063
3182
|
function lqMore(d){var el=$('lqmore');if(!el)return;if(!LQ.moreOpen||!d){el.innerHTML='';return;}
|
|
@@ -3092,6 +3211,7 @@ h+=home?"<button type='button' class='lt' title='open "+escQ(nm)+", with this li
|
|
|
3092
3211
|
if(y.kind!=='ask')return h+"<div class='bq'>"+esc(y.kind==='approve'?String(y.sub||'').replace(/ · only you decide$/,''):String(y.sub||'').charAt(0).toUpperCase()+String(y.sub||'').slice(1)+'.')+"</div><div class='bo'><button type='button' class='nopt rec ngo'><span class='bt'>"+(y.kind==='approve'?'Review and approve':y.id==='setup:inbox'?(y.signin?'Sign in again':'Show me your inbox'):y.id==='setup:ai'?'Connect an AI':y.pick?'Use '+y.pick.name:y.urgent?'Reconnect':'Open Settings')+"</span></button></div>";
|
|
3093
3212
|
var os=y.options||[];if(y.draft)h+=draftHtml(y.draft);h+="<div class='bq' title='"+escQ((y.q||'')+(y.why?' ('+y.why+')':''))+"'>"+esc(y.q||y.sub||'')+"</div>";
|
|
3094
3213
|
if(y.fix==='handover'&&y.sub)h+="<div class='bfor' title='"+escQ(y.sub)+"'>for: "+esc(y.sub)+"</div>";
|
|
3214
|
+
if(y.signInTo)h+="<div class='bo'><button type='button' class='nopt bsign' title='opens "+escQ(y.signInTo.name)+" in the Symbiot Browser: sign in there, then click Done'><span class='bt'>Sign in to "+esc(y.signInTo.name)+" ›</span></button></div>";
|
|
3095
3215
|
if(os.length)h+="<div class='bo'>"+os.map(function(o,j){return "<button type='button' class='nopt"+(j===0&&/[(]recommended[)]/i.test(o)?' rec':'')+"' data-j='"+j+"' title='"+escQ(o)+"'><span class='bt'>"+whoHtml(blobOpt(o))+"</span></button>";}).join('')+"</div><button type='button' class='bfree' aria-expanded='false'>"+(y.draft?'Change it':'or answer in your own words')+"</button>";
|
|
3096
3216
|
h+="<form class='bfx"+(os.length?' hidden':'')+"'><input placeholder='"+(y.draft?'what to change':'your answer')+"' aria-label='"+(y.draft?'what to change in the reply':'your answer to '+escQ(nm))+"'><button type='submit'>"+(y.draft?'Redraft':'Send')+"</button></form>";
|
|
3097
3217
|
if(home&&y.more)h+="<span class='bmore'>+"+y.more+" more question"+(y.more>1?'s':'')+" after this one</span>";
|
|
@@ -3106,6 +3226,7 @@ if(!r||r.error){lock(false);say(nm+': '+((r&&r.error)||'the answer didn’t save
|
|
|
3106
3226
|
say(y.fix?nm+': '+(r.said||'Done.'):'Sent to '+(r.lane||nm)+'. '+(r.yours&&r.yours.length?'Still yours to do: '+r.yours.join(' '):r.rerun?'Its agent is picking it up now.':(r.note||'Saved in its ANSWERS.md.')));
|
|
3107
3227
|
bdSet(y,'');if(o.sent)o.sent();if(box.classList)box.classList.add('gone');setTimeout(function(){if(o.done)o.done();},650);}).catch(function(e){lock(false);say(nm+': '+String((e&&e.message)||e),true);});};
|
|
3108
3228
|
box.querySelectorAll('.nopt[data-j]').forEach(function(b){b.addEventListener('click',function(){var j=+b.getAttribute('data-j');send((y.options||[])[j],j);});});
|
|
3229
|
+
var bs=box.querySelector('.bsign');if(bs)bs.addEventListener('click',function(){bs.disabled=true;openSignIn(y.signInTo,function(t,ok){bs.disabled=false;say(t,!ok);});});
|
|
3109
3230
|
var fr=box.querySelector('.bfree'),fx=box.querySelector('.bfx'),fi=fx&&fx.querySelector('input');
|
|
3110
3231
|
if(fi){var dr=bdGet(y);if(dr&&!fi.value){fi.value=dr.text;if(fx.classList)fx.classList.remove('hidden');if(fr)fr.setAttribute('aria-expanded','true');try{if(dr.s!=null)fi.setSelectionRange(dr.s,dr.e!=null?dr.e:dr.s);}catch(e){}}
|
|
3111
3232
|
var keep=function(){bdSet(y,fi.value,fi.selectionStart,fi.selectionEnd);};['input','keyup','mouseup','select'].forEach(function(t){fi.addEventListener(t,keep);});}
|
|
@@ -3144,7 +3265,7 @@ lqLight();};
|
|
|
3144
3265
|
api('/api/lanes').then(function(d){paint(((d&&d.handoffs)||[]).filter(function(x){return x.status==='held'&&(x.to===repo||x.from===repo);}));}).catch(function(){paint([]);});}
|
|
3145
3266
|
// What lit the orb, lit up where it opened (.lit): the first in view, and with only
|
|
3146
3267
|
// one, its first answer focused. Again after each re-render, but scrolled to once.
|
|
3147
|
-
var SETFOCUS={ai:'provider',work:'scanroots',agent:'agentcmd',links:'links',company:'knowroots',checks:'knowchecks'};
|
|
3268
|
+
var SETFOCUS={ai:'provider',work:'scanroots',agent:'agentcmd',links:'links',company:'knowroots',checks:'knowchecks',phone:'phonelink'};
|
|
3148
3269
|
function lqLight(){var f=LQ.focus;if(!f||LQ.mode!=='pool')return;var things=[],also=[];
|
|
3149
3270
|
if(f.kind==='setup'){var a=$(SETFOCUS[f.focus]||''),s=a&&a.closest?a.closest('.sset'):null;if(f.focus==='checks'&&a)things.push(a);else if(s)things.push(s);}
|
|
3150
3271
|
else if(current==='tasks'&&f.repo&&f.repo!=='ops'){document.querySelectorAll('#needsbox .nitem').forEach(function(e){things.push(e);});document.querySelectorAll('#reviewlist .rcard').forEach(function(e){if(e.getAttribute('data-repo')===f.repo)(things.length?also:things).push(e);});}
|
|
@@ -3176,8 +3297,9 @@ var h=!n?'':LQ.talkMin?"<button type='button' class='lqtb-open' aria-expanded='f
|
|
|
3176
3297
|
if(!sameHtml(el,h)){var bt=el.querySelector('button');if(bt)bt.addEventListener('click',function(){if(LQ.talkMin)lqChatOpen();else lqChatMin();});}}
|
|
3177
3298
|
function lqChatMin(){if(!LQ.talk.length||LQ.talkMin)return;LQ.talkMin=true;var i=$('lqask');if(i&&i.blur&&document.activeElement===i)i.blur();lqTalkMode(false);lqTalkBar();lqAct();}
|
|
3178
3299
|
function lqChatOpen(){LQ.talkMin=false;lqTalkMode(true);lqTalkShow(false);lqAct();var i=$('lqask');if(i&&i.focus)i.focus();}
|
|
3179
|
-
function lqTalkShow(wait){var el=$('lqtalk');if(!el)return;var n=LQ.talk.length;el.innerHTML=LQ.talk.map(function(m,i){var age=n-1-i,op=age<2?1:age===2?0.7:age===3?0.45:0.25;return "<div class='lqmsg"+(m.me?' me':'')+"' style='opacity:"+op+"'>"+(m.imgs&&m.imgs.length?"<span class='lqmimgs'>"+m.imgs.map(function(d){return "<img src='"+d+"' alt='your screenshot'>";}).join('')+"</span>":"")+esc(m.text)+lqAltsHtml(m.go)+stepsHtml(m.steps)+"</div>";}).join('')+(wait?"<div class='lqmsg thinking'>Thinking: recalling what it knows, reading what's here…</div>":'');if(el.scrollHeight)el.scrollTop=el.scrollHeight;
|
|
3300
|
+
function lqTalkShow(wait){var el=$('lqtalk');if(!el)return;var n=LQ.talk.length;el.innerHTML=LQ.talk.map(function(m,i){var age=n-1-i,op=age<2?1:age===2?0.7:age===3?0.45:0.25;return "<div class='lqmsg"+(m.me?' me':'')+"' style='opacity:"+op+"'>"+(m.imgs&&m.imgs.length?"<span class='lqmimgs'>"+m.imgs.map(function(d){return "<img src='"+d+"' alt='your screenshot'>";}).join('')+"</span>":"")+esc(m.text)+lqAltsHtml(m.go)+(m.signInTo?"<span class='lqalts'><button type='button' class='lqmb lqsign' data-i='"+i+"'>Sign in to "+esc(m.signInTo.name)+"</button></span>":"")+stepsHtml(m.steps)+"</div>";}).join('')+(wait?"<div class='lqmsg thinking'>Thinking: recalling what it knows, reading what's here…</div>":'');if(el.scrollHeight)el.scrollTop=el.scrollHeight;
|
|
3180
3301
|
el.querySelectorAll('[data-go]').forEach(function(b){b.addEventListener('click',function(){lqNav(b.getAttribute('data-go'));});});
|
|
3302
|
+
el.querySelectorAll('.lqsign').forEach(function(b){b.addEventListener('click',function(){var m=LQ.talk[+b.getAttribute('data-i')];if(m)openSignIn(m.signInTo,function(t){lqSaid(t);});});});
|
|
3181
3303
|
// scrolled up to read back: the whole history shows, clear; back at the bottom it rolls on again
|
|
3182
3304
|
if(!el.wired&&el.addEventListener){el.wired=true;el.addEventListener('scroll',function(){var up=el.scrollHeight-el.scrollTop-el.clientHeight>24;if(el.classList)el.classList.toggle('back',up);});}lqTalkBar();}
|
|
3183
3305
|
// Screenshots in the talk: paste one (Ctrl+V) or drop it on the bar; it shows as a
|
|
@@ -3279,7 +3401,7 @@ if(hit){lqNav(hit);return;}
|
|
|
3279
3401
|
if(/^(go|start|go for it|start them|start it)[.! ]*$/.test(low)){lqTalkMode(true);lqGoWork();return;}
|
|
3280
3402
|
if(/^(home|back)[.! ]*$/.test(low)&&LQ.scene==='work'){lqHome();return;}
|
|
3281
3403
|
LQ.talkMin=false;lqTalkMode(true);LQ.talk.push({me:true,text:q,imgs:shots.map(function(a){return a.data;})});LQ.talk=LQ.talk.slice(-8);lqTalkShow(true);
|
|
3282
|
-
api('/api/home/ask',{question:q,images:shots}).then(function(r){LQ.talk.push({me:false,steps:r.steps||[],go:r.go||null,text:r.error==='not-connected'?'Connect an AI in Settings to talk to me. Say "open settings".':(r.answer||r.error||'(no answer)')});if(spoke)voiceSpeak(r.spoken||LQ.talk[LQ.talk.length-1].text);LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.47,LQ.t];if(r.go&&r.go.to){lqNav(r.go.to,r.go);return;}lqLoad(false);}).catch(function(e){LQ.talk.push({me:false,text:String((e&&e.message)||e)});lqTalkShow(false);});}
|
|
3404
|
+
api('/api/home/ask',{question:q,images:shots}).then(function(r){LQ.talk.push({me:false,steps:r.steps||[],go:r.go||null,signInTo:r.signInTo||null,text:r.error==='not-connected'?'Connect an AI in Settings to talk to me. Say "open settings".':(r.answer||r.error||'(no answer)')});if(spoke)voiceSpeak(r.spoken||LQ.talk[LQ.talk.length-1].text);LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.47,LQ.t];if(r.go&&r.go.to){lqNav(r.go.to,r.go);return;}lqLoad(false);}).catch(function(e){LQ.talk.push({me:false,text:String((e&&e.message)||e)});lqTalkShow(false);});}
|
|
3283
3405
|
// The physics: each droplet a critically damped spring to its place (no wobble,
|
|
3284
3406
|
// no overshoot), pushed off its neighbours and the core where they'd overlap.
|
|
3285
3407
|
function lqStep(){var S0=lqSize();if(S0.w!==LQ.bw||S0.h!==LQ.bh){if(LQ.scene==='work'&&LQ.workData)lqBuildWork();else if(LQ.adapt)lqBuild();}var S=S0,cx=S.w/2,cy=(LQ.scene!=='work'&&LQ.cy)||S.h*0.47,k=0.022,c=2*Math.sqrt(k),th=LQ.theme,still=th.still,rest=LQ.mode==='rest',pool=LQ.mode==='pool',talk=LQ.talking&&!pool;
|
|
@@ -3439,10 +3561,11 @@ var raf=window.requestAnimationFrame&&function(f){return window.requestAnimation
|
|
|
3439
3561
|
// A new Symbiot shows this before anything else (home.mjs onboarding): what it is, your
|
|
3440
3562
|
// AI, where your work is, your agent, every app you use (connected, or "I don't use
|
|
3441
3563
|
// it"), your documents, then Home. Where you are is kept, so closing it resumes there.
|
|
3442
|
-
var ONB=null,ONB_T=null,ONB_NAMES={welcome:'Welcome',ai:'Your AI',work:'Your work',agent:'Your agent',apps:'Your apps',docs:'Documents',done:'Done'};
|
|
3564
|
+
var ONB=null,ONB_T=null,ONB_NAMES={computer:'Your computer',welcome:'Welcome',ai:'Your AI',work:'Your work',agent:'Your agent',apps:'Your apps',docs:'Documents',phone:'Your phone',done:'Done'};
|
|
3565
|
+
function onbStep(d){var st=(ONB&&ONB.steps)||[],k=st.indexOf(ONB&&ONB.step);return st[k+d]||'';} // the step before (-1) or after (1) this one
|
|
3443
3566
|
function onbLoad(fresh){return api('/api/onboarding'+(fresh?'?fresh=1':'')).then(function(o){ONB=o;var el=$('onb');if(!el)return;if(!o||!o.pending){el.classList.add('hidden');document.body.classList.remove('lq-onb');if(ONB_T){clearInterval(ONB_T);ONB_T=null;}return;}
|
|
3444
3567
|
el.classList.remove('hidden');document.body.classList.add('lq-onb');onbRender();
|
|
3445
|
-
if(!ONB_T)ONB_T=setInterval(function(){if(ONB&&ONB.pending&&['ai','work','agent','apps'].indexOf(ONB.step)>=0&&!onbBusy())onbLoad(ONB.step==='ai');},2500);}).catch(function(){});}
|
|
3568
|
+
if(!ONB_T)ONB_T=setInterval(function(){if(ONB&&ONB.pending&&['ai','work','agent','apps','phone','computer'].indexOf(ONB.step)>=0&&!onbBusy())onbLoad(ONB.step==='ai');},2500);}).catch(function(){});}
|
|
3446
3569
|
// not while you're typing in it
|
|
3447
3570
|
function onbBusy(){var a=document.activeElement;return !!(a&&$('onb').contains(a)&&(a.tagName==='INPUT'||a.tagName==='SELECT'));}
|
|
3448
3571
|
function onbGo(step){api('/api/onboarding/set',{step:step}).then(function(o){ONB=o;onbRender();var b=$('onbbody');if(b&&b.parentNode)b.parentNode.scrollTop=0;});}
|
|
@@ -3453,7 +3576,7 @@ var h='';
|
|
|
3453
3576
|
if(o.step==='welcome'){
|
|
3454
3577
|
h="<img class='onborb' src='/favicon.svg?look="+encodeURIComponent(lqLookGet()||'ferro')+"' alt=''><h2 id='onbt'>Meet Symbiot</h2><p class='onbl'>It works alongside you, so your time goes on what only you can do.</p><div class='onbcards'>"+
|
|
3455
3578
|
[['It watches','What you work on and what comes in: your projects, your mail, chats and code.'],['It works','It hands tasks to your coding agent, which does them on its own and tells you what it did.'],['It asks','Only what only you can do: a sign-in, a decision, your OK before anything goes out.']].map(function(c){return "<div class='onbcard'><b>"+c[0]+"</b><span>"+c[1]+"</span></div>";}).join('')+
|
|
3456
|
-
"</div><p class='onbnote'>Setup takes a few minutes: your AI, your work, your agent and every app you use. Symbiot starts finding your projects meanwhile.</p>"+onbNav(
|
|
3579
|
+
"</div><p class='onbnote'>Setup takes a few minutes: your AI, your work, your agent and every app you use. Symbiot starts finding your projects meanwhile.</p>"+onbNav(onbStep(-1), 'ai', 'Set it up');}
|
|
3457
3580
|
else if(o.step==='ai'){var a=o.ai||{},c=a.claude||{};
|
|
3458
3581
|
h="<h2 id='onbt'>Your AI</h2><p class='onbl'>Symbiot thinks with an AI: it reads what comes in, talks with you and writes for you.</p>";
|
|
3459
3582
|
if(a.connected)h+="<div class='onbok'><i>✓</i><div>Symbiot is using <b>"+esc(a.line)+"</b>"+(a.provider==='claude'?". It’s the Claude account you’re signed in with in Claude Code: no key, nothing extra to pay.":".")+"</div></div>";
|
|
@@ -3490,12 +3613,24 @@ else if(o.step==='docs'){var fs=(o.docs||{}).folders||[];
|
|
|
3490
3613
|
h="<h2 id='onbt'>Your documents</h2><p class='onbl'>Optional. A folder of your company’s documents lets Symbiot quote them when you ask, and spot where they disagree (a deadline on someone’s leave, a price told two ways).</p>";
|
|
3491
3614
|
if(fs.length)h+="<div class='onbok'><i>✓</i><div>"+fs.map(function(f){return "<code>"+esc(f)+"</code>";}).join(' ')+"</div></div>";
|
|
3492
3615
|
h+="<div class='onbbox'><div class='onbrow'><input id='onbdoc' placeholder='A folder, e.g. ~/Company'><button class='ghost' id='onbadddoc'>Add</button></div><p id='onbdocmsg' style='margin:8px 0 0'></p></div>";
|
|
3493
|
-
h+=onbNav('apps',
|
|
3616
|
+
h+=onbNav('apps',onbStep(1),fs.length?'Continue':'Skip for now');}
|
|
3617
|
+
else if(o.step==='phone'){var pp=o.phone||{},pn=pp.phones||[];
|
|
3618
|
+
h="<h2 id='onbt'>Your phone</h2><p class='onbl'>Optional. Your tasks, what needs you and your agents’ questions on your phone, with Symbiot’s app there: tick, add, answer and approve from it, at home or away.</p>";
|
|
3619
|
+
if(pn.length)h+="<div class='onbok'><i>✓</i><div>Paired: <b>"+esc(pn.join(', '))+"</b>. Your work is on it now.</div></div>";
|
|
3620
|
+
else if(pp.on&&pp.listening&&pp.qr)h+="<div class='onbbox'>"+phoneQr(pp,"Symbiot’s Settings → <b>Your phone</b>")+"<p class='onbnote' style='margin:0'>Away from home, what they send passes through symbiot.co.za, sealed: it can’t read it. No app on your phone yet? It’s at <a href='https://symbiot.co.za' target='_blank' rel='noopener'>symbiot.co.za</a>.</p></div>";
|
|
3621
|
+
else h+=(pp.error?"<p class='onbnote err'>"+esc(pp.error)+"</p>":"")+"<div class='onbbox'><h3>Pair the app on your phone</h3><p>This computer shows a code for your phone to scan. It listens on your Wi-Fi for your phone alone, and everything between the two is sealed with a key they agree.</p><button class='act' id='onbphone'>"+(pp.on&&pp.listening?'New code':'Show the code')+"</button></div>";
|
|
3622
|
+
h+=onbNav('docs','done',pn.length?'Continue':'I don’t have the app');}
|
|
3623
|
+
else if(o.step==='computer'){var pc0=o.phone||{};
|
|
3624
|
+
h="<h2 id='onbt'>Your computer</h2><p class='onbl'>Does Symbiot run on your computer? Pair this phone with it and your work there is here too: your tasks, what needs you, your agents’ questions, and Approve, at home or away.</p>";
|
|
3625
|
+
if(pc0.paired)h+="<div class='onbok'><i>✓</i><div>Paired with <b>"+esc(pc0.name)+"</b>. Its work is on Home, in its own droplet.</div></div><div class='onbnav'><button class='back' data-go='welcome'>Also use Symbiot on this phone</button><span class='sp'></span><button class='act' id='onbfinish'>Open Symbiot</button></div>";
|
|
3626
|
+
else h+="<div class='onbbox'><h3>Scan the code your computer shows</h3><p>In Symbiot on your computer, open Settings → <b>Your phone</b> and switch it on: it shows a QR. Point this phone’s camera at it and tap its link.</p><p id='onbpairmsg' class='onbnote' style='margin:0'></p></div>"+
|
|
3627
|
+
"<div class='onbbox'><h3>Or type its address and code</h3><div class='onbrow'><input id='onbpaddr' placeholder='e.g. 192.168.8.50:7392'><input id='onbpcode' placeholder='code' inputmode='numeric' style='flex:0 0 100px;min-width:0'><button class='ghost' id='onbppair'>Pair</button></div><div class='onbrow' style='margin-top:8px'><button class='ghost' id='onbpfind'>Look for it on this network</button></div><div id='onbpfound' class='pcfound'></div></div>"+
|
|
3628
|
+
"<div class='onbnav'><span class='sp'></span><button class='act' data-go='welcome'>No computer: use Symbiot on this phone</button></div>";}
|
|
3494
3629
|
else{var conn=(o.apps||[]).filter(function(x){return x.state!=='off'&&!x.skipped;}).length;
|
|
3495
3630
|
h="<h2 id='onbt'>You’re set up</h2><p class='onbl'>Here’s what Symbiot has to work with. You can change any of it in Settings.</p><div class='onbbox'><p>"+
|
|
3496
|
-
[(o.ai&&o.ai.connected?'✓ ':'· ')+'AI: '+esc((o.ai&&o.ai.line)||'not connected'),(((o.work||{}).count)?'✓ ':'· ')+((o.work||{}).count||0)+' project'+(((o.work||{}).count)===1?'':'s'),(o.agent&&o.agent.cmd?'✓ ':'· ')+'Agent: '+(o.agent&&o.agent.cmd?'ready':'none yet'),(conn?'✓ ':'· ')+(conn?conn+' app'+(conn===1?'':'s')+' connected':'no apps connected yet')].join('<br>')+
|
|
3631
|
+
[(o.ai&&o.ai.connected?'✓ ':'· ')+'AI: '+esc((o.ai&&o.ai.line)||'not connected'),(((o.work||{}).count)?'✓ ':'· ')+((o.work||{}).count||0)+' project'+(((o.work||{}).count)===1?'':'s'),(o.agent&&o.agent.cmd?'✓ ':'· ')+'Agent: '+(o.agent&&o.agent.cmd?'ready':'none yet'),(conn?'✓ ':'· ')+(conn?conn+' app'+(conn===1?'':'s')+' connected':'no apps connected yet')].concat(o.phone&&o.phone.role==='computer'?[((o.phone.phones||[]).length?'✓ Phone: '+esc(o.phone.phones.join(', ')):'· Phone: not paired (Settings → Your phone)')]:o.phone&&o.phone.paired?['✓ Computer: '+esc(o.phone.name)]:[]).join('<br>')+
|
|
3497
3632
|
"</p></div><h3 style='margin:18px 0 4px;font:600 15px var(--sans);color:var(--bone)'>Finding your way</h3><ul class='onbtour'><li>Each orb is a place: tap it to open it, right-click to go back.</li><li>Amber means something needs you.</li><li>Talk to Symbiot at the bottom: ask anything, or tell it what to do.</li></ul>"+
|
|
3498
|
-
"<div class='onbnav'><button class='back' data-go='
|
|
3633
|
+
"<div class='onbnav'><button class='back' data-go='"+onbStep(-1)+"'>Back</button><span class='sp'></span><button class='act' id='onbfinish'>Open Symbiot</button></div>";}
|
|
3499
3634
|
b.innerHTML=h;
|
|
3500
3635
|
b.querySelectorAll('[data-go]').forEach(function(x){x.addEventListener('click',function(){onbGo(x.getAttribute('data-go'));});});
|
|
3501
3636
|
var on=function(id,f){var x=$(id);if(x)x.addEventListener('click',f);};
|
|
@@ -3510,6 +3645,9 @@ on('onbuse',function(){var c=$('onbuse').getAttribute('data-cmd');api('/api/agen
|
|
|
3510
3645
|
on('onbskiprest',function(){api('/api/onboarding/set',{skipRest:true}).then(function(r){ONB=r;onbRender();});});
|
|
3511
3646
|
on('onbadddoc',function(){var v=$('onbdoc').value.trim();if(!v)return;api('/api/knowledge/add',{path:v}).then(function(r){$('onbdocmsg').textContent=r&&r.error?r.error:'';onbLoad();});});
|
|
3512
3647
|
on('onbfinish',function(){api('/api/onboarding/done',{}).then(function(){onbLoad();LQ.fresh=true;lqLoad(true);});});
|
|
3648
|
+
on('onbphone',function(){var x=$('onbphone');x.disabled=true;api(ONB.phone&&ONB.phone.on&&ONB.phone.listening?'/api/phone/code':'/api/phone/link',{on:true}).then(function(){onbLoad();});});
|
|
3649
|
+
on('onbppair',function(){var m=$('onbpairmsg'),x=$('onbppair');x.disabled=true;api('/api/phone/pair',{address:$('onbpaddr').value,code:$('onbpcode').value}).then(function(r){x.disabled=false;if(r&&r.error){if(m)m.textContent=r.error;return;}onbLoad();LQ.fresh=true;});});
|
|
3650
|
+
on('onbpfind',function(){phoneFind($('onbpfind'),$('onbpfound'),function(u){$('onbpaddr').value=u.replace(/^http:[/][/]/,'');$('onbpcode').focus();});});
|
|
3513
3651
|
b.querySelectorAll('[data-link]').forEach(function(x){x.addEventListener('click',function(){x.disabled=true;api('/api/links/link',{id:x.getAttribute('data-link')}).then(function(){onbLoad();});});});
|
|
3514
3652
|
b.querySelectorAll('[data-check]').forEach(function(x){x.addEventListener('click',function(){x.disabled=true;api('/api/links/check',{id:x.getAttribute('data-check')}).then(function(){onbLoad();});});});
|
|
3515
3653
|
b.querySelectorAll('[data-skip]').forEach(function(x){x.addEventListener('click',function(){api('/api/onboarding/set',{skip:x.getAttribute('data-skip')}).then(function(r){ONB=r;onbRender();});});});
|