symbiot 0.54.2 → 0.54.4

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/ui.mjs CHANGED
@@ -101,6 +101,9 @@ body.lq-liquid.lq-light.lq-pooled #lqsinkrow{background:rgba(246,248,251,.94);bo
101
101
  #lqtitle{margin:0;font:650 24px/1.15 var(--sans);letter-spacing:-.02em;color:var(--bone);overflow-wrap:anywhere}
102
102
  #lqtitle .lqpk{margin-left:10px;font:600 11.5px var(--sans);letter-spacing:.06em;text-transform:uppercase;color:var(--amber);vertical-align:middle}
103
103
  #lqpark{align-self:center;padding:5px 12px;font-size:12.5px}
104
+ /* what the open panel does to itself (its Refresh) sits beside Close */
105
+ .lqacts,#lqtools{display:flex;align-items:center;gap:8px;flex:0 0 auto}#lqtools:empty,#lqtools:not(:has(>:not(.hidden))){display:none}
106
+ #pushtasks{padding:7px 14px;font-size:13px}
104
107
  #lqscene{display:none}
105
108
  body.lq-work:not(.lq-pooled) #lqscene{display:block;position:absolute;left:50%;top:50px;transform:translateX(-50%);z-index:3;margin:0;font:650 24px/1.15 var(--sans);letter-spacing:-.02em;color:#F3F0EA;text-shadow:0 1px 12px rgba(0,0,0,.6);pointer-events:none;white-space:nowrap}
106
109
  body.lq-light.lq-work #lqscene{color:#151A21;text-shadow:0 1px 10px rgba(255,255,255,.8)}
@@ -140,7 +143,7 @@ body.lq-liquid.lq-look-pearl button.act,body.lq-look-pearl #lqgo.on,body.lq-liqu
140
143
  body.lq-liquid button.ghost{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.14);border-radius:999px;color:var(--bone);backdrop-filter:blur(6px)}
141
144
  body.lq-liquid.lq-light button.ghost{background:rgba(255,255,255,.7);border-color:rgba(20,30,45,.18)}
142
145
  body.lq-liquid input,body.lq-liquid select,body.lq-liquid textarea{border-radius:12px;border:1px solid rgba(220,228,240,.2);background:rgba(255,255,255,.04);color:var(--bone)}
143
- body.lq-liquid.lq-light input,body.lq-liquid.lq-light select,body.lq-liquid.lq-light textarea{background:#FFFFFF;border-color:rgba(20,30,45,.18)}
146
+ body.lq-liquid.lq-light input,body.lq-liquid.lq-light select,body.lq-liquid.lq-light textarea{background-color:#FFFFFF;border-color:rgba(20,30,45,.18)}
144
147
  body.lq-liquid input:focus,body.lq-liquid select:focus,body.lq-liquid textarea:focus{outline:none;border-color:rgba(255,255,255,.7);box-shadow:0 0 0 3px rgba(200,210,224,.18)}
145
148
  body.lq-liquid .tgroup{color:#C9D1DC;letter-spacing:.16em}
146
149
  body.lq-liquid.lq-light .tgroup{color:#3B4656}
@@ -216,6 +219,7 @@ body.lq-pooled #lqneedt,body.lq-work #lqneedt{display:none}
216
219
  .lqblob .bfx button{padding:0 12px;border-radius:12px;background:#F3F0EA;color:#0A0B0D;font-weight:600}
217
220
  .lqblob .bfx.hidden{display:none}
218
221
  .lqblob .bmore{font-size:11.5px;color:#8A919B}
222
+ .lqblob .bfor{font-size:11.5px;line-height:1.3;color:#8A919B;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
219
223
  .lqblob .nopt:focus-visible,.lqblob .bfree:focus-visible,.lqblob .bfx button:focus-visible{outline:2px solid currentColor;outline-offset:2px}
220
224
  .lqblob.gone{opacity:0!important;filter:blur(8px);transition:opacity .6s ease,filter .6s ease;pointer-events:none!important}
221
225
  body.lq-light .lqd.lqblob{background:rgba(255,255,255,.94);border-color:rgba(154,82,0,.32);box-shadow:0 18px 40px -26px rgba(30,45,60,.5)}
@@ -224,7 +228,7 @@ body.lq-light .lqblob .nopt{background:#FFFFFF;border-color:rgba(20,30,45,.16);c
224
228
  body.lq-light .lqblob .nopt.rec{background:#151A21;color:#FFFFFF;border-color:#151A21}
225
229
  body.lq-light .lqblob .bfx input{background:#FFFFFF;border-color:rgba(20,30,45,.18)}
226
230
  body.lq-light .lqblob .bfx button{background:#151A21;color:#FFFFFF}
227
- body.lq-light .lqblob .bfree,body.lq-light .lqd.lqblob .lt i,body.lq-light .lqblob .bmore{color:#5A6470}
231
+ body.lq-light .lqblob .bfree,body.lq-light .lqd.lqblob .lt i,body.lq-light .lqblob .bmore,body.lq-light .lqblob .bfor{color:#5A6470}
228
232
  body.lq-look-pearl .lqblob .nopt.rec,body.lq-look-pearl .lqblob .bfx button{background:linear-gradient(180deg,#FFFFFF 0%,#F1F2F5 60%,#E6E9EE 100%);color:#1B1E23;border-color:rgba(0,0,0,.08)}
229
233
  body.lq-contrast .lqblob .nopt{border:2px solid currentColor}
230
234
  body.lq-touch .lqblob .nopt,body.lq-touch .lqblob .bfx input{min-height:44px}
@@ -247,6 +251,7 @@ body.lq-talking .lqd.lqblob{opacity:.55}
247
251
  #firststeps .fst span{font-size:12.5px;color:var(--faint);overflow-wrap:anywhere}
248
252
  #firststeps li button{flex:none;padding:5px 12px;font-size:12.5px}
249
253
  #needsbox:empty{display:none}
254
+ #pushout .pushed{margin-top:6px;padding:10px 14px}#pushout .pushed .dh{flex-wrap:wrap;row-gap:2px}#pushout .pushed .dd{font-size:12.5px;line-height:1.5}#pushout>.dd{margin-top:4px}
250
255
  #needsbox{margin:4px 0 12px}
251
256
  #needsbox .nitem{display:flex;flex-direction:column;align-items:stretch;gap:6px;padding:12px 14px;margin-top:8px}
252
257
  #needsbox .nitem .nopt{max-width:720px}
@@ -396,6 +401,13 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
396
401
  .pcard textarea.pedit:focus{outline:none;border-color:var(--green)}
397
402
  .pcard .psrc{margin-top:8px;font-size:12px;color:var(--faint)}.pcard .psrc summary{cursor:pointer}.pcard .psrc div{margin-top:3px;overflow-wrap:anywhere}
398
403
  .pcard .pchars{font-size:11px;color:var(--faint);margin-left:auto}
404
+ .pcard .pshow{margin-top:8px;font-size:12.5px;color:var(--text)}
405
+ .pcard .pmedia{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
406
+ .pcard .pthumb{position:relative;border:1px solid var(--line);border-radius:9px;overflow:hidden;background:var(--ink);line-height:0}
407
+ .pcard .pthumb img,.pcard .pthumb video{display:block;max-height:150px;max-width:260px;width:auto;height:auto}
408
+ .pcard .pthumb .pmdel{position:absolute;top:4px;right:4px;padding:0 7px;min-height:0;line-height:20px;font-size:15px;border-radius:999px;background:var(--ink);opacity:.85}
409
+ .pcard .pthumb .pmdel:hover,.pcard .pthumb .pmdel:focus-visible{opacity:1}
410
+ .pcard .pform input.purl{min-width:0}
399
411
  body.lq-liquid .rcard.pcard,body.lq-liquid.lq-light .rcard.pcard{border-left:3px solid var(--amber)}
400
412
  .tag.cust{background:transparent;border-color:var(--amber);color:var(--amber)}
401
413
  .rrow{cursor:pointer}.rrow:hover,.rrow:focus-visible{border-color:var(--green)}.rrow.rnew{border-left:3px solid var(--amber)}
@@ -500,6 +512,8 @@ body.lq-light .cx .lbtn:hover{background:rgba(21,26,33,.05)}
500
512
  body.lq-light .cx.ok .cxb{box-shadow:0 1px 2px rgba(0,0,0,.25);background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#E3E6EA 22%,#9BA2AC 55%,#3A3F46 100%)}
501
513
  .cx.signin .cxb,.cx.signedout .cxb,.cx.error .cxb{opacity:1;box-shadow:inset 0 0 0 2px var(--amber),0 0 10px rgba(242,165,65,.35)}
502
514
  .cxn{font:500 13.5px var(--sans);color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
515
+ .cxa{flex:none;font-size:12px;color:var(--faint);opacity:.55}.cxa.ready{color:var(--bone);opacity:1}
516
+ .cxlg{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px}.cxh .cxlg .cxlgf{display:inline-flex;align-items:center;gap:6px;margin-left:8px;font:inherit;color:inherit}
503
517
  .cx.ok .cxn{color:var(--bone)}
504
518
  .cxs{margin-left:auto;padding-left:8px;font-size:11.5px;color:var(--faint);white-space:nowrap}
505
519
  .cx.signin .cxs,.cx.signedout .cxs,.cx.error .cxs{color:var(--amber)}
@@ -682,7 +696,8 @@ body.lq-liquid .sset label.check{display:flex;align-items:center;gap:10px;margin
682
696
  body.lq-liquid .sset .note{font-size:12.5px;max-width:70ch}
683
697
  body.lq-liquid .sset .cxh{margin-top:0}
684
698
  /* the Close bar carries the panel's own ground, so what scrolls under it stays clean */
685
- body.lq-liquid.lq-pooled #lqsinkrow{margin:-14px -14px 10px;padding:10px 10px 8px;background:linear-gradient(180deg,rgba(6,8,11,.97) 70%,rgba(6,8,11,0));border-radius:22px 22px 0 0}
699
+ /* top: the panel's padding (main's 16px), so it sticks at the very top edge: scrolled content no longer shows in a strip above it */
700
+ body.lq-liquid.lq-pooled #lqsinkrow{top:-16px;margin:-14px -14px 10px;padding:10px 10px 8px;background:linear-gradient(180deg,rgba(6,8,11,.97) 70%,rgba(6,8,11,0));border-radius:22px 22px 0 0}
686
701
  body.lq-liquid.lq-light.lq-pooled #lqsinkrow{background:linear-gradient(180deg,rgba(246,248,251,.98) 70%,rgba(246,248,251,0))}
687
702
 
688
703
  /* Agents: needs you, at work, finished (folded) */
@@ -743,6 +758,11 @@ body.lq-light .bwthread{border-top-color:rgba(184,106,12,.45)}
743
758
  .pshead{display:flex;flex-direction:column;gap:2px;margin:24px 0 10px}
744
759
  .pshead span{font:600 15px var(--sans);color:var(--bone)}
745
760
  .pshead small{font-size:12.5px;color:var(--faint)}
761
+ /* Marketing's 4-week test: a row a week; the week you're in marked, the ones to come faint */
762
+ .mkwrap{overflow-x:auto}.mktab{width:100%;border-collapse:collapse;font-size:13px}
763
+ .mktab th{font:500 11px var(--sans);letter-spacing:.08em;text-transform:uppercase;color:var(--faint);text-align:left;padding:6px 10px 8px 0;border-bottom:1px solid var(--line);white-space:nowrap}
764
+ .mktab td{padding:9px 10px 9px 0;border-bottom:1px solid var(--line);color:var(--text);vertical-align:baseline}.mktab td b{color:var(--bone)}
765
+ .mktab td.n{font-variant-numeric:tabular-nums}.mktab tr[aria-current] td{color:var(--bone)}.mktab tr[aria-current] td:first-child{box-shadow:inset 2px 0 0 var(--amber);padding-left:8px}
746
766
  .pshelf{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px;align-items:start}
747
767
  body.lq-liquid .pshelf .pcard{position:relative;display:flex;flex-direction:column;gap:10px;margin:0;padding:16px 18px;border-radius:20px;border:1px solid rgba(255,255,255,.08);border-left:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.035)}
748
768
  body.lq-liquid.lq-light .pshelf .pcard{background:rgba(255,255,255,.75);border-color:rgba(21,26,33,.08)}
@@ -769,6 +789,9 @@ body.lq-liquid #panel-tasks .task.open .t{display:block;-webkit-line-clamp:unset
769
789
  #tasklist.onerepo .task .rp,.rcard .task .rp{display:none}
770
790
  body.lq-liquid .rcard{border:1px solid rgba(242,165,65,.3);border-radius:20px;padding:16px 18px 14px;background:rgba(255,255,255,.035);box-shadow:none;margin-top:10px}
771
791
  body.lq-liquid.lq-light .rcard{background:rgba(255,255,255,.75);border-color:rgba(154,82,0,.25)}
792
+ /* a long card's Approve row sticks to the panel's bottom edge (bottom: main's padding) while the card is in view, so it's never scrolled out of reach */
793
+ body.lq-liquid .rcard>.ract{position:sticky;bottom:-16px;z-index:1;margin:8px -18px 0;padding:12px 18px 10px;background:linear-gradient(180deg,rgba(9,11,15,0),rgba(9,11,15,.97) 14px)}
794
+ body.lq-liquid.lq-light .rcard>.ract{background:linear-gradient(180deg,rgba(248,249,251,0),rgba(248,249,251,.96) 14px)}
772
795
  body.lq-liquid .rcard .rhead b{font:600 16px var(--sans);letter-spacing:-.01em}
773
796
  body.lq-liquid .rcard .task{margin-top:6px;padding:9px 12px;border-radius:12px}
774
797
  .rwork{display:flex;align-items:center;gap:9px;margin:10px 0 2px;font-size:13px;color:var(--text)}
@@ -845,7 +868,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
845
868
  .bcard .tchat{border-top:1px solid var(--line);border-radius:9px;margin-top:8px}
846
869
  .bcard .bbrief{white-space:pre-line;margin:8px 0;padding:8px 10px;border-left:3px solid var(--green);background:var(--ink3);border-radius:0 8px 8px 0;font-size:13px;color:var(--bone)}
847
870
  @media(max-width:760px){.maprow{flex-direction:column}.detail{width:auto;max-height:none}}
848
- @media(max-width:600px){#panel-tasks .row>#newtask,#screenname,#pagesite{flex:1 1 100%!important}.tabs{overflow-x:auto;scrollbar-width:none;padding:0 8px;gap:0}.tabs::-webkit-scrollbar{display:none}.tab{flex:none;padding:9px 11px}header{padding:12px 14px 8px}main{padding:12px}}
871
+ @media(max-width:600px){#screenname,#pagesite{flex:1 1 100%!important}.tabs{overflow-x:auto;scrollbar-width:none;padding:0 8px;gap:0}.tabs::-webkit-scrollbar{display:none}.tab{flex:none;padding:9px 11px}header{padding:12px 14px 8px}main{padding:12px}}
849
872
  </style></head><body>
850
873
  <div id="liquid" aria-label="Symbiot">
851
874
  <canvas id="lq" aria-hidden="true"></canvas>
@@ -879,11 +902,11 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
879
902
  <button class="tab" data-tab="todo">Todo</button>
880
903
  <button class="tab" data-tab="tasks">Tasks</button>
881
904
  <button class="tab" data-tab="agents">Agents</button>
882
- <button class="tab" data-tab="reports">Reports</button>
905
+ <button class="tab" data-tab="reports">Reports</button><button class="tab" data-tab="marketing">Marketing</button>
883
906
  <button class="tab" data-tab="settings">Settings</button>
884
907
  </div>
885
908
  <main>
886
- <div id="lqsinkrow"><div class="lqwhere"><span id="lqcrumb" class="lqcrumb">Home</span><h2 id="lqtitle" aria-live="polite"></h2><button type="button" class="ghost hidden" id="lqpark"></button></div><button class="iconbtn wide" type="button" id="lqsink" title="close it: it sinks back into the liquid (Esc, or right-click)"><svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 2l8 8M10 2l-8 8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>Close</button></div>
909
+ <div id="lqsinkrow"><div class="lqwhere"><span id="lqcrumb" class="lqcrumb">Home</span><h2 id="lqtitle" aria-live="polite"></h2><button type="button" class="ghost hidden" id="lqpark"></button></div><div class="lqacts"><span id="lqtools"><button class="ghost hidden" type="button" id="pushtasks" title="Write .symbiot/TASKS.md into each repo for your coding agent">Send to repos</button></span><button class="iconbtn wide" type="button" id="lqsink" title="close it: it sinks back into the liquid (Esc, or right-click)"><svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 2l8 8M10 2l-8 8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>Close</button></div></div>
887
910
  <section id="panel-map">
888
911
  <div class="profile muted" id="profile">Mapping your work&hellip;</div>
889
912
  <div class="maprow">
@@ -921,7 +944,6 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
921
944
  <div id="bstream" class="bstream"><canvas id="bsc" aria-hidden="true"></canvas><div id="bsov"></div></div>
922
945
  <div id="boardtalk"></div>
923
946
  <div id="boardawait"></div>
924
- <div id="boardposts"></div>
925
947
  <details id="blist" class="blist"><summary>All of it as a list</summary><div id="board" class="board"></div></details>
926
948
  <div id="boardlinks"></div>
927
949
  </section>
@@ -931,8 +953,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
931
953
  <div class="rfoot" id="outfoot" style="display:none"></div>
932
954
  </section>
933
955
  <section id="panel-tasks" class="hidden">
934
- <div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><select id="newtaskrepo" title="Which repo this task is for (needed to send it to an agent)" style="flex:0 0 auto;max-width:180px"><option value="">repo…</option></select><button class="act" id="addtask">Add</button><button class="ghost" id="pushtasks" title="Write .symbiot/TASKS.md into each repo for your coding agent">Send to repos</button></div>
935
- <details class="tnhow"><summary>How tasks work</summary><div class="note muted">To give tasks to your agent, use <b>Send to repos</b> &mdash; filter by tag below to choose which. As the agent finishes each one it lands in <b>Awaiting your review</b>: <b>Approve</b> commits it on a branch and opens a PR, <b>&#8630;</b> sends it back. Ticking a task yourself marks it done (it auto-archives). Click <b><i class=ic-chat></i></b> on a task to ask questions about it.</div></details>
956
+ <details class="tnhow"><summary>How tasks work</summary><div class="note muted">To add a task, tell Symbiot in the bar at the bottom: it files it under the right project. To give tasks to your agent, use <b>Send to repos</b> at the top, beside Close &mdash; filter by tag below to choose which. As the agent finishes each one it lands in <b>Awaiting your review</b>: <b>Approve</b> commits it on a branch and opens a PR, <b>&#8630;</b> sends it back. Ticking a task yourself marks it done (it auto-archives). Click <b><i class=ic-chat></i></b> on a task to ask questions about it.</div></details>
936
957
  <div id="needsbox"></div>
937
958
  <div id="pushout"></div>
938
959
  <div id="reviewout"></div>
@@ -941,7 +962,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
941
962
  <div id="tasklist"></div>
942
963
  </section>
943
964
  <section id="panel-agents" class="hidden">
944
- <div class="row"><span class="muted">Agents Symbiot has handed work to — live status and output. Questions, options and ideas an agent leaves for you show up on its block, whichever model it runs.</span><button class="iconbtn" id="agentsrefresh" style="margin-left:auto" 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>
965
+ <div class="row"><span class="muted">Agents Symbiot has handed work to — live status and output. 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>
945
966
  <div id="agentsmsg"></div>
946
967
  <div id="laneslist"></div>
947
968
  <div id="agentslist"></div>
@@ -951,6 +972,13 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
951
972
  <div id="reportslist"></div>
952
973
  <div id="reportview"></div>
953
974
  </section>
975
+ <section id="panel-marketing" class="hidden">
976
+ <div class="row"><span class="muted" style="flex:1">What you post about your work, and who answers: your drafts, the replies to them, and the 4-week test of whether posting brings anyone. Symbiot never posts: you do, on LinkedIn.</span></div>
977
+ <div id="mkmsg"></div>
978
+ <div id="boardposts"></div>
979
+ <div id="mkreplies"></div>
980
+ <div id="mktest"></div>
981
+ </section>
954
982
  <section id="panel-drift" class="hidden">
955
983
  <div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
956
984
  <label class="muted swl" style="margin-left:auto"><input type="checkbox" class="sw" id="driftfetch"> Fetch latest</label>
@@ -968,7 +996,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
968
996
  <option value="gemini">Gemini (Google)</option>
969
997
  <option value="ollama">Local model (Ollama) - free, no key</option>
970
998
  </select>
971
- <div class="note muted">The Map above needs no key. For the write-ups, a hosted model needs an API key - or run <b>Ollama</b> locally for a free, private option (nothing leaves your machine).</div>
999
+ <div class="note muted">The Map needs no key. For the write-ups, a hosted model needs an API key - or run <b>Ollama</b> locally for a free, private option (nothing leaves your machine).</div>
972
1000
  <div id="keyWrap"><label>API key</label><input id="key" type="password" placeholder="paste your key">
973
1001
  <div class="note"><a id="getkey">Where do I get a key?</a></div></div>
974
1002
  <div id="baseWrap" class="hidden"><label>Ollama URL</label><input id="baseUrl" type="text" value="http://localhost:11434"></div>
@@ -1083,9 +1111,10 @@ $('panel-tasks').classList.toggle('hidden',!isTasks);
1083
1111
  $('panel-drift').classList.toggle('hidden',!isDrift);
1084
1112
  $('panel-agents').classList.toggle('hidden',!isAgents);
1085
1113
  $('panel-reports').classList.toggle('hidden',tab!=='reports');if(tab==='reports')loadReports();
1114
+ $('panel-marketing').classList.toggle('hidden',tab!=='marketing');if(tab==='marketing')loadMarketing();
1086
1115
  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();}
1087
1116
  if(isMap&&!mapLoaded)loadMap();
1088
- if(isTasks){fillTaskRepos();loadTasks();}
1117
+ if(isTasks)loadTasks();
1089
1118
  if(isDrift&&!driftLoaded)loadDrift();
1090
1119
  if(isAgents)loadAgents(); else stopAgentsPoll();
1091
1120
  lqTitle();if(isTasks&&TFILTER.repo)loadParked(lqTitle);}
@@ -1106,6 +1135,9 @@ var fsTimer=null;(function(){var ps=$('panel-settings');if(ps&&ps.addEventListen
1106
1135
  var tabPicked=false;tabs().forEach(function(t){t.addEventListener('click',function(){tabPicked=true;setTab(t.dataset.tab);});});
1107
1136
  // Watching a page (your inbox, GitHub)? The app opens on the Dashboard instead of
1108
1137
  // the Map, unless you've picked a tab already. The Map still loads behind it.
1138
+ // A link to a screen (#reports, #board…) opens on it: how symbiot post's picture of
1139
+ // "the new Reports view" shows that view, not home.
1140
+ function lqDeep(){var h=String(location.hash||'').slice(1);if(!h||!LQNAMES[h])return;setTimeout(function(){tabPicked=true;lqPool(h);},400);}
1109
1141
  function firstTab(){api('/api/watch/board').then(function(b){if(b&&b.cards&&b.cards.length&&!tabPicked&&current==='map')setTab('board');}).catch(function(){});}
1110
1142
  // The AI in use isn't shown while it works (it's noise); only when none is
1111
1143
  // connected, as an urgent line that opens its part of Settings. Home has it out
@@ -1123,6 +1155,9 @@ var tf=typeof TFILTER!=='undefined'&&TFILTER||{},arch=typeof TARCH!=='undefined'
1123
1155
  if(proj)name=lqProjName(proj);var parked=!!proj&&PARKED.repos.indexOf(proj)>=0;
1124
1156
  if(c)c.textContent=proj?'Projects':'Home';t.innerHTML=esc(name)+(parked?"<span class='lqpk'>parked</span>":'');
1125
1157
  var pk=$('lqpark');if(pk){pk.classList.toggle('hidden',!proj);pk.textContent=parked?'Unpark':'Park this project';pk.title=parked?'let its tasks start agent runs again':'stop its tasks starting agent runs, until you unpark it';pk.setAttribute('data-repo',proj);}
1158
+ // the open panel's own Refresh sits beside Close, not under it on the panel's top line
1159
+ var tl=$('lqtools');if(tl&&tl.appendChild)['agents','reports'].forEach(function(p){var b=$(p+'refresh'),row=$('panel-'+p);row=row&&row.querySelector('.row');if(!b)return;if(p===current){if(b.parentNode!==tl)tl.appendChild(b);}else if(b.parentNode===tl&&row&&row.appendChild)row.appendChild(b);});
1160
+ var pt=$('pushtasks');if(pt&&pt.classList)pt.classList.toggle('hidden',current!=='tasks'||!!arch);
1126
1161
  try{document.title=name+' \\u00b7 Symbiot';}catch(e){}}
1127
1162
  function lqPark(repo,on,then,path){api('/api/lanes/park',{repo:repo,path:path||'',on:on}).then(function(r){if(r&&r.error){if(then)then(r.error);return;}loadParked(function(){lqTitle();if(then)then('');if(LQ.scene==='work')lqLoadWork();});});}
1128
1163
  function loadParked(then){api('/api/lanes/parked').then(function(p){PARKED={repos:(p&&p.repos)||[],paths:(p&&p.paths)||[]};if(then)then();}).catch(function(){if(then)then();});}
@@ -1369,7 +1404,7 @@ else if(ur)h+="<div class='muted' style='margin:6px 0'><span class='err'>Unrelea
1369
1404
  // The version is already released (its v* tag exists, or npm has it) and these changes keep it:
1370
1405
  // Approve bumps it in the PR too, a patch unless you pick otherwise.
1371
1406
  var bo=r.bumpOffer;if(bo&&r.files.length)h+="<div class='row' style='margin:6px 0;font-size:12px'><span class='muted'>Version</span><select class='bumpsel' title='bump the version in package.json (and the lockfile) in this PR' style='flex:0 0 auto;width:auto'><option value='patch'>bump to "+esc(bo.patch)+" (patch)</option><option value='minor'>bump to "+esc(bo.minor)+" (minor)</option><option value=''>keep "+esc(bo.version)+"</option></select></div>";
1372
- h+="<div class='row'>"+(pq&&r.path?"<button class='act answerq' data-path='"+esc(r.path).replace(/'/g,'&#39;')+"'>Answer its question"+(pq>1?"s":"")+"</button>":"")+"<button class='"+(pd?'ghost':'act')+" approve'"+(pd?" data-partly='1'":"")+(r.path&&!r.running?"":" disabled")+(r.running?" title='the agent is still editing this repo'>"+(r.untasked?"Approve changes without a task":"Approve")+" &middot; agent still working":r.untasked?" data-untasked='1' title='commit on a branch, push and open a PR, without a task'>Approve changes without a task &rarr; PR":" title='commit on a branch, push, open a PR, then archive'>Approve &rarr; "+(r.files.length?"PR":"archive"))+"</button>"+(r.files.length?"<button class='ghost showdiff'>Show diff</button>":"")+"<label title='Queue GitHub auto-merge so this PR lands once its CI checks pass. Needs Allow auto-merge on the repo.' style='margin-left:auto;font-size:12px;color:var(--faint);display:flex;align-items:center;gap:6px'><input type='checkbox' class='amtoggle' style='width:auto'"+(r.autoMerge?" checked":"")+"> auto-merge on green CI</label></div><div class='rdiff hidden'></div></div>";});
1407
+ h+="<div class='row ract'>"+(pq&&r.path?"<button class='act answerq' data-path='"+esc(r.path).replace(/'/g,'&#39;')+"'>Answer its question"+(pq>1?"s":"")+"</button>":"")+"<button class='"+(pd?'ghost':'act')+" approve'"+(pd?" data-partly='1'":"")+(r.path&&!r.running?"":" disabled")+(r.running?" title='the agent is still editing this repo'>"+(r.untasked?"Approve changes without a task":"Approve")+" &middot; agent still working":r.untasked?" data-untasked='1' title='commit on a branch, push and open a PR, without a task'>Approve changes without a task &rarr; PR":" title='commit on a branch, push, open a PR, then archive'>Approve &rarr; "+(r.files.length?"PR":"archive"))+"</button>"+(r.files.length?"<button class='ghost showdiff'>Show diff</button>":"")+"<label title='Queue GitHub auto-merge so this PR lands once its CI checks pass. Needs Allow auto-merge on the repo.' style='margin-left:auto;font-size:12px;color:var(--faint);display:flex;align-items:center;gap:6px'><input type='checkbox' class='amtoggle' style='width:auto'"+(r.autoMerge?" checked":"")+"> auto-merge on green CI</label></div><div class='rdiff hidden'></div></div>";});
1373
1408
  el.innerHTML=h;
1374
1409
  el.querySelectorAll('.rcard').forEach(function(card){var repo=card.getAttribute('data-repo');
1375
1410
  card.querySelectorAll('.task').forEach(function(row){wireAsk(row,row.getAttribute('data-id'));});
@@ -1448,6 +1483,13 @@ api('/api/agents/skip',{path:a.path,text:s.text}).then(function(r){if(!r||r.erro
1448
1483
  // A step of yours an answer picked (👤 You): the next run waits for it (agents.mjs waitingFor)
1449
1484
  function waitHtml(a){var w=a.waiting;if(!w)return '';var fs=(w.files||[]).map(function(f){return "<code>"+esc(f)+"</code>";}).join(" or ");if(w.cmd)fs=(fs?fs+" or ":"")+"the agent command in Settings &rarr; Handoff";
1450
1485
  return "<div class='aq'><h4><i class=ic-pause></i> Waiting on your step</h4><div class='qt'><i class=ic-person></i> "+esc(w.step)+"</div><div class='qc'>"+(fs?"Your agent "+(w.rerun?"starts by itself":"can start")+" once "+fs+" changes"+(w.rerun?", while Symbiot runs":"")+". Done it some other way?":"Your agent waits for it, so it doesn't stop on the same questions again.")+" Start it now once it's done.</div><div class='row'><button class='act wstart' data-id='"+esc(a.id)+"'>Start it now</button></div></div>";}
1486
+ // A run that ended waiting on you (agents.mjs needsOf): your OK for what it would send or
1487
+ // change, or a step only you can do, with what to check first. Answered as on Home.
1488
+ function needsHtml(a){var n=a.needs;if(!n||n.waiting)return '';var ok=n.kind==='approve';
1489
+ return "<div class='aq'><h4><i class=ic-person></i> "+(ok?"Waits for your OK":"Needs you")+"</h4><div class='qt'>"+esc(n.what)+"</div>"+(n.check?"<div class='qc'><b>Check first:</b> "+esc(n.check)+"</div>":"")
1490
+ +"<div class='row'><button class='act ngo' data-id='"+esc(a.id)+"' title='"+(ok?"its agent goes ahead with it":"its agent checks it worked and carries on")+"'>"+(ok?"Go ahead":"Done it")+"</button><button class='ghost nskip' data-id='"+esc(a.id)+"' title='it stops asking'>Skip</button></div></div>";}
1491
+ function wireNeeds(el){el.querySelectorAll('.ngo,.nskip').forEach(function(btn){btn.addEventListener('click',function(){var a=agentById(btn.getAttribute('data-id'));if(!a)return;btn.disabled=true;var msg=document.getElementById('agentsmsg');
1492
+ api('/api/home/answer',{id:'needs:'+a.id,pick:btn.classList.contains('ngo')?0:1}).then(function(r){msg.innerHTML=r&&!r.error?"<div class='note ok'>"+esc(a.name)+": "+esc(r.said||'Done.')+"</div>":"<div class='note err'>"+esc((r&&r.error)||'failed')+"</div>";loadAgents();}).catch(function(e){btn.disabled=false;msg.innerHTML="<div class='note err'>"+esc(String((e&&e.message)||e))+"</div>";});});});}
1451
1493
  function wireWaits(el){el.querySelectorAll('.wstart').forEach(function(btn){btn.addEventListener('click',function(){var a=agentById(btn.getAttribute('data-id'));if(!a)return;btn.disabled=true;
1452
1494
  api('/api/open',{path:a.path,force:true}).then(function(x){var msg=document.getElementById('agentsmsg');msg.innerHTML=x&&x.opened?"<div class='note ok'>&#10003; Started your agent in <b>"+esc(a.name)+"</b>.</div>":"<div class='note err'>"+esc(a.name)+": "+(x&&x.busy?"an agent is already running there.":"it didn&#39;t start. Check the command in Settings.")+"</div>";loadAgents();}).catch(function(e){btn.disabled=false;document.getElementById('agentsmsg').innerHTML="<div class='note err'>"+esc(String((e&&e.message)||e))+"</div>";});});});}
1453
1495
  // What a run left for Symbiot's memory (.symbiot/REMEMBER.json, handback.mjs): nothing goes in until Remember
@@ -1504,7 +1546,7 @@ function loadAgents(){loadLanes();loadParked(agentsDraw);}
1504
1546
  // Agents, in order: what needs you (a question, a step of yours), what's at work, and
1505
1547
  // what's finished, folded away (open stays open across refreshes).
1506
1548
  var AGDONEOPEN=false;
1507
- function agentGroups(list,fn){var needs=[],run=[],done=[];list.forEach(function(a){if(nQs(a)||a.waiting)needs.push(a);else if(a.status==='running')run.push(a);else done.push(a);});
1549
+ function agentGroups(list,fn){var needs=[],run=[],done=[];list.forEach(function(a){if(nQs(a)||a.waiting||a.needs)needs.push(a);else if(a.status==='running')run.push(a);else done.push(a);});
1508
1550
  var g=function(t,xs){return xs.length?"<div class='tgroup agg'>"+t+" <span class='tcount'>"+xs.length+"</span></div>"+xs.map(fn).join(''):'';};
1509
1551
  return g('Needs you',needs)+g('At work',run)+(done.length?"<details class='agdone'"+(AGDONEOPEN?' open':'')+"><summary class='tgroup agg'>Finished <span class='tcount'>"+done.length+"</span></summary>"+done.map(fn).join('')+"</details>":'');}
1510
1552
  function agentsDraw(){api('/api/agents').then(function(list){var el=document.getElementById('agentslist');
@@ -1518,6 +1560,7 @@ if(a.fromHeld)st+=" &middot; started on the tasks held for the last run";
1518
1560
  if(a.earlier)st+=" &middot; "+(a.status==='running'?"started outside this window":"ran before Symbiot last started");
1519
1561
  if(a.waiting)st+=" &middot; <span style='color:var(--amber)'>waiting on your step</span>";
1520
1562
  if(nQs(a))st+=" &middot; <span style='color:var(--amber)'>needs your answers</span>";
1563
+ if(a.needs&&!a.needs.waiting)st+=" &middot; <span style='color:var(--amber)'>"+(a.needs.kind==='approve'?"waits for your OK":"needs you")+"</span>";
1521
1564
  if(a.remember&&a.remember.length)st+=" &middot; <span style='color:var(--amber)'>found "+a.remember.length+" thing"+(a.remember.length>1?"s":"")+" to remember</span>";
1522
1565
  if(a.held!=null){var hn=a.held.length,ht=a.held.map(function(t){return "&bull; "+esc(t).replace(/'/g,'&#39;');}).join('&#10;');
1523
1566
  ht+=(hn?'&#10;&#10;':'')+(a.status==='running'?"Sent while this agent was running. They wait in .symbiot/TASKS.next.md, replace TASKS.md when it finishes (keeping its ticks), and an agent starts on them then.":"They wait in .symbiot/TASKS.next.md for the agent running in this folder to finish. After that, an agent starts on them the next time the Tasks tab checks this repo, or send again.");
@@ -1525,7 +1568,7 @@ st+=" &middot; <span style='color:var(--amber);cursor:help' title='"+ht+"'><i cl
1525
1568
  // a repo's run can be parked from here in one click: its tasks start no runs until it's unparked
1526
1569
  var pk=!/\\/drafts\\/act-[^/]*$/.test(String(a.path||''))&&a.path,parked=pk&&PARKED.paths.indexOf(a.path)>=0;
1527
1570
  if(parked)st+=" &middot; <span style='color:var(--amber)'>parked: no new runs</span>";
1528
- var b="<div class='dh'><span class='orb "+cls+"'></span><span class='dn'>"+esc(String(a.name||'').split('**').join(''))+"</span><span class='dd'>"+st+"</span>"+(pk?"<button type='button' class='ghost apark' data-on='"+(parked?'0':'1')+"' title='"+(parked?"let its tasks start agent runs again":"stop its tasks starting agent runs, until you unpark it")+"'>"+(parked?'Unpark':'Park')+"</button>":"")+"</div>";
1571
+ var b="<div class='dh'><span class='orb "+cls+"'></span><span class='dn'>"+esc(String(a.name||'').split('**').join('').replace(/^(Agent:\s*)What['’]s needed:\s*/i,'$1'))+"</span><span class='dd'>"+st+"</span>"+(pk?"<button type='button' class='ghost apark' data-on='"+(parked?'0':'1')+"' title='"+(parked?"let its tasks start agent runs again":"stop its tasks starting agent runs, until you unpark it")+"'>"+(parked?'Unpark':'Park')+"</button>":"")+"</div>";
1529
1572
  b+=workHtml(a);
1530
1573
  var ch=a.changed;
1531
1574
  if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
@@ -1537,12 +1580,12 @@ if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
1537
1580
  }else if(a.status==='done'){
1538
1581
  b+="<div class='changed muted'>No file changes detected (the agent may have only planned or asked).</div>";
1539
1582
  }
1540
- b+=waitHtml(a)+askHtml(a)+factsHtml(a);
1583
+ b+=waitHtml(a)+needsHtml(a)+askHtml(a)+factsHtml(a);
1541
1584
  b+=a.work?("<details class='wkraw'><summary>what it said, in full</summary><pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(nothing yet)')+"</pre></details>"):("<pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(waiting for output…)')+"</pre>");
1542
1585
  return "<div class='agent' data-i='"+list.indexOf(a)+"'>"+b+"</div>";};
1543
1586
  el.innerHTML=agentGroups(list,agentBlock);var dn=el.querySelector('.agdone');if(dn)dn.addEventListener('toggle',function(){AGDONEOPEN=dn.open;});
1544
1587
  el.querySelectorAll('.alogout').forEach(function(p){p.scrollTop=p.scrollHeight;});
1545
- restoreDrafts(el);wireAsks(el);wireWaits(el);wireFacts(el);lqLight();
1588
+ restoreDrafts(el);wireAsks(el);wireWaits(el);wireNeeds(el);wireFacts(el);lqLight();
1546
1589
  el.querySelectorAll('.apark').forEach(function(btn){btn.addEventListener('click',function(){var g=AGENTLIST[+btn.closest('.agent').getAttribute('data-i')];if(!g)return;btn.disabled=true;lqPark('',btn.getAttribute('data-on')==='1',function(err){btn.disabled=false;$('agentsmsg').innerHTML=err?"<div class='note err'>"+esc(err)+"</div>":'';loadAgents();},g.path);});});
1547
1590
  stopAgentsPoll();if(anyRunning&&current==='agents')agentsTimer=setTimeout(loadAgents,2000);});}
1548
1591
  function loadDrift(){var out=document.getElementById('driftout');out.innerHTML="<div class='muted' style='margin-top:12px'>Reading your repos&hellip;</div>";
@@ -1557,16 +1600,15 @@ var clean=repos.filter(function(r){return !r.flags.length;}).map(function(r){ret
1557
1600
  if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clean.join(", "))+"</div>";
1558
1601
  if(!repos.length)h+="<div class='muted'>No repos found under "+esc(d.base||"your home folder")+".</div>";
1559
1602
  out.innerHTML=h;});}
1560
- function fillTaskRepos(){var sel=document.getElementById('newtaskrepo');if(!sel||!GRAPH)return;var cur=sel.value;var names=GRAPH.nodes.filter(function(n){return n.type==='repo'||n.type==='folder';}).map(function(n){return n.label;}).sort();sel.innerHTML="<option value=''>repo…</option>"+names.map(function(n){return "<option value='"+esc(n)+"'"+(n===cur?" selected":"")+">"+esc(n)+"</option>";}).join("");}
1561
- function addTaskUI(){var i=document.getElementById('newtask');var v=(i.value||'').trim();if(!v)return;var repo=(document.getElementById('newtaskrepo')||{}).value||'';api('/api/tasks/add',{text:v,repo:repo}).then(function(){i.value='';TFILTER={type:'',repo:''};loadTasks();});}
1562
1603
  function pushTasksUI(){var o=document.getElementById('pushout');var btn=document.getElementById('pushtasks');btn.disabled=true;
1563
1604
  o.innerHTML="<div class='muted' style='margin-top:10px'><span class='dot-c' style='background:var(--amber)'></span> Writing .symbiot/TASKS.md into your repos&hellip;</div>";
1564
1605
  api('/api/tasks/push',{type:TFILTER.type||'',repo:TFILTER.repo||''}).then(function(r){
1565
- if(r.empty){btn.disabled=false;o.innerHTML="<div class='muted' style='margin-top:10px'>No open tasks to send. Tick ideas in a repo review, or add tasks above.</div>";return;}
1566
- var n=(r.written||[]).length;var opens=[];var h="<div class='drift' style='margin-top:10px'><div class='dh'><span class='dot-c'></span><span class='dn'>Done &mdash; wrote "+n+" file"+(n===1?"":"s")+"</span></div>";
1567
- if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>&#10003; <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span>"+(w.held?" <span class='ev'><i class=ic-pause></i> held: an agent is still running there, so these land when it finishes</span>":"")+"</li>";});h+="</ul>";}
1568
- if(r.unresolved&&r.unresolved.length){var names=r.unresolved.map(function(u){return u.name;}).join(", ");var hasNoRepo=r.unresolved.some(function(u){return u.name==='(no repo)';});h+="<div class='dd' style='margin-top:6px'><i class=ic-warn></i> not sent: "+esc(names)+". "+(hasNoRepo?"Pick a repo in the dropdown next to <b>Add</b> so the task has somewhere to go.":"That repo isn't in the map &mdash; add its folder in Settings.")+"</div>";}
1569
- if(r.handoff&&r.written&&r.written.length){opens=r.written.map(function(w){return api('/api/open',{path:w.path}).then(function(x){if(x&&x.blocked){var d=document.createElement('div');d.className='dd';d.innerHTML="<i class=ic-pause></i> <b>"+esc(w.name)+"</b>: "+esc(x.note)+" <button class='ghost' style='padding:2px 8px;font-size:12px'>Start it anyway</button>";d.querySelector('button').addEventListener('click',function(){this.disabled=true;api('/api/open',{path:w.path,force:true}).then(function(y){d.innerHTML=y&&y.opened?"&#10003; Started an agent in <b>"+esc(w.name)+"</b>.":"<i class=ic-warn></i> <b>"+esc(w.name)+"</b>: "+(y&&y.busy?"an agent is already running there.":"it didn&#39;t start. Check the command in Settings.");});});o.appendChild(d);}if(x&&x.busy){var d=document.createElement('div');d.className='dd';d.innerHTML="<i class=ic-warn></i> <b>"+esc(w.name)+"</b> already has an agent running, so another wasn&#39;t started. "+(x.auto?"Its new tasks are held, and an agent starts on them when it finishes.":"Its new tasks are held until it finishes. After that, an agent starts on them the next time this tab checks the repo, or send again.");o.appendChild(d);}}).catch(function(){});});h+="<div class='dd' style='margin-top:8px'><i class=ic-bot></i> Handed "+n+" repo(s) to your agent &mdash; opening the <b>Agents</b> tab to watch it work&hellip;</div>";setTimeout(function(){setTab('agents');},500);}
1606
+ if(r.empty){btn.disabled=false;o.innerHTML="<div class='muted' style='margin-top:10px'>No open tasks to send. Tell Symbiot what to do in the bar at the bottom, or tick ideas in a repo review.</div>";return;}
1607
+ var n=(r.written||[]).length;var opens=[];
1608
+ // one line, its paths on hover: the review cards below it stay in view
1609
+ var h="<div class='drift pushed'><div class='dh'><span class='dot-c'></span><span class='dn'>Sent to "+n+" repo"+(n===1?"":"s")+"</span><span class='dd'>"+(r.written||[]).map(function(w){return "<span title='"+esc(w.file).replace(/'/g,'&#39;')+"'>&#10003; <b>"+esc(w.name)+"</b> ("+w.count+" task"+(w.count===1?"":"s")+(w.held?", <i class=ic-pause></i> held till its running agent finishes":"")+")</span>";}).join(" &middot; ")+"</span></div>";
1610
+ if(r.unresolved&&r.unresolved.length){var names=r.unresolved.map(function(u){return u.name;}).join(", ");var hasNoRepo=r.unresolved.some(function(u){return u.name==='(no repo)';});h+="<div class='dd' style='margin-top:6px'><i class=ic-warn></i> not sent: "+esc(names)+". "+(hasNoRepo?"Tell Symbiot in the bar at the bottom which project it&#39;s for, so it has somewhere to go.":"That repo isn't in the map &mdash; add its folder in Settings.")+"</div>";}
1611
+ if(r.handoff&&r.written&&r.written.length){opens=r.written.map(function(w){return api('/api/open',{path:w.path}).then(function(x){if(x&&x.blocked){var d=document.createElement('div');d.className='dd';d.innerHTML="<i class=ic-pause></i> <b>"+esc(w.name)+"</b>: "+esc(x.note)+" <button class='ghost' style='padding:2px 8px;font-size:12px'>Start it anyway</button>";d.querySelector('button').addEventListener('click',function(){this.disabled=true;api('/api/open',{path:w.path,force:true}).then(function(y){d.innerHTML=y&&y.opened?"&#10003; Started an agent in <b>"+esc(w.name)+"</b>.":"<i class=ic-warn></i> <b>"+esc(w.name)+"</b>: "+(y&&y.busy?"an agent is already running there.":"it didn&#39;t start. Check the command in Settings.");});});o.appendChild(d);}if(x&&x.busy){var d=document.createElement('div');d.className='dd';d.innerHTML="<i class=ic-warn></i> <b>"+esc(w.name)+"</b> already has an agent running, so another wasn&#39;t started. "+(x.auto?"Its new tasks are held, and an agent starts on them when it finishes.":"Its new tasks are held until it finishes. After that, an agent starts on them the next time this tab checks the repo, or send again.");o.appendChild(d);}}).catch(function(){});});h+="<div class='dd'><i class=ic-bot></i> Handed "+n+" repo"+(n===1?"":"s")+" to your agent &mdash; opening the <b>Agents</b> tab to watch it work&hellip;</div>";setTimeout(function(){setTab('agents');},500);}
1570
1612
  else{h+="<div class='dd' style='margin-top:8px'>Set an <b>agent command</b> in Settings to auto-run it on send (and watch it in the Agents tab). For now, tell your agent: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div>";}
1571
1613
  h+="</div>";
1572
1614
  o.innerHTML=h;
@@ -1660,10 +1702,17 @@ function escQ(t){return esc(t).replace(/'/g,'&#39;');}
1660
1702
  // Connections: a column per kind of site; each site a bead in the look: solid when
1661
1703
  // linked, amber when it wants you to sign in, hollow when it isn't linked. Link,
1662
1704
  // check (↻) and Unlink show on hover (always on touch).
1663
- function linksHtml(d,board){var h="<div class='cxh'><span>Connections</span><small>"+(d.linked?"What Symbiot watches for you. Link more to see what arrives there, here and in Week and Standup.":"Link your work to see what arrives there, here and in Week and Standup.")+"</small></div>";
1705
+ // The plug marks the sites your agent's runs can use too, before you link them: only
1706
+ // those Claude has a connector for, and only with Claude Code as your agent (agents.mjs
1707
+ // linkReach). Faint until you've connected it in claude.ai.
1708
+ function reachOf(d,id){var r=d.reach,s=r&&r.claude&&r.sites&&r.sites[id];return s||null;}
1709
+ function linksHtml(d,board){var anyR=d.items.some(function(it){return reachOf(d,it.id);});
1710
+ var h="<div class='cxh'><span>Connections</span><small>"+(d.linked?"What Symbiot watches for you. Link more to see what arrives there, here and in Week and Standup.":"Link your work to see what arrives there, here and in Week and Standup.")+"</small>"
1711
+ +(anyR?"<small class='cxlg'><i class='ic-plug cxa ready' aria-hidden='true'></i> your agent&#39;s runs can use it too, through Claude&#39;s connector for it <span class='cxlgf'><i class='ic-plug cxa' aria-hidden='true'></i> faint: once you connect it in claude.ai (Settings &rarr; Connectors)</span></small>":"")+"</div>";
1664
1712
  if(d.error)h+="<div class='note err'>"+esc(d.error)+"</div>";
1665
1713
  h+="<div class='cxg'>"+d.groups.map(function(g){return "<div class='cxc'><div class='cxl'>"+esc(g)+"</div>"+d.items.filter(function(x){return x.group===g;}).map(function(it){var on=it.state!=='off';
1666
- return "<div class='lnk cx "+escQ(it.state)+"' data-id='"+escQ(it.id)+"'><button type='button' class='lbtn' title='"+escQ(it.note||(on?'open '+it.url+' to sign in again':'sign in at '+it.url+' and link it'))+"'><span class='cxb' aria-hidden='true'></span><span class='cxn'>"+esc(it.name)+"</span><span class='cxs'>"+(on?esc(LINKWORD[it.state]||''):'Link')+"</span></button>"+
1714
+ var rc=reachOf(d,it.id),rt=rc?(rc.ready?" Your agent's runs can use it too, through Claude's "+rc.connector.replace(/^claude\.ai\s+/i,'')+" connector.":" Your agent's runs can use it too once you connect "+rc.name+" in claude.ai → Settings → Connectors (only you can sign in there)."):'';
1715
+ return "<div class='lnk cx "+escQ(it.state)+"' data-id='"+escQ(it.id)+"'><button type='button' class='lbtn' title='"+escQ(String(it.note||(on?'open '+it.url+' to sign in again':'sign in at '+it.url+' and link it')).replace(/\.?\s*$/,rt?'.':'')+rt)+"'><span class='cxb' aria-hidden='true'></span><span class='cxn'>"+esc(it.name)+"</span>"+(rc?"<i class='ic-plug cxa"+(rc.ready?" ready":"")+"' role='img' aria-label='"+escQ(rc.ready?"agent runs can use it too":"agent runs can use it once connected in claude.ai")+"'></i>":"")+"<span class='cxs'>"+(on?esc(LINKWORD[it.state]||''):'Link')+"</span></button>"+
1667
1716
  (on?"<button type='button' class='lmore' title='check it now' aria-label='Check "+escQ(it.name)+" now'>&#8635;</button><button type='button' class='lrm' title='unlink: stop watching it and stop trusting it'>Unlink</button>":"")+"</div>";}).join('')+"</div>";}).join('')+"</div>";return h;}
1668
1717
  function linksOut(html){var o=document.getElementById('linksmsg');if(o)o.innerHTML=html;var b=document.getElementById('boardmsg');if(b&&LINKS&&!LINKS.linked)b.innerHTML=html;}
1669
1718
  function renderLinks(){if(!LINKS)return;['links','boardlinks'].forEach(function(id){var el=document.getElementById(id);if(!el)return;
@@ -1730,7 +1779,7 @@ function scanLine(s){return 'Mapping your work… '+s.phase+(s.total?' '+s.done+
1730
1779
  function loadMap(){var p=document.getElementById("profile");p.textContent="Mapping your work...";document.getElementById("graph").innerHTML="";sel=null;hideDetail();hideReview();var done=false;
1731
1780
  function poll(){if(done)return;api('/api/scan').then(function(s){if(done)return;if(s&&s.active&&s.phase)p.textContent=scanLine(s);setTimeout(poll,600);}).catch(function(){});}
1732
1781
  setTimeout(poll,400);
1733
- api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;fillTaskRepos();layout(g.nodes,g.edges);lmBuild(g);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" &middot; <span class='err'>partial &mdash; the scan hit its time limit</span>":"");var af=$('allowfiles');if(af)af.addEventListener('click',function(){SymbiotAndroid.storage();});var ut=$('usetermux');if(ut)ut.addEventListener('click',function(){SymbiotAndroid.openTermux();});render();});}
1782
+ api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;layout(g.nodes,g.edges);lmBuild(g);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" &middot; <span class='err'>partial &mdash; the scan hit its time limit</span>":"");var af=$('allowfiles');if(af)af.addEventListener('click',function(){SymbiotAndroid.storage();});var ut=$('usetermux');if(ut)ut.addEventListener('click',function(){SymbiotAndroid.openTermux();});render();});}
1734
1783
  // The Android app calls this when you come back having allowed file access, so
1735
1784
  // the map rescans instead of staying empty.
1736
1785
  window.symbiotStorageGranted=function(){if(current==='map')loadMap();else mapLoaded=false;};
@@ -1851,25 +1900,44 @@ var CUSTWHY={pricing:'asks about pricing',install:'asks how to install it',team:
1851
1900
  function custTag(n){return n.customer?" <span class='tag cust' title='"+escQ(CUSTWHY[n.customer]||'')+"'>maybe a customer</span>":"";}
1852
1901
  // ---- Posts (post.mjs): the week's real work as drafts waiting on you: Approve, Edit, Skip. Symbiot never posts:
1853
1902
  // Approve copies the post and opens LinkedIn's share box, where you paste it and post it yourself ----
1854
- var POSTS=null,PEDIT={},PBUSY=0,PMSG='';
1903
+ var POSTS=null,PEDIT={},PBUSY=0,PMSG='',PFORM={},PSEEN='';
1855
1904
  // what the last action said: kept across a re-render (the list refreshes right after an Approve)
1856
1905
  function postsMsg(cls,html){PMSG=html?"<div class='note "+cls+"'>"+html+"</div>":'';var o=$('postsmsg');if(o){o.innerHTML=PMSG;wireShare();}}
1857
- var PSHARE='';function wireShare(){var sh=document.querySelector('#postsmsg .pshare');if(sh&&PSHARE)sh.addEventListener('click',function(){window.open(PSHARE,'_blank','noopener');});}
1858
- function loadPostsUI(){api('/api/posts').then(function(p){if(!p||!p.posts)return;POSTS=p;var f=document.activeElement;if(PBUSY||(f&&f.closest&&f.closest('#boardposts')&&f.tagName==='TEXTAREA'))return;renderPosts();});}
1906
+ var PSHARE='',PFOLDER='';function wireShare(){var sh=document.querySelector('#postsmsg .pshare');if(sh&&PSHARE)sh.addEventListener('click',function(){window.open(PSHARE,'_blank','noopener');});
1907
+ var fo=document.querySelector('#postsmsg .pfolder');if(fo&&PFOLDER)fo.addEventListener('click',function(){api('/api/posts/media/folder',{id:PFOLDER}).then(function(r){if(r&&r.error)postsMsg('err',esc(r.error));});});}
1908
+ // unchanged since the last look: left as it is, so a playing video or a half-typed page stays
1909
+ function loadPostsUI(){api('/api/posts').then(function(p){if(!p||!p.posts)return;POSTS=p;var f=document.activeElement,k=JSON.stringify(p);if(PBUSY||(f&&f.closest&&f.closest('#boardposts')&&/^(TEXTAREA|INPUT)$/.test(f.tagName)))return;if(k===PSEEN&&$('boardposts')&&$('boardposts').innerHTML)return;PSEEN=k;renderPosts();});}
1910
+ // a draft's pictures and video, each with a way off it, and the page form (From a page) when it's open
1911
+ function pmediaSrc(x,a){return '/api/posts/media?post='+encodeURIComponent(x.id)+'&m='+encodeURIComponent(a.id)+'&t='+encodeURIComponent(T);}
1912
+ function pmediaHtml(x){var m=x.media||[],h='',pf=PFORM[x.id],c=POSTS.clip||{min:3,max:30};
1913
+ if(m.length)h+="<div class='pmedia'>"+m.map(function(a){var src=pmediaSrc(x,a),what=a.from==='page'?'picture of '+(a.url||a.name):a.from==='clip'?'clip of '+(a.url||a.name):(a.name||a.kind);
1914
+ return "<div class='pthumb' data-m='"+escQ(a.id)+"' title='"+escQ(what)+"'>"+(a.kind==='video'?"<video src='"+escQ(src)+"' controls muted playsinline preload='metadata'></video>":"<img src='"+escQ(src)+"' alt='"+escQ(what)+"'>")
1915
+ +"<button class='ghost pmdel' title='take it off this draft' aria-label='Remove "+escQ(a.kind)+"'>&times;</button></div>";}).join('')+"</div>";
1916
+ if(pf)h+="<div class='row pform'><input class='purl' placeholder='a page: localhost:3000, your site, github.com/you' value='"+escQ(pf.url||'')+"' style='flex:1'>"
1917
+ +"<button class='ghost ppic' title='the page as it looks now, sharp enough for a feed'>Take a picture</button>"
1918
+ +"<select class='psecs' title='how long the clip runs' style='flex:0 0 auto;width:auto'>"+[5,8,15,30].filter(function(s){return s>=c.min&&s<=c.max;}).map(function(s){return "<option value='"+s+"'"+(s===(pf.secs||8)?" selected":"")+">"+s+" s</option>";}).join('')+"</select>"
1919
+ +"<button class='ghost pclip'"+(POSTS.canClip?" title='records the page, scrolling slowly down it, as an MP4'":" disabled title='recording a clip needs ffmpeg, which isn&#39;t installed'")+">Record a clip</button>"
1920
+ +"<button class='ghost pfclose'>Cancel</button></div>";
1921
+ return h;}
1859
1922
  // copy in the page (the click on Approve), else select it for Ctrl+C; gives whether it copied
1860
1923
  function copyPost(text,done){function fallback(){var t=document.createElement('textarea');t.value=text;t.style.position='fixed';t.style.opacity='0';document.body.appendChild(t);t.select();var ok=false;try{ok=document.execCommand('copy');}catch(e){}t.remove();done(ok);}
1861
1924
  if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(text).then(function(){done(true);},fallback);else fallback();}
1862
1925
  function renderPosts(){var p=POSTS,el=$('boardposts');if(!el||!p)return;var L=p.labels||{},h='';
1863
1926
  // it can't draft yet (no AI, or neither LinkedIn linked nor examples in voice.md): one line, not a block a new user meets first
1864
- if(!p.posts.length&&p.canDraft===false){var need=[];if(!p.connected)need.push('an AI is connected (Settings)');if(!p.linkedin&&!p.voice.count)need.push('LinkedIn is linked (below), or <code>'+esc(p.voice.file)+'</code> has a few of your posts');
1865
- el.innerHTML="<div class='muted' style='margin-top:12px;font-size:13px'>Posts: Symbiot drafts your week&#39;s LinkedIn posts here once "+need.join(', and ')+".</div>";return;}
1927
+ if(!p.posts.length&&p.canDraft===false){var need=[];if(!p.connected)need.push('an AI is connected (Settings)');if(!p.linkedin&&!p.voice.count)need.push('LinkedIn is linked, or <code>'+esc(p.voice.file)+'</code> has a few of your posts');
1928
+ el.innerHTML="<div class='pshead'><span>Drafts to post</span><small>Symbiot drafts your week&#39;s LinkedIn posts from your git, in your voice, once "+need.join(', and ')+".</small></div>"+(!p.linkedin?"<div class='row'><button class='ghost plink' title='sign in to LinkedIn once in Symbiot&#39;s browser: it reads your posts for your voice, and replies to them show here'>Link LinkedIn</button></div>":"");
1929
+ var pl=el.querySelector('.plink');if(pl)pl.addEventListener('click',function(){pl.disabled=true;api('/api/links/link',{id:'linkedin'}).then(function(r){pl.disabled=false;mkMsg(r&&!r.error?'ok':'err',r&&!r.error?"Opened LinkedIn in Symbiot&#39;s browser. Sign in as you normally do, then close that window.":esc((r&&r.error)||'failed'));});});return;}
1866
1930
  if(p.posts.length){h+="<div class='pshead'><span>Drafts to post</span><small>"+p.posts.length+" waiting on you &middot; Approve copies one and opens LinkedIn, where you post it yourself</small></div><div class='pshelf'>";
1867
1931
  h+=p.posts.map(function(x){var ed=Object.prototype.hasOwnProperty.call(PEDIT,x.id);
1868
1932
  return "<div class='rcard pcard"+(ed?" editing":"")+"' data-id='"+escQ(x.id)+"'><div class='rhead'><b>"+esc(L[x.kind]||x.kind)+"</b><span class='muted'>LinkedIn &middot; drafted "+agoTxt(x.drafted)+(x.edited?" &middot; edited":"")+"</span></div>"
1869
1933
  +(ed?"<textarea class='pedit' maxlength='3000'>"+esc(PEDIT[x.id])+"</textarea>":"<div class='ptext'>"+esc(x.text)+"</div><button type='button' class='pmore' aria-expanded='false'>Read it all</button>")
1934
+ +(x.show&&!(x.media||[]).length?"<div class='pshow'><span class='muted'>Picture idea:</span> "+esc(x.show)+"</div>":"")
1935
+ +pmediaHtml(x)
1870
1936
  +(x.sources&&x.sources.length?"<details class='psrc'><summary>From git: "+x.sources.length+" thing"+(x.sources.length===1?"":"s")+" it says</summary>"+x.sources.map(function(s){return "<div>"+esc(s)+"</div>";}).join('')+"</details>":"")
1871
1937
  +"<div class='row'>"+(ed?"<button class='act psave'>Save</button><button class='ghost pcancel'>Cancel</button><span class='pchars'>"+String(PEDIT[x.id]).length+" / 3000</span>"
1872
- :"<button class='act papprove' title='copies it to your clipboard and opens LinkedIn&#39;s share box, where you paste it and post it yourself. Symbiot never posts it, and doesn&#39;t schedule it'>Approve</button><button class='ghost pedit-btn' title='change its words here'>Edit</button><button class='ghost pskip' title='drop this draft (it stays in the log)'>Skip</button>")+"</div></div>";}).join('')+"</div>";}
1938
+ :"<button class='act papprove' title='copies it to your clipboard and opens LinkedIn&#39;s share box, where you paste it and post it yourself. Symbiot never posts it, and doesn&#39;t schedule it'>Approve</button><button class='ghost pedit-btn' title='change its words here'>Edit</button><button class='ghost pskip' title='drop this draft (it stays in the log)'>Skip</button>"
1939
+ +"<button class='ghost pmadd' title='a picture or video of yours: PNG, JPG, GIF, MP4, MOV or WebM'>Add picture or video</button>"+(PFORM[x.id]?"":"<button class='ghost pmfrom' title='a picture, or a short clip, of a web page (your app on localhost too)'>From a page</button>")
1940
+ +"<input type='file' class='pmfile hidden' accept='image/png,image/jpeg,image/gif,video/mp4,video/quicktime,video/webm'>")+"</div></div>";}).join('')+"</div>";}
1873
1941
  else h+="<div class='row' style='margin-top:10px'><span class='muted' style='flex:1'>"+(p.voice.count?"Posts: draft this week&#39;s 3 LinkedIn posts from your git, in your voice ("+p.voice.count+" example"+(p.voice.count===1?"":"s")+"). Nothing is posted until you approve one, and then you post it yourself."
1874
1942
  :"Posts: Symbiot drafts LinkedIn posts from your week&#39;s git in your own voice, once it has examples of your posts. Link LinkedIn (below) and click <b>Fill from LinkedIn</b>, or paste 5&ndash;10 of your posts into <code>"+esc(p.voice.file)+"</code>, a line of --- between each.")+"</span>"
1875
1943
  +"<button class='ghost pvoice' title='reads your recent posts on LinkedIn in Symbiot&#39;s signed-in browser into voice.md, which you can read over'>Fill from LinkedIn</button>"
@@ -1879,8 +1947,28 @@ el.querySelectorAll('.pmore').forEach(function(b){b.addEventListener('click',fun
1879
1947
  function post(btn){var id=btn.closest('.pcard').getAttribute('data-id');return p.posts.filter(function(x){return x.id===id;})[0];}
1880
1948
  el.querySelectorAll('.papprove').forEach(function(btn){btn.addEventListener('click',function(){var x=post(btn);if(!x)return;btn.disabled=true;postsMsg('','');
1881
1949
  api('/api/posts/approve',{id:x.id}).then(function(r){if(!r||r.error){btn.disabled=false;postsMsg('err',esc((r&&r.error)||'failed'));return;}
1882
- copyPost(r.post.text,function(ok){PSHARE=r.share;
1883
- postsMsg('ok',(ok?"&#10003; Approved and copied to your clipboard. ":"&#10003; Approved. Your browser didn&#39;t let Symbiot copy it: select it below and copy it (Ctrl+C). ")+"<button class='ghost pshare'>Open LinkedIn&#39;s share box</button> and paste it there (Ctrl+V), then post it yourself. Symbiot doesn&#39;t post or schedule it."+(ok?"":"<div style='user-select:all;white-space:pre-wrap;color:var(--bone);margin-top:6px'>"+esc(r.post.text)+"</div>"));loadPostsUI();});}).catch(function(e){btn.disabled=false;postsMsg('err',esc(String((e&&e.message)||e)));});});});
1950
+ copyPost(r.post.text,function(ok){PSHARE=r.share;PFOLDER=r.files&&r.files.length?r.post.id:'';
1951
+ var n=(r.post.media||[]).length,vid=(r.post.media||[]).some(function(a){return a.kind==='video';}),what=vid?'its video':n===1?'its picture':'its '+n+' pictures';
1952
+ postsMsg('ok',(ok?"&#10003; Approved and copied to your clipboard. ":"&#10003; Approved. Your browser didn&#39;t let Symbiot copy it: select it below and copy it (Ctrl+C). ")+"<button class='ghost pshare'>Open LinkedIn&#39;s share box</button> and paste it there (Ctrl+V)"
1953
+ +(n?", then add "+what+" with LinkedIn&#39;s "+(vid?"video":"photo")+" button: <button class='ghost pfolder' title='"+escQ(r.folder||'')+"'>Open "+(n===1?"its":"their")+" folder</button>. Post it yourself.":", then post it yourself.")+" Symbiot doesn&#39;t post or schedule it."+(ok?"":"<div style='user-select:all;white-space:pre-wrap;color:var(--bone);margin-top:6px'>"+esc(r.post.text)+"</div>"));loadPostsUI();});}).catch(function(e){btn.disabled=false;postsMsg('err',esc(String((e&&e.message)||e)));});});});
1954
+ // pictures and video: yours (a file), or a picture or clip of a page; each comes off with its &times;
1955
+ function pmediaDone(btn,label){return function(r){PBUSY=Math.max(0,PBUSY-1);if(btn){btn.disabled=false;if(label)btn.textContent=label;}if(!r||r.error){postsMsg('err',esc((r&&r.error)||'failed'));return;}
1956
+ if(r.media&&r.post)delete PFORM[r.post.id];var f=document.activeElement;if(f&&f.blur&&f.closest&&f.closest('#boardposts'))f.blur();postsMsg('','');loadPostsUI();};}
1957
+ function pfail(btn,label){return function(e){PBUSY=Math.max(0,PBUSY-1);if(btn){btn.disabled=false;if(label)btn.textContent=label;}postsMsg('err',esc(String((e&&e.message)||e)));};}
1958
+ el.querySelectorAll('.pmadd').forEach(function(btn){var inp=btn.parentNode.querySelector('.pmfile');btn.addEventListener('click',function(){if(inp)inp.click();});
1959
+ if(inp)inp.addEventListener('change',function(){var x=post(btn),f=inp.files&&inp.files[0];if(!x||!f)return;btn.disabled=true;btn.textContent='Adding…';PBUSY++;postsMsg('','');
1960
+ fetch('/api/posts/media/add?id='+encodeURIComponent(x.id)+'&name='+encodeURIComponent(f.name),{method:'POST',headers:{'x-symbiot-token':T,'content-type':'application/octet-stream'},body:f}).then(function(r){return r.json();})
1961
+ .then(pmediaDone(btn,'Add picture or video'),pfail(btn,'Add picture or video'));});});
1962
+ el.querySelectorAll('.pmfrom').forEach(function(btn){btn.addEventListener('click',function(){var x=post(btn);if(!x)return;PFORM[x.id]={url:p.lastUrl||'',secs:8};renderPosts();var i=el.querySelector(".pcard[data-id='"+x.id+"'] .purl");if(i){i.focus();i.select();}});});
1963
+ el.querySelectorAll('.pform').forEach(function(row){var x=post(row),i=row.querySelector('.purl'),s=row.querySelector('.psecs');if(!x)return;
1964
+ i.addEventListener('input',function(){PFORM[x.id].url=i.value;});if(s)s.addEventListener('change',function(){PFORM[x.id].secs=Number(s.value)||8;});
1965
+ i.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();row.querySelector('.ppic').click();}if(e.key==='Escape'){delete PFORM[x.id];renderPosts();}});
1966
+ row.querySelector('.pfclose').addEventListener('click',function(){delete PFORM[x.id];renderPosts();});
1967
+ function shoot(btn,clip){if(!i.value.trim()){i.focus();return;}var label=btn.textContent,secs=s?Number(s.value)||8:8;btn.disabled=true;btn.textContent=clip?'Recording '+secs+' s…':'Taking it…';PBUSY++;postsMsg('','');
1968
+ api('/api/posts/media/page',{id:x.id,url:i.value.trim(),clip:clip,seconds:secs}).then(pmediaDone(btn,label),pfail(btn,label));}
1969
+ row.querySelector('.ppic').addEventListener('click',function(){shoot(this,false);});row.querySelector('.pclip').addEventListener('click',function(){shoot(this,true);});});
1970
+ el.querySelectorAll('.pmdel').forEach(function(btn){btn.addEventListener('click',function(){var x=post(btn),m=btn.closest('.pthumb').getAttribute('data-m');if(!x)return;btn.disabled=true;PBUSY++;
1971
+ api('/api/posts/media/remove',{id:x.id,m:m}).then(pmediaDone(null,''),pfail(null,''));});});
1884
1972
  el.querySelectorAll('.pedit-btn').forEach(function(btn){btn.addEventListener('click',function(){var x=post(btn);if(!x)return;PEDIT[x.id]=x.text;renderPosts();var t=el.querySelector(".pcard[data-id='"+x.id+"'] textarea");if(t)t.focus();});});
1885
1973
  el.querySelectorAll('textarea.pedit').forEach(function(t){t.addEventListener('input',function(){var id=t.closest('.pcard').getAttribute('data-id');PEDIT[id]=t.value;var c=t.closest('.pcard').querySelector('.pchars');if(c)c.textContent=t.value.length+' / 3000';});});
1886
1974
  el.querySelectorAll('.pcancel').forEach(function(btn){btn.addEventListener('click',function(){var x=post(btn);if(x)delete PEDIT[x.id];renderPosts();});});
@@ -1891,10 +1979,33 @@ el.querySelectorAll('.pskip').forEach(function(btn){btn.addEventListener('click'
1891
1979
  api('/api/posts/skip',{id:x.id}).then(function(r){if(!r||r.error){btn.disabled=false;postsMsg('err',esc((r&&r.error)||'failed'));return;}loadPostsUI();}).catch(function(e){btn.disabled=false;postsMsg('err',esc(String((e&&e.message)||e)));});});});
1892
1980
  var dr=el.querySelector('.pdraft');if(dr)dr.addEventListener('click',function(){dr.disabled=true;postsMsg('','');dr.textContent='Drafting…';PBUSY++;
1893
1981
  api('/api/posts/draft',{}).then(function(r){PBUSY=Math.max(0,PBUSY-1);dr.disabled=false;dr.textContent="Draft this week's posts";if(!r||r.error){postsMsg('err',esc((r&&r.error)||'failed'));return;}
1894
- POSTS.posts=r.posts;renderPosts();if(r.dropped&&r.dropped.length)postsMsg('muted',"Dropped "+r.dropped.length+": "+esc(r.dropped.map(function(d){return (L[d.kind]||d.kind)+(d.cited?" (it claimed "+d.unsupported.join(', ')+", which git doesn't show)":" (it cited nothing from git)");}).join('; ')));loadPostsUI();}).catch(function(e){PBUSY=Math.max(0,PBUSY-1);dr.disabled=false;dr.textContent="Draft this week's posts";postsMsg('err',esc(String((e&&e.message)||e)));});});
1982
+ POSTS.posts=r.posts;renderPosts();var said=[];if(r.retried)said.push("The first try kept none (each claimed what git doesn't show), so I tried again.");if(r.pictures)said.push((r.pictures===1?"One draft names a screen of an app you run here: its picture's on it already.":r.pictures+" drafts name a screen of an app you run here: their pictures are on them already.")+" Keep or remove it.");if(r.dropped&&r.dropped.length)said.push("Dropped "+r.dropped.length+": "+r.dropped.map(function(d){return (L[d.kind]||d.kind)+(d.cited?" (it claimed "+d.unsupported.join(', ')+", which git doesn't show)":" (it cited nothing from git)");}).join('; '));if(said.length)postsMsg('muted',esc(said.join(' ')));loadPostsUI();}).catch(function(e){PBUSY=Math.max(0,PBUSY-1);dr.disabled=false;dr.textContent="Draft this week's posts";postsMsg('err',esc(String((e&&e.message)||e)));});});
1895
1983
  var vb=el.querySelector('.pvoice');if(vb)vb.addEventListener('click',function(){vb.disabled=true;postsMsg('','');vb.textContent='Reading LinkedIn…';
1896
1984
  api('/api/posts/voice',{confirmed:true}).then(function(r){vb.disabled=false;vb.textContent='Fill from LinkedIn';if(!r||r.error){postsMsg('err',esc((r&&r.error)||'failed'));return;}
1897
1985
  postsMsg('ok',"&#10003; Added "+r.added+" of your LinkedIn posts: "+r.total+" in <code>"+esc(r.file)+"</code>. Read them over, and delete any that don&#39;t sound like you.");loadPostsUI();}).catch(function(e){vb.disabled=false;vb.textContent='Fill from LinkedIn';postsMsg('err',esc(String((e&&e.message)||e)));});});}
1986
+ // ---- Marketing: what you post about your work (post.mjs), who answers, and the 4-week test ----
1987
+ // Off the Dashboard, which shows only what you watch. Its orb shows on Home once posts
1988
+ // can be drafted, or are (home.mjs marketing).
1989
+ var MKT=null;
1990
+ function mkMsg(cls,html){var o=$('mkmsg');if(o)o.innerHTML=html?"<div class='note "+cls+"'>"+html+"</div>":'';}
1991
+ function loadMarketing(){loadPostsUI();api('/api/marketing').then(function(d){if(d&&!d.error){MKT=d;renderMarketing();}});}
1992
+ var MKDAY=function(s){var d=new Date(s+'T00:00:00');return isNaN(d)?s:d.toLocaleDateString(undefined,{day:'numeric',month:'short'});};
1993
+ function renderMarketing(){var d=MKT,r=$('mkreplies'),t=$('mktest');if(!d||!r||!t)return;var rs=d.replies||[];
1994
+ r.innerHTML="<div class='pshead'><span>Replies</span><small>"+(!d.linkedin?"Link LinkedIn, and comments on and mentions of your posts show here, marked when they may be a customer.":rs.length?"On LinkedIn in the last 30 days. Draft a reply leaves one in the comment box for you to post.":"None on LinkedIn in the last 30 days. Comments on and mentions of your posts show here, marked when they may be a customer.")+"</small></div>"
1995
+ +rs.map(function(n){return "<div class='task' data-id='"+escQ(n.id)+"'><span class='t'>"+esc(n.text)+custTag(n)+" <span class='muted' style='font-size:11px'>"+agoTxt(n.ts)+"</span></span><button class='ghost mkdraft' title='your agent drafts a reply in your voice and leaves it in the comment box, never posted'>Draft a reply</button></div>";}).join('');
1996
+ r.querySelectorAll('.mkdraft').forEach(function(b){b.addEventListener('click',function(){var id=b.closest('.task').getAttribute('data-id');b.disabled=true;
1997
+ api('/api/watch/draft',{id:id}).then(function(x){b.disabled=false;mkMsg(x&&!x.error?'ok':'err',x&&!x.error?DRAFTING_SOCIAL:esc((x&&x.error)||'failed'));}).catch(function(e){b.disabled=false;mkMsg('err',esc(String((e&&e.message)||e)));});});});
1998
+ var T4=d.test;if(!T4){t.innerHTML='';return;}
1999
+ var pk=(T4.packages||[]).join(', '),now=T4.week,over=T4.rows.every(function(x){return x.over;});
2000
+ var cell=function(x,v,extra){return x.started?"<td class='n'>"+v+(extra?" <span class='muted'>("+extra+")</span>":"")+"</td>":"<td class='n muted'>&ndash;</td>";};
2001
+ var h="<div class='pshead'><span>The 4-week test</span><small>"+(over?"Done: "+MKDAY(T4.start)+" to "+MKDAY(T4.end)+".":now?"Week "+now+" of 4 ("+MKDAY(T4.start)+" to "+MKDAY(T4.end)+").":"Starts "+MKDAY(T4.start)+", runs to "+MKDAY(T4.end)+".")+" Does posting bring anyone? Real replies, installs or pricing questions say it does.</small></div>"
2002
+ +"<div class='mkwrap'><table class='mktab'><thead><tr><th>Week</th><th>Posts published</th><th>Replies</th><th title='LinkedIn&#39;s own count: Who viewed your profile'>Profile visits</th><th"+(pk?" title='npm downloads of "+escQ(pk)+"'":"")+">Installs</th><th>Pricing questions</th></tr></thead><tbody>"
2003
+ +T4.rows.map(function(x){return "<tr"+(x.started?"":" class='later'")+(x.started&&!x.over?" aria-current='true'":"")+"><td><b>"+x.week+"</b> <span class='muted'>"+MKDAY(x.from)+"&ndash;"+MKDAY(x.to)+"</span></td>"
2004
+ +cell(x,x.published,x.media?x.media+" with a picture":"")+cell(x,x.replies,x.customers?x.customers+" maybe a customer":"")
2005
+ +(x.started?"<td class='n muted' title='LinkedIn&#39;s own count: its analytics on each post, and Who viewed your profile'>on LinkedIn</td>":"<td class='n muted'>&ndash;</td>")
2006
+ +cell(x,x.installs==null?"&ndash;":Number(x.installs).toLocaleString())+cell(x,x.pricing)+"</tr>";}).join('')+"</tbody></table></div>"
2007
+ +"<div class='muted' style='font-size:12px;margin-top:6px'>"+(pk?"Installs: npm downloads of "+esc(pk)+". Releases, mirrors and CI download too, so trust replies and pricing questions more. ":"")+"Replies and pricing questions come from LinkedIn&#39;s notifications, which Symbiot watches once LinkedIn is linked.</div>";
2008
+ t.innerHTML=h;}
1898
2009
  // ---- Dashboard: a card per page you watch (your inbox, GitHub, WhatsApp…), what's new on each ----
1899
2010
  var BOARD=null;
1900
2011
  // a chat's line says who its last message is from (watch.mjs fromOf): only unread ones count as waiting on you
@@ -1904,7 +2015,7 @@ var BTALK={},BTALKDRAFT={},BTALKBUSY=0;
1904
2015
  var TALKHINT="<div class='muted' style='font-size:12px'>Go over what's new here with your AI: what needs you, and what to say to whom. Then click <b>Draft a reply</b> on one, and your agent writes it the way you agreed here. Your AI is sent what this card lists (for mail: the sender, subject and preview).</div>";
1905
2016
  function talking(){var f=document.activeElement;return BTALKBUSY>0||!!(f&&f.closest&&f.closest('.btalk'));}
1906
2017
  var BOARDH=24;
1907
- function loadBoard(){api('/api/watch/board?hours='+BOARDH).then(function(b){if(b&&b.cards){BOARD=b;if(!talking())renderBoard();}});loadAwaiting();loadPostsUI();}
2018
+ function loadBoard(){api('/api/watch/board?hours='+BOARDH).then(function(b){if(b&&b.cards){BOARD=b;if(!talking())renderBoard();}});loadAwaiting();}
1908
2019
  function boardMsg(cls,html){$('boardmsg').innerHTML="<div class='note "+cls+"'>"+html+"</div>";}
1909
2020
 
1910
2021
  // Replies an agent's email waits on (handback.mjs): Symbiot watches your inbox for them, and hands each on when it's in
@@ -1916,8 +2027,11 @@ var AWAITS=[];
1916
2027
  function bsWaitWord(){var n=(AWAITS||[]).filter(function(w){return w.status==='waiting';}).length;return n?' · waiting on '+n+(n===1?' reply':' replies'):'';}
1917
2028
  function bsMailLane(){var cs=(BS.b&&BS.b.cards)||[];for(var i=0;i<cs.length;i++)if(cs[i].source==='mail')return i;return -1;}
1918
2029
  function loadAwaiting(){api('/api/awaiting').then(function(d){var el=$('boardawait');if(!el||!d)return;var ws=d.waits||[];AWAITS=ws;if(BS.b)bsBuild(BS.b);
1919
- if(!ws.length||bsMailLane()>=0){el.innerHTML='';return;}var open=ws.filter(function(w){return w.status==='waiting';}).length;
1920
- el.innerHTML="<div class='tgroup' style='margin-top:14px'>Waiting on replies <span class='tcount'>"+open+"</span></div><div class='muted' style='font-size:12px;margin-bottom:6px'>Emails your agents sent that need an answer. Symbiot looks for each reply in your inbox itself, and when one is in, the agent that does the next step starts on it. You don&#39;t have to say they replied.</div>"+
2030
+ if(!ws.length){el.innerHTML='';return;}var open=ws.filter(function(w){return w.status==='waiting';}).length,s=d.sight;
2031
+ // nothing can see the reply arrive: no inbox watched, or the one watched is signed out
2032
+ var blind=s&&!s.sees?"<div class='note err'>"+((s.signedOut||[]).length?esc(s.signedOut[0])+" is signed out, so a reply would go unseen. Under Screens, type its address and click Sign in, sign in once and close the window.":"Symbiot can&#39;t see your inbox, so a reply would go unseen. Watch your inbox (map it under Screens, then Watch), or turn on Email in Settings.")+"</div>":"";
2033
+ if(bsMailLane()>=0){el.innerHTML=blind;return;}
2034
+ el.innerHTML="<div class='tgroup' style='margin-top:14px'>Waiting on replies <span class='tcount'>"+open+"</span></div><div class='muted' style='font-size:12px;margin-bottom:6px'>Emails your agents sent that need an answer. Symbiot looks for each reply in your inbox itself, and when one is in, the agent that does the next step starts on it. You don&#39;t have to say they replied.</div>"+blind+
1921
2035
  ws.map(function(w){var who=(w.to||[]).join(', ')||'?',st=w.status==='waiting'?'waiting since '+agoTxt(w.sent):(AWAITWORD[w.status]||w.status)+(w.reply?' '+agoTxt(w.reply.ts):'')+(w.handed?' &rarr; '+(w.handed==='ops'?'an agent':esc(w.handed)+'&#39;s agent')+' has the next step':'');
1922
2036
  return "<div class='task'><span class='t'><b>"+esc(who)+"</b>: "+esc(w.subject)+(w.asked?"<div class='muted' style='font-size:12px'>asked for: "+esc(w.asked)+"</div>":"")+(w.error?"<div class='note err'>"+esc(w.error)+"</div>":"")+"</span><span class='rp'"+(w.status==='waiting'?" style='color:var(--amber)'":"")+">"+st+"</span>"+(w.status==='waiting'?"<button class='ghost astop' data-id='"+esc(w.id)+"' title='stop looking for this reply' style='padding:3px 9px;font-size:12px'>Stop waiting</button>":"")+"</div>";}).join('');
1923
2037
  el.querySelectorAll('.astop').forEach(function(btn){btn.addEventListener('click',function(){btn.disabled=true;api('/api/awaiting/stop',{id:btn.getAttribute('data-id')}).then(function(r){if(!r||r.error){btn.disabled=false;boardMsg('err',esc((r&&r.error)||'failed'));return;}loadAwaiting();});});});});}
@@ -2124,7 +2238,6 @@ document.getElementById('addtrusted').addEventListener('click',addTrustedUI);
2124
2238
  document.getElementById('newtrusted').addEventListener('keydown',function(e){if(e.key==='Enter')addTrustedUI();});
2125
2239
  document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
2126
2240
  document.getElementById('agentsrefresh').addEventListener('click',loadAgents);
2127
- document.getElementById('addtask').addEventListener('click',addTaskUI);
2128
2241
  document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
2129
2242
  document.getElementById('agentcmd').addEventListener('change',saveAgent);
2130
2243
  document.getElementById('granttoolbtn').addEventListener('click',function(){grant('tool');});
@@ -2136,7 +2249,6 @@ document.getElementById('mailaddrs').addEventListener('change',saveMailAddrs);
2136
2249
  document.getElementById('addmail').addEventListener('click',addMailUI);
2137
2250
  document.getElementById('newmail').addEventListener('keydown',function(e){if(e.key==='Enter')addMailUI();});
2138
2251
  document.getElementById('mailpreview').addEventListener('click',previewMail);
2139
- document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
2140
2252
  document.getElementById('capture').addEventListener('click',captureUI);
2141
2253
  document.getElementById('screenload').addEventListener('click',pickImageUI);
2142
2254
  document.getElementById('mappage').addEventListener('click',function(){mapUI($('pagesite').value,$('mappage'));});
@@ -2229,12 +2341,12 @@ if(!ps.length){el.innerHTML='';return;}
2229
2341
  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||'';
2230
2342
  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>";
2231
2343
  if(p.summary)h+="<p class='rlsum' title='"+escQ(p.summary)+"'>"+esc(p.summary)+"</p>";
2232
- if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
2344
+ 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,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
2233
2345
  if(p.ready)h+="<button type='button' class='act rlok'>Review and approve</button>";
2234
2346
  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('');
2235
2347
  return h+"<div class='rlf'><button type='button' class='rlopen'>Its tasks &rsaquo;</button><button type='button' class='rlag'>Details in Agents &rsaquo;</button></div></section>";}).join('');
2236
2348
  el.querySelectorAll('.rly').forEach(function(box){var p=ps[+box.getAttribute('data-i')],q=(p.qs||[])[0],nm=p.name||p.repo;
2237
- var ab=box.querySelector('.rlask');if(ab&&q)wireBlob(ab,{kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path},{done:function(){LQ.relaySig='';lqLoadWork();}});
2349
+ var ab=box.querySelector('.rlask');if(ab&&q)wireBlob(ab,{kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,fix:q.fix,id:q.id},{done:function(){LQ.relaySig='';lqLoadWork();}});
2238
2350
  var ok=box.querySelector('.rlok');if(ok)ok.addEventListener('click',function(){lqOpen({kind:'proj',repo:p.repo,lit:true});});
2239
2351
  box.querySelector('.rlopen').addEventListener('click',function(){lqOpen({kind:'proj',repo:p.repo});});
2240
2352
  box.querySelector('.rlag').addEventListener('click',function(){lqPool('agents');});
@@ -2284,7 +2396,7 @@ list.forEach(function(d){d.ax=d.tx;d.ay=d.ty;});lqRelax(list,S,cx,cy,92*S.s);
2284
2396
  var old={};LQ.drops.forEach(function(d){old[d.id]=d;});
2285
2397
  LQ.drops=list.map(function(d){var o=old[d.id];d.x=o?o.x:cx;d.y=o?o.y:cy;d.vx=o?o.vx:0;d.vy=o?o.vy:0;d.cr=o?o.cr:0;return d;});
2286
2398
  var n=w.projectCount||list.length,nr=(w.running||[]).length,nw=w.waitingCount||0;
2287
- LQ.coreText=n?n+' project'+(n>1?'s':'')+(nr?' \u00b7 '+nr+' at work':'')+(nw?' \u00b7 '+nw+' tasks waiting':''):'No projects with work on them yet. Add a task, or tell me what to do.';
2399
+ LQ.coreText=n?n+' project'+(n>1?'s':'')+(nr?' \u00b7 '+nr+' at work':'')+(nw?' \u00b7 '+nw+' tasks waiting':''):'No projects with work on them yet. Tell me what to do.';
2288
2400
  var go=$('lqgo');if(go){var k=w.canGo||0;go.textContent=k?'Go \u00b7 start '+k+' task'+(k>1?'s':''):'';if(go.classList)go.classList.toggle('on',k>0);}
2289
2401
  lqLabels();}
2290
2402
  function lqGoWork(){var go=$('lqgo');if(go)go.disabled=true;api('/api/work/go',{}).then(function(r){if(go)go.disabled=false;
@@ -2300,7 +2412,7 @@ LQ.talk.push({me:false,text:t});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.r
2300
2412
  // comes out of the core (mass is conserved). Your colours come from the system,
2301
2413
  // live: light or dark, contrast, transparency, forced colours, your accent, night.
2302
2414
  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};
2303
- var LQNAMES={board:'Dashboard',map:'Map',tasks:'Tasks',agents:'Agents',week:'Week',standup:'Standup',todo:'Todo',drift:'Drift',settings:'Settings',reports:'Reports'};
2415
+ var LQNAMES={board:'Dashboard',map:'Map',tasks:'Tasks',agents:'Agents',week:'Week',standup:'Standup',todo:'Todo',drift:'Drift',settings:'Settings',reports:'Reports',marketing:'Marketing'};
2304
2416
  var LQ_REST=60000,LQ_MAX=11;
2305
2417
  function lqMM(q){try{return !!(window.matchMedia&&window.matchMedia(q).matches);}catch(e){return false;}}
2306
2418
  function lqLookGet(){var l='';try{l=window.localStorage&&window.localStorage.getItem('symbiot-look')||'';}catch(e){}return l==='glass'||l==='pearl'?l:'ferro';}
@@ -2322,7 +2434,7 @@ function lqLoad(commit){if(LQ.scene==='work'){lqLoadWork();return;}var fresh=LQ.
2322
2434
  // (work with an end) up left, Areas (what you keep up with) up right, Resources (to
2323
2435
  // look things up in) down left, the Archive (what's done) sunk low; and what's new on
2324
2436
  // what you watch (Watching) as a zone of its own on the right.
2325
- var PARA_OF={tasks:'p',agents:'p',todo:'p',board:'a',week:'a',standup:'a',map:'r',drift:'r',settings:'r',reports:'r'};
2437
+ var PARA_OF={tasks:'p',agents:'p',todo:'p',board:'a',week:'a',standup:'a',marketing:'a',map:'r',drift:'r',settings:'r',reports:'r'};
2326
2438
  var PARA_NAME={p:'Projects',a:'Areas',w:'Watching',r:'Resources',x:'Archive'},PARA_SUB={p:'work with an end',a:'what you keep up with',w:'new on what you watch',r:'to look things up in',x:'what’s done'};
2327
2439
  var PARA_ANG={p:-2.55,a:-0.6,w:0.3,r:2.6,x:1.25},PARA_LIVE={p:0.45,a:0.3,w:0.3,r:0.12,x:0};
2328
2440
  function lqPara(d){return d.kind==='you'?'n':d.kind==='archive'?'x':d.kind==='feed'&&d.shape!=='reports'?'w':PARA_OF[d.shape]||'p';}
@@ -2334,7 +2446,7 @@ var LQ_BLOBW=250,LQ_NEEDY=128;
2334
2446
  function lqNeeds(h,S){var rank={ask:0,approve:1,setup:2},rk=function(y){return y.urgent?(y.id==='setup:ai'?-3:y.id==='setup:agent'?-2:-1):rank[y.kind]||0;},ys=(h.you||[]).slice().sort(function(a,b){return rk(a)-rk(b)||(a.id<b.id?-1:a.id>b.id?1:0);});
2335
2447
  var R=Math.round(24*Math.max(0.85,Math.min(1.1,S.s))),unit=2*R+30+LQ_BLOBW,gap=28,cols=Math.max(1,Math.min(3,Math.floor((S.w-24+gap)/(unit+gap)))),pages=Math.max(1,Math.ceil(ys.length/cols));
2336
2448
  LQ.needPage=(LQ.needPage||0)%pages;var shown=ys.slice(LQ.needPage*cols,LQ.needPage*cols+cols),x0=Math.max(12,(S.w-(shown.length*unit+(shown.length-1)*gap))/2);
2337
- var tall=shown.some(function(y){return y.kind==='ask'&&(y.options||[]).length;})?172:116;
2449
+ var tall=(shown.some(function(y){return y.kind==='ask'&&(y.options||[]).length;})?172:116)+(shown.some(function(y){return y.fix==='handover';})?16:0);
2338
2450
  var items=shown.map(function(y,i){return {id:y.id,kind:'you',blob:true,item:y,shape:y.shape,title:y.title,sub:y.sub,r:R,tx:x0+i*(unit+gap)+R,ty:LQ_NEEDY+R,ax:0,ay:0};});
2339
2451
  return {items:items,bottom:items.length?LQ_NEEDY+tall+20:0,x:x0,page:LQ.needPage,pages:pages,count:ys.length};}
2340
2452
  function lqNeedPage(){LQ.needPage=(LQ.needPage||0)+1;lqAct();lqBuild();}
@@ -2367,7 +2479,9 @@ else if((d.kind==='you'||d.kind==='feed')&&at[d.shape]!=null)links.push([at[d.sh
2367
2479
  function lqBuild(){var a=LQ.adapt||{},h=LQ.home||{},S=lqSize(),list=[];LQ.bw=S.w;LQ.bh=S.h;
2368
2480
  // the band first: everything else sits under it, round a core centred in what's left
2369
2481
  var need=lqNeeds(h,S),top=need.bottom,cx=S.w/2,cy=need.items.length?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;
2370
- var feeds=(h.feeds||[]).slice(0,2),lay=(a.layout&&a.layout.items)||[],more=((a.layout&&a.layout.more)||[]).slice();
2482
+ // Marketing only once posts can be drafted, or are (home.mjs): a new user meets what they watch first
2483
+ var mk=function(id){return id!=='marketing'||!!h.marketing;};
2484
+ var feeds=(h.feeds||[]).slice(0,2),lay=((a.layout&&a.layout.items)||[]).filter(function(it){return mk(it.id);}),more=((a.layout&&a.layout.more)||[]).filter(mk);
2371
2485
  var vx=Math.max(0.5,Math.min(S.s*1.3,(S.w/2-90)/400)),vy=Math.max(0.35,Math.min(S.s*0.8,(S.h-cy-230)/400,(cy-(top?top+60:160))/400));var rs=Math.min(S.s,(vx+vy)/1.5);var room=Math.max(3,LQ_MAX-feeds.length-1-(need.items.length?2:0));lay.slice(room).forEach(function(it){more.push(it.id);});
2372
2486
  var repNew=(h.feeds||[]).filter(function(f){return f.id==='feed:reports';})[0],repOut=repNew&&feeds.indexOf(repNew)<0; // new reports, when their own droplet didn't fit
2373
2487
  lay.slice(0,room).forEach(function(it){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':'',r:it.r*rs,tx:cx+Math.cos(it.angle)*it.d*vx,ty:cy+Math.sin(it.angle)*it.d*vy});});
@@ -2395,12 +2509,14 @@ LQ.talkWeight=(a.modes&&a.modes.talkWeight)||0.35;
2395
2509
  lqLabels();}
2396
2510
  // Home in zones, on a screen wide enough for them: the core in the middle of what's
2397
2511
  // under the band; Projects then Resources to its left, Areas then Watching (with the
2398
- // Archive and "more" at its end) to its right; each a row with its name at its
2399
- // start. Fixed places in a fixed order, so nothing overlaps and each part is where
2400
- // it was last time. Sizes still come from how you use them (adapt.mjs), all scaled
2512
+ // Archive and "more" at its end) to its right; each a row with its name at its outer
2513
+ // end, so the droplets, which flow out of the core, never cross a name on their way
2514
+ // (on the right, names next to the core had the Dashboard orb sitting on "what you
2515
+ // keep up with" until it landed). Fixed places in a fixed order, so nothing overlaps
2516
+ // and each part is where it was last time. Sizes still come from how you use them (adapt.mjs), all scaled
2401
2517
  // by one factor when the screen is short, so they stay comparable. null when they
2402
2518
  // can't fit (a narrow or very short screen): the liquid's own layout then.
2403
- var ZONE_ORDER=['tasks','agents','todo','board','week','standup','map','drift','settings','reports'],ZONE_KIND={shape:0,feed:1,archive:2,more:3};
2519
+ var ZONE_ORDER=['tasks','agents','todo','board','week','standup','marketing','map','drift','settings','reports'],ZONE_KIND={shape:0,feed:1,archive:2,more:3};
2404
2520
  function lqZones(list,S,top,coreR){LQ.zoneT=null;if(S.w<1000||S.h<540)return null;
2405
2521
  var y0=top?top+10:96,y1=S.h-180,mid=(y0+y1)/2,cx=S.w/2,gapC=coreR+36,colW=S.w/2-gapC-20,titleW=150;if(colW-titleW<250||mid-y0<70)return null;
2406
2522
  var cells={p:[cx-gapC-colW,y0,mid],a:[cx+gapC,y0,mid],r:[cx-gapC-colW,mid,y1],w:[cx+gapC,mid,y1]},by={},plan={},k=1;
@@ -2414,8 +2530,8 @@ var R=0;m.forEach(function(d){R=Math.max(R,d.r);});var av=colW-titleW,fitW=funct
2414
2530
  if(fitW(per)<0.6&&m.length>1)per=Math.ceil(m.length/2);var rows=Math.ceil(m.length/per);
2415
2531
  k=Math.min(k,fitW(per),((c[2]-c[1])/rows-46)/(2*R));plan[g]={m:m,per:per,rows:rows,stepX:av/per};});
2416
2532
  if(k<0.55)return null;k=Math.min(1,k);LQ.zoneT={};
2417
- Object.keys(plan).forEach(function(g){var c=cells[g],p=plan[g],rowH=(c[2]-c[1])/p.rows,x0=c[0]+titleW+(colW-titleW)/2;
2418
- LQ.zoneT[g]=[c[0]+titleW/2,c[1]+rowH/2-13];
2533
+ Object.keys(plan).forEach(function(g){var c=cells[g],p=plan[g],rowH=(c[2]-c[1])/p.rows,right=g==='a'||g==='w',x0=c[0]+(right?0:titleW)+(colW-titleW)/2;
2534
+ LQ.zoneT[g]=[right?c[0]+colW-titleW/2:c[0]+titleW/2,c[1]+rowH/2-13];
2419
2535
  p.m.forEach(function(d,i){d.r*=k;var row=Math.floor(i/p.per),n=Math.min(p.per,p.m.length-row*p.per);d.tx=x0+(i%p.per-(n-1)/2)*p.stepX;d.ty=c[1]+row*rowH+rowH/2-13;});});
2420
2536
  return {cy:mid};}
2421
2537
  // Force-directed layout, solved once per change: push overlapping targets apart
@@ -2475,6 +2591,8 @@ if(d.kind==='more'){LQ.moreOpen=!LQ.moreOpen;lqMore(d);return;}
2475
2591
  if(d.kind==='archive'){lqArchive();return;}
2476
2592
  if(d.kind==='you'&&d.item){if(d.x!=null){var S1=lqSize();LQ.ripple=[d.x/S1.w,d.y/S1.h,LQ.t];}lqUse(d.shape||'agents',ev);lqFocus(d.item);return;}
2477
2593
  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;}
2594
+ // where your files disagree: the checks, lit up in Settings, and these clashes reached you (it dissolves)
2595
+ 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;}
2478
2596
  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');}
2479
2597
  function lqMore(d){var el=$('lqmore');if(!el)return;if(!LQ.moreOpen||!d){el.innerHTML='';return;}
2480
2598
  if(d.lanes){el.innerHTML=d.lanes.map(function(p,i){return "<button type='button' class='lqmb lqm' data-i='"+i+"'>"+(p.lit?"<span style='color:#F2A541'>&#9679;</span> ":"")+esc(p.name)+"</button>";}).join('');
@@ -2489,8 +2607,9 @@ function lqGo(shape){if(shape==='tasks'||shape==='agents'){lqWorkBy('task');retu
2489
2607
  function blobOpt(o){return String(o||'').replace(/[ ]*[(]recommended[)][ ]*$/i,'');}
2490
2608
  function blobBody(y,home){var h='',nm=y.name||y.repo||(y.kind==='setup'?y.title:'')||'';
2491
2609
  h+=home?"<button type='button' class='lt' title='open "+escQ(nm)+", with this lit up'><span>"+esc(lqShort(nm,30))+"</span><i>open &rsaquo;</i></button>":"<div class='nhead'><b>"+esc(nm)+(y.kind==='ask'?" asks":"")+"</b>"+(y.more?"<span class='muted'>+"+y.more+" more on its block in Agents</span>":"")+"</div>";
2492
- 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'?'Show me your inbox':y.id==='setup:ai'?'Connect an AI':y.urgent?'Reconnect':'Open Settings')+"</span></button></div>";
2493
- var os=y.options||[];h+="<div class='bq' title='"+escQ(y.q||'')+"'>"+esc(y.q||y.sub||'')+"</div>";
2610
+ 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>";
2611
+ var os=y.options||[];h+="<div class='bq' title='"+escQ((y.q||'')+(y.why?' ('+y.why+')':''))+"'>"+esc(y.q||y.sub||'')+"</div>";
2612
+ if(y.fix==='handover'&&y.sub)h+="<div class='bfor' title='"+escQ(y.sub)+"'>for: "+esc(y.sub)+"</div>";
2494
2613
  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'>or answer in your own words</button>";
2495
2614
  h+="<form class='bfx"+(os.length?' hidden':'')+"'><input placeholder='your answer' aria-label='your answer to "+escQ(nm)+"'><button type='submit'>Send</button></form>";
2496
2615
  if(home&&y.more)h+="<span class='bmore'>+"+y.more+" more question"+(y.more>1?'s':'')+" after this one</span>";
@@ -2499,22 +2618,26 @@ return h;}
2499
2618
  // continue): saved to its ANSWERS.md, and its agent picks it up. Then it dissolves.
2500
2619
  function wireBlob(box,y,o){o=o||{};var say=o.say||lqSaid,nm=y.name||y.repo||'the agent';
2501
2620
  var lock=function(on){box.querySelectorAll('button,input').forEach(function(b){b.disabled=on;});};
2502
- var send=function(ans){ans=String(ans||'').trim();if(!ans||!y.path)return;lock(true);
2503
- api('/api/agents/answer',{path:y.path,answers:[{q:y.q,a:ans}],rerun:true}).then(function(r){
2621
+ var send=function(ans,j){ans=String(ans||'').trim();if(!ans||(!y.path&&!y.fix))return;lock(true);
2622
+ (y.fix?api('/api/home/answer',j!=null?{id:y.id,pick:j}:{id:y.id,text:ans}):api('/api/agents/answer',{path:y.path,answers:[{q:y.q,a:ans}],rerun:true})).then(function(r){
2504
2623
  if(!r||r.error){lock(false);say(nm+': '+((r&&r.error)||'the answer didn’t save. Try again on its block in Agents.'),true);return;}
2505
- say('Sent to '+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.')));
2624
+ say(y.fix?nm+': '+(r.said||'Done.'):'Sent to '+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.')));
2506
2625
  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);});};
2507
- box.querySelectorAll('.nopt[data-j]').forEach(function(b){b.addEventListener('click',function(){send((y.options||[])[+b.getAttribute('data-j')]);});});
2626
+ box.querySelectorAll('.nopt[data-j]').forEach(function(b){b.addEventListener('click',function(){var j=+b.getAttribute('data-j');send((y.options||[])[j],j);});});
2508
2627
  var fr=box.querySelector('.bfree'),fx=box.querySelector('.bfx');
2509
2628
  if(fr&&fx)fr.addEventListener('click',function(){var shut=fx.classList.toggle('hidden');fr.setAttribute('aria-expanded',shut?'false':'true');var i=fx.querySelector('input');if(!shut&&i&&i.focus)i.focus();});
2510
2629
  if(fx)fx.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();var i=fx.querySelector('input');send(i&&i.value);});
2511
- var go=box.querySelector('.ngo');if(go)go.addEventListener('click',function(){lqFocus(y);});}
2630
+ var go=box.querySelector('.ngo');if(go)go.addEventListener('click',function(){if(!y.pick){lqFocus(y);return;}
2631
+ // Pick your agent: the one Symbiot found, in one click; Settings has the others
2632
+ lock(true);api('/api/agentcmd',{cmd:y.pick.tmpl}).then(function(r){if(!r||r.error){lock(false);say((r&&r.error)||'It didn’t save. Pick it in Settings.',true);return;}
2633
+ say('Your agent is '+y.pick.name+' now: Send to repos and Go hand your tasks to it.');if(box.classList)box.classList.add('gone');LQ.fresh=true;setTimeout(function(){if(o.done)o.done();else lqLoad(false);},650);}).catch(function(e){lock(false);say(String((e&&e.message)||e),true);});});}
2512
2634
  function lqSaid(t){LQ.talk.push({me:false,text:t});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);}
2513
2635
  // A lit orb opens its project with what lit it lit up and in view (lqLight): a
2514
2636
  // repo's questions and its Approve, under "Needs you here" at the top of its
2515
2637
  // tasks, with its held handovers; an ops run's question on its block in Agents;
2516
2638
  // a first step on its part of Settings.
2517
2639
  function lqFocus(y){lqAct();y=y||{};LQ.focus={kind:y.kind||'',repo:y.repo||'',path:y.path||'',focus:y.focus||'',shown:false};
2640
+ document.querySelectorAll('main .lit').forEach(function(x){x.classList.remove('lit');}); // only what this orb lit stays lit
2518
2641
  if(y.kind==='setup'){lqPool('settings');setTimeout(lqLight,300);return;}
2519
2642
  if(!y.repo||y.repo==='ops'){lqPool('agents');return;}
2520
2643
  TFILTER.repo=y.repo;TFILTER.type='';lqPool('tasks');
@@ -2534,9 +2657,9 @@ lqLight();};
2534
2657
  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([]);});}
2535
2658
  // What lit the orb, lit up where it opened (.lit): the first in view, and with only
2536
2659
  // one, its first answer focused. Again after each re-render, but scrolled to once.
2537
- var SETFOCUS={ai:'provider',work:'scanroots',agent:'agentcmd',links:'links',company:'knowroots'};
2660
+ var SETFOCUS={ai:'provider',work:'scanroots',agent:'agentcmd',links:'links',company:'knowroots',checks:'knowchecks'};
2538
2661
  function lqLight(){var f=LQ.focus;if(!f||LQ.mode!=='pool')return;var things=[],also=[];
2539
- if(f.kind==='setup'){var a=$(SETFOCUS[f.focus]||''),s=a&&a.closest?a.closest('.sset'):null;if(s)things.push(s);}
2662
+ 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);}
2540
2663
  else if(current==='tasks'){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);});}
2541
2664
  else if(current==='agents'){document.querySelectorAll('#agentslist .agent[data-i]').forEach(function(e){var g=AGENTLIST[+e.getAttribute('data-i')];if(g&&(nQs(g)||g.waiting)&&(f.path?g.path===f.path:/act-[0-9a-f]+$/.test(String(g.path))))things.push(e);});}
2542
2665
  things.concat(also).forEach(function(e){if(e.classList)e.classList.add('lit');});
@@ -2588,10 +2711,13 @@ if(still){d.x=tx;d.y=ty;d.vx=0;d.vy=0;}else{d.vx+=ax;d.vy+=ay;d.x+=d.vx;d.y+=d.v
2588
2711
  if(!rest&&!pool){d.x=Math.max(d.cr+12,Math.min(S.w-d.cr-12,d.x));d.y=Math.max((d.blob?60:140)+d.cr,Math.min(S.h-150-d.cr-50,d.y));}
2589
2712
  d.cr+=(tr-d.cr)*(still?1:0.06);
2590
2713
  var b=LQ.btns[i];if(b&&b.offsetWidth)d.lw=b.offsetWidth;if(b&&b.style){b.style.transform=d.blob?'translate('+Math.round(d.x+d.r+30)+'px,'+Math.round(d.y-d.r-4)+'px)':'translate('+Math.round(d.x-(b.offsetWidth||0)/2)+'px,'+Math.round(d.y+d.cr+(d.kind==='run'?28:4))+'px)';var show=!rest&&!pool;b.style.opacity=show?'1':'0';b.style.pointerEvents=show?'auto':'none';var lt=LQ.lts&&LQ.lts[i];if(lt)lt.tabIndex=show?0:-1;}}
2591
- (LQ.gbtns||[]).forEach(function(gb){var g=gb.getAttribute('data-g'),top=1e9,sx=0,nn=0;D.forEach(function(d){if(d.kind!=='more'&&lqPara(d)===g){top=Math.min(top,d.y-d.cr);sx+=d.x;nn++;}});if(!nn||!gb.style)return;var gw=gb.offsetWidth||0,gh=gb.offsetHeight||0,zt=LQ.scene!=='work'&&LQ.zoneT&&LQ.zoneT[g];if(zt)gb.style.transform='translate('+Math.round(zt[0]-gw/2)+'px,'+Math.round(zt[1]-gh/2)+'px)';else gb.style.transform='translate('+Math.round(Math.max(12,Math.min(lqSize().w-gw-12,sx/nn-gw/2)))+'px,'+Math.round(Math.max(LQ.scene!=='work'&&LQ.top?LQ.top:48,top-gh-30))+'px)';var gshow=!rest&&!pool&&!talk;gb.style.opacity=gshow?'1':'0';gb.style.pointerEvents=gshow?'auto':'none';gb.tabIndex=gshow?0:-1;});
2714
+ (LQ.gbtns||[]).forEach(function(gb){var g=gb.getAttribute('data-g'),top=1e9,sx=0,nn=0;D.forEach(function(d){if(d.kind!=='more'&&lqPara(d)===g){top=Math.min(top,d.y-d.cr);sx+=d.x;nn++;}});if(!nn||!gb.style)return;var gw=gb.offsetWidth||0,gh=gb.offsetHeight||0,zt=LQ.scene!=='work'&&LQ.zoneT&&LQ.zoneT[g];if(zt)gb.style.transform='translate('+Math.round(zt[0]-gw/2)+'px,'+Math.round(zt[1]-gh/2)+'px)';else{var gx=Math.max(12,Math.min(lqSize().w-gw-12,sx/nn-gw/2)),gy=top-gh-30;
2715
+ // off any droplet it would sit on (the "more" droplet, another group's): above it instead
2716
+ for(var pass=0;pass<3;pass++)D.forEach(function(d){if(d.blob||lqPara(d)===g&&d.kind!=='more')return;var l=d.x-d.cr-6,r=d.x+d.cr+6,t=d.y-d.cr-6,b=d.y+d.cr+(d.lw?24:6);if(gx<r&&gx+gw>l&&gy<b&&gy+gh>t)gy=t-gh-4;});
2717
+ gb.style.transform='translate('+Math.round(gx)+'px,'+Math.round(Math.max(LQ.scene!=='work'&&LQ.top?LQ.top:48,gy))+'px)';}var gshow=!rest&&!pool&&!talk;gb.style.opacity=gshow?'1':'0';gb.style.pointerEvents=gshow?'auto':'none';gb.tabIndex=gshow?0:-1;});
2592
2718
  var ntl=$('lqneedt');if(ntl&&ntl.style)ntl.style.opacity=rest?'0':'1';
2593
2719
  (LQ.orbs||[]).forEach(function(o){var d=D[+o.getAttribute('data-i')];if(!d||!o.style)return;var R=Math.round(d.cr+20);o.style.width=o.style.height=(2*R)+'px';o.style.transform='translate('+Math.round(d.x-R)+'px,'+Math.round(d.y-R)+'px)';o.style.opacity=rest||pool?'0':'1';});
2594
- var cb=$('lqcore');if(cb&&cb.style){var inC=LQ.theme&&LQ.theme.look!=='pearl'&&core.cr>60;cb.style.maxWidth=inC?Math.round(core.cr*1.5)+'px':'340px';var cw=cb.offsetWidth||0,chh=cb.offsetHeight||0;cb.style.transform='translate('+Math.round(cx-cw/2)+'px,'+(rest||inC?(inC?Math.round(core.y-chh/2):Math.round(core.y+core.cr+14)):56)+'px)';var ct=rest?(LQ.coreText||'All handled'):talk||pool?'':(LQ.coreText||'');lqCoreText(cb,ct);cb.style.opacity=ct?'1':'0';cb.style.pointerEvents=ct?'auto':'none';}}
2720
+ var cb=$('lqcore');if(cb&&cb.style){var inC=LQ.theme&&LQ.theme.look!=='pearl'&&core.cr>60;cb.style.maxWidth=inC?Math.round(core.cr*1.5)+'px':'340px';var cw=cb.offsetWidth||0,chh=cb.offsetHeight||0;cb.style.transform='translate('+Math.round(cx-cw/2)+'px,'+(rest||inC?(inC?Math.round(core.y-chh/2):Math.round(core.y+core.cr+14)):LQ.scene==='work'?112:56)+'px)';var ct=rest?(LQ.coreText||'All handled'):talk||pool?'':(LQ.coreText||'');lqCoreText(cb,ct);cb.style.opacity=ct?'1':'0';cb.style.pointerEvents=ct?'auto':'none';}}
2595
2721
  // The liquid: a metaball surface (Σ r²/d² = 1) shaded as chrome, on the GPU.
2596
2722
  var LQ_FS=['precision highp float;',
2597
2723
  'uniform vec2 uRes;uniform float uT;uniform float uDpr;uniform vec4 uB[NB];uniform vec3 uRip;uniform float uStyle;uniform float uContrast;uniform float uExposure;',
@@ -2696,5 +2822,5 @@ var G=lqGL();if(G){try{lqStep();lqDraw(G);}catch(e){}}if(typeof window.requestAn
2696
2822
  function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0)lqStep();var every=(GLSOFT||GLSTILL)?glEvery(LQ.mode==='aware'):LQ.mode==='pool'?6:LQ.mode==='rest'?2:1;if(G&&LQ.frame%every===0)lqDraw(G);}raf(frame);} // moves every frame; drawn less at rest, behind a panel, without a GPU
2697
2823
  raf(frame);}
2698
2824
 
2699
- initGraphEvents();syncP();refresh();loadMap();firstTab();lqInit();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();
2825
+ initGraphEvents();syncP();refresh();loadMap();firstTab();lqInit();lqDeep();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();
2700
2826
  </script></body></html>`;