symbiot 0.49.1 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui.mjs CHANGED
@@ -25,6 +25,9 @@ body.lq-forced #lq{display:none}
25
25
  .lqd{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;border-radius:14px;border:1px solid transparent;background:transparent;color:#ECE9E4;font:600 13.5px/1.25 var(--sans);letter-spacing:-.005em;pointer-events:auto;transition:opacity .5s ease,background-color .2s ease,border-color .2s ease;will-change:transform;text-shadow:0 1px 10px rgba(0,0,0,.6)}
26
26
  .lqd .lt .lm{display:block;font-weight:500;font-size:11.5px;color:#8A919B;margin-top:1px}
27
27
  .lqd .lt{all:unset;box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:7px;min-height:36px;padding:6px 10px;cursor:pointer;text-align:center;max-width:220px}
28
+ .lqd.lq-proj .lt{flex-direction:column;gap:1px;max-width:260px}
29
+ .lqd.lq-proj .lt .lm{font-weight:500;font-size:12px;color:#8A919B;max-width:250px}
30
+ body.lq-light .lqd.lq-proj .lt .lm{color:#5A6470}
28
31
  .lqd.lq-you .lt::before{content:'';flex:none;width:7px;height:7px;border-radius:50%;background:#F2A541;box-shadow:0 0 0 3px rgba(242,165,65,.2)}
29
32
  .lqd .lt span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
30
33
  .lqd .lgo{display:none;margin:4px 10px 10px;min-height:38px;padding:0 16px;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;font:700 12.5px var(--sans);cursor:pointer}
@@ -97,8 +100,11 @@ body.lq-liquid.lq-light.lq-pooled main{background:rgba(246,248,251,.9);box-shado
97
100
  body.lq-liquid.lq-solid.lq-pooled main{backdrop-filter:none;background:var(--ink)}
98
101
  body.lq-liquid .task,body.lq-liquid .agent,body.lq-liquid .rcard,body.lq-liquid .bcard,body.lq-liquid .drift,body.lq-liquid .q,body.lq-liquid .out,body.lq-liquid .tchat{background:linear-gradient(180deg,rgba(255,255,255,.075),rgba(255,255,255,.02));border:1px solid rgba(220,228,240,.16);border-radius:16px;box-shadow:inset 0 1px 0 rgba(255,255,255,.09),0 12px 30px -24px rgba(0,0,0,.9)}
99
102
  body.lq-liquid.lq-light .task,body.lq-liquid.lq-light .agent,body.lq-liquid.lq-light .rcard,body.lq-liquid.lq-light .bcard,body.lq-liquid.lq-light .drift,body.lq-liquid.lq-light .q,body.lq-liquid.lq-light .out,body.lq-liquid.lq-light .tchat{background:linear-gradient(180deg,#FFFFFF,#F2F4F8);border-color:rgba(20,30,45,.12);box-shadow:inset 0 1px 0 #FFFFFF,0 12px 28px -24px rgba(20,30,45,.5)}
100
- body.lq-liquid button.act{background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;border:1px solid rgba(255,255,255,.9);border-radius:999px;box-shadow:0 6px 18px -10px rgba(0,0,0,.7)}
101
- body.lq-liquid button.ghost{background:rgba(255,255,255,.05);border:1px solid rgba(220,228,240,.28);border-radius:999px;color:var(--bone);backdrop-filter:blur(6px)}
103
+ body.lq-liquid button.act{background:#F3F0EA;color:#0A0B0D;border:0;border-radius:999px;box-shadow:none;font-weight:600}
104
+ body.lq-liquid button.act:hover{background:#FFFFFF}
105
+ body.lq-liquid.lq-light button.act{background:#151A21;color:#FFFFFF}
106
+ body.lq-liquid.lq-look-pearl button.act,body.lq-look-pearl #lqgo.on,body.lq-liquid.lq-look-pearl .updatebar.show{background:linear-gradient(180deg,#FFFFFF 0%,#D9DDE2 45%,#9AA1AA 55%,#E8EAED 100%);color:#1B1E23;box-shadow:0 2px 6px rgba(0,0,0,.18),inset 0 0 0 1px rgba(0,0,0,.08)}
107
+ 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)}
102
108
  body.lq-liquid.lq-light button.ghost{background:rgba(255,255,255,.7);border-color:rgba(20,30,45,.18)}
103
109
  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)}
104
110
  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)}
@@ -106,7 +112,8 @@ body.lq-liquid input:focus,body.lq-liquid select:focus,body.lq-liquid textarea:f
106
112
  body.lq-liquid .tgroup{color:#C9D1DC;letter-spacing:.16em}
107
113
  body.lq-liquid.lq-light .tgroup{color:#3B4656}
108
114
  body.lq-liquid h2,body.lq-liquid h3{letter-spacing:-.01em}
109
- body.lq-liquid .fchip.on{background:linear-gradient(180deg,#FFFFFF,#C9D1DC);color:#06080B;border-color:#FFFFFF}
115
+ body.lq-liquid .fchip.on{background:#F3F0EA;color:#0A0B0D;border-color:#F3F0EA}
116
+ body.lq-liquid.lq-light .fchip.on{background:#151A21;color:#FFFFFF;border-color:#151A21}
110
117
  body.lq-contrast.lq-liquid .task,body.lq-contrast.lq-liquid .agent{border:2px solid currentColor}
111
118
 
112
119
  /* the work scene: tasks and agents in the liquid, with spheres revolving round what's at work */
@@ -128,7 +135,8 @@ body.lq-pooled #lqgroups{display:none}
128
135
  @keyframes lqorbit{to{transform:rotate(360deg)}}
129
136
  @media (prefers-reduced-motion: reduce){.lqorbit span{animation:none}.lqorbit span:nth-child(2){transform:rotate(120deg)}.lqorbit span:nth-child(3){transform:rotate(240deg)}}
130
137
  #lqgo,#lqback{display:none}
131
- body.lq-work #lqgo.on{display:inline-flex;position:absolute;left:50%;bottom:86px;transform:translateX(-50%);z-index:4;align-items:center;gap:8px;min-height:52px;padding:0 26px;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;font:700 16px var(--sans);letter-spacing:.02em;cursor:pointer;box-shadow:0 16px 40px -16px rgba(0,0,0,.75)}
138
+ body.lq-work #lqgo.on{display:inline-flex;position:absolute;left:50%;bottom:86px;transform:translateX(-50%);z-index:4;align-items:center;gap:8px;min-height:52px;padding:0 26px;border-radius:999px;border:0;background:#F3F0EA;color:#06080B;font:700 16px var(--sans);letter-spacing:.02em;cursor:pointer;box-shadow:0 16px 40px -16px rgba(0,0,0,.75)}
139
+ body.lq-light.lq-work #lqgo.on{background:#151A21;color:#FFFFFF}
132
140
  body.lq-work #lqback{display:inline-flex;position:absolute;left:16px;top:52px;z-index:4;align-items:center;min-height:44px;padding:0 16px;border-radius:999px;border:1px solid rgba(220,228,240,.3);background:rgba(4,5,7,.6);backdrop-filter:blur(8px);color:#EEF2F8;font:600 14px var(--sans);cursor:pointer}
133
141
  body.lq-light.lq-work #lqback{background:rgba(255,255,255,.8);color:#10151C;border-color:rgba(20,30,45,.2)}
134
142
  body.lq-pooled #lqgo,body.lq-pooled #lqback,body.lq-pooled #lqorbits,body.lq-work #lqgroups{display:none}
@@ -205,7 +213,8 @@ a{color:var(--green);cursor:pointer}.hidden{display:none}
205
213
  .note{font-size:12px;margin-top:10px}.ok{color:var(--green)}.err{color:var(--amber)}
206
214
  .updatebar{display:none;align-items:center;gap:12px;padding:9px 16px;font-size:13px;font-weight:600;background:var(--green);color:var(--ink)}
207
215
  .updatebar.show{display:flex}
208
- body.lq-liquid .updatebar.show{position:fixed;top:10px;left:50%;transform:translateX(-50%);width:max-content;max-width:calc(100vw - 24px);z-index:20;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;box-shadow:0 14px 40px -14px rgba(0,0,0,.7)}
216
+ body.lq-liquid .updatebar.show{position:fixed;top:10px;left:50%;transform:translateX(-50%);width:max-content;max-width:calc(100vw - 24px);z-index:20;border-radius:999px;border:0;background:#F3F0EA;color:#06080B;box-shadow:0 14px 40px -14px rgba(0,0,0,.7)}
217
+ body.lq-liquid.lq-light .updatebar.show{background:#151A21;color:#FFFFFF}
209
218
  body.lq-liquid .updatebar.show.reconnect{background:var(--amber)}
210
219
  body.lq-liquid #appbar.updatebar.show{top:58px}
211
220
  .updatebar.reconnect{background:var(--amber)}
@@ -318,6 +327,231 @@ label.check input{width:auto}
318
327
  .scrbox span{position:absolute;left:0;top:0;font-size:11px;background:var(--green);color:var(--ink);padding:1px 6px;white-space:nowrap}
319
328
  .scrbox.draw{border:2px dashed var(--amber);background:rgba(242,165,65,.14)}
320
329
  .board{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-top:12px}
330
+
331
+ /* ---- finer details: the controls every panel shares, in the look ---- */
332
+ .iconbtn{display:inline-grid;place-items:center;width:36px;height:36px;padding:0;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);color:var(--bone);cursor:pointer;transition:background-color .15s ease}
333
+ .iconbtn:hover{background:rgba(255,255,255,.1)}
334
+ .iconbtn.wide{display:inline-flex;gap:7px;width:auto;padding:0 14px;font:500 13px var(--sans)}
335
+ .iconbtn:focus-visible,.seg button:focus-visible{outline:2px solid currentColor;outline-offset:2px}
336
+ body.lq-light .iconbtn{border-color:rgba(21,26,33,.12);background:rgba(21,26,33,.04)}
337
+ body.lq-light .iconbtn:hover{background:rgba(21,26,33,.08)}
338
+ .seg{display:inline-flex;gap:2px;padding:3px;border-radius:999px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1)}
339
+ .seg button{min-height:30px;padding:0 13px;border:0;border-radius:999px;background:transparent;color:var(--faint);font:500 12.5px var(--sans);cursor:pointer}
340
+ .seg button[aria-checked='true']{background:#F3F0EA;color:#0A0B0D}
341
+ body.lq-light .seg{background:rgba(21,26,33,.04);border-color:rgba(21,26,33,.1)}
342
+ body.lq-light .seg button[aria-checked='true']{background:#151A21;color:#FFFFFF}
343
+ body.lq-liquid select{-webkit-appearance:none;appearance:none;padding:8px 34px 8px 12px;border-radius:12px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238A919B' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 13px center;color:var(--bone);font:500 13px var(--sans);color-scheme:dark;cursor:pointer}
344
+ body.lq-liquid select option{background:#131518;color:#ECE9E4}
345
+ body.lq-liquid.lq-light select{border-color:rgba(21,26,33,.14);background-color:rgba(21,26,33,.04);color-scheme:light}
346
+ body.lq-liquid.lq-light select option{background:#FFFFFF;color:#151A21}
347
+ /* the Dashboard's head */
348
+ .bhead{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin:2px 0 4px}
349
+ .bsumw{flex:1;min-width:220px}
350
+ .bsum{font:500 16px/1.4 var(--sans);color:var(--bone);text-wrap:balance}
351
+ .bsum b{font-weight:600}
352
+ .bsub{font-size:12.5px;color:var(--faint);margin-top:2px}
353
+ .blane .blsub{font-size:11.5px;color:var(--faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:1px}
354
+ /* Connections */
355
+ .cxh{display:flex;flex-direction:column;gap:2px;margin:26px 0 12px}
356
+ .cxh span{font:600 15px var(--sans);color:var(--bone)}
357
+ .cxh small{font-size:12.5px;color:var(--faint)}
358
+ .cxg{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:18px 28px}
359
+ .cxc{min-width:0}
360
+ .cxl{font:500 11px var(--sans);letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin-bottom:4px}
361
+ .lnk.cx{display:flex;align-items:center;gap:0}
362
+ .cx .lbtn{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:9px;flex:1;min-width:0;min-height:32px;padding:4px 8px;margin-left:-8px;border-radius:10px;cursor:pointer}
363
+ .cx .lbtn:hover{background:rgba(255,255,255,.05)}
364
+ .cx .lbtn:focus-visible{outline:2px solid currentColor;outline-offset:1px}
365
+ body.lq-light .cx .lbtn:hover{background:rgba(21,26,33,.05)}
366
+ .cxb{flex:none;width:11px;height:11px;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--faint);opacity:.6}
367
+ .cx.ok .cxb{opacity:1;box-shadow:0 0 0 1px rgba(255,255,255,.08);background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#C9CFD9 20%,#5B626D 58%,#121418 100%)}
368
+ 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%)}
369
+ .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)}
370
+ .cxn{font:500 13.5px var(--sans);color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
371
+ .cx.ok .cxn{color:var(--bone)}
372
+ .cxs{margin-left:auto;padding-left:8px;font-size:11.5px;color:var(--faint);white-space:nowrap}
373
+ .cx.signin .cxs,.cx.signedout .cxs,.cx.error .cxs{color:var(--amber)}
374
+ .cx.off .cxs{opacity:0;transition:opacity .15s ease}
375
+ .cx.off:hover .cxs,.cx.off .lbtn:focus-visible .cxs,body.lq-touch .cx.off .cxs{opacity:1}
376
+ .cx .lmore,.cx .lrm{display:none;border:0;background:transparent;color:var(--faint);font:500 12px var(--sans);padding:4px 6px;border-radius:8px;cursor:pointer}
377
+ .cx .lmore:hover{color:var(--bone)}.cx .lrm:hover{color:var(--amber)}
378
+ .cx:hover .lmore,.cx:hover .lrm,.cx:focus-within .lmore,.cx:focus-within .lrm,body.lq-touch .cx .lmore,body.lq-touch .cx .lrm{display:inline-flex}
379
+ /* the card list, quiet */
380
+ body.lq-liquid .board .bcard{background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.08);border-radius:16px;box-shadow:none}
381
+ body.lq-liquid.lq-light .board .bcard{background:rgba(255,255,255,.6);border-color:rgba(21,26,33,.08)}
382
+ body.lq-liquid .board .bcard.has{border-color:rgba(242,165,65,.35)}
383
+ body.lq-liquid .bcard .bc{font:300 30px/1.1 var(--sans);letter-spacing:-.02em;color:var(--faint)}
384
+ body.lq-liquid .bcard.has .bc{color:var(--amber)}
385
+ body.lq-liquid .bcard .bbrief{border-left:2px solid var(--line);background:transparent;padding:2px 0 2px 12px;color:var(--text)}
386
+
387
+
388
+ /* the Map, in the liquid */
389
+ #lmap,#lmtools{display:none}
390
+ body.lq-liquid #graph,body.lq-liquid #mapbar,body.lq-liquid .legend .lg,body.lq-liquid .legend > .muted{display:none}
391
+ body.lq-liquid #lmap{display:block;position:relative;flex:1;min-width:0;height:64vh;min-height:420px;border-radius:18px;overflow:hidden;isolation:isolate;background:#08090B;box-shadow:0 0 0 1px rgba(255,255,255,.08);touch-action:none;cursor:grab}
392
+ body.lq-liquid #lmap:active{cursor:grabbing}
393
+ body.lq-liquid.lq-light #lmap{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
394
+ body.lq-look-pearl #lmap{background:#EDECE9}
395
+ #lmc{position:absolute;inset:0;width:100%;height:100%;display:block}
396
+ #lmlines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
397
+ #lmlines line{stroke:rgba(255,255,255,.12);stroke-width:1;stroke-dasharray:2 4}
398
+ #lmlines line.hot{stroke:rgba(255,255,255,.4)}
399
+ body.lq-light #lmlines line{stroke:rgba(21,26,33,.14)}body.lq-light #lmlines line.hot{stroke:rgba(21,26,33,.45)}
400
+ #lmov{position:absolute;inset:0;pointer-events:none}
401
+ .lml{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;gap:0;padding:2px 6px;border:0;border-radius:8px;background:transparent;color:#ECE9E4;font:inherit;text-align:center;white-space:nowrap;pointer-events:auto;cursor:pointer;transition:opacity .25s ease;will-change:transform;text-shadow:0 1px 10px rgba(0,0,0,.7)}
402
+ .lml b{font:600 13px/1.2 var(--sans)}
403
+ .lml span{font:400 11.5px/1.3 var(--sans);color:#8A919B}
404
+ .lml.dormant{opacity:.55}.lml.far{opacity:.3}.lml.near b{color:#F2A541}
405
+ .lmme{position:absolute;left:0;top:0;display:flex;align-items:center;gap:7px;pointer-events:none;will-change:transform;font:500 11px var(--sans);letter-spacing:.08em;text-transform:uppercase;color:#ECE9E4}
406
+ .lmme i{width:14px;height:14px;border-radius:50%;background:#F3F0EA;box-shadow:0 0 0 4px rgba(243,240,234,.18),0 0 18px rgba(243,240,234,.45)}
407
+ body.lq-light .lmme{color:#151A21}body.lq-light .lmme i{background:#151A21;box-shadow:0 0 0 4px rgba(21,26,33,.15)}
408
+ .lml:focus-visible{outline:2px solid currentColor;outline-offset:2px}
409
+ body.lq-light .lml{color:#151A21;text-shadow:0 1px 8px rgba(255,255,255,.8)}body.lq-light .lml span{color:#5A6470}body.lq-light .lml.near b{color:#A85F0A}
410
+ .lmc{position:absolute;left:0;top:0;font:500 11px var(--sans);letter-spacing:.12em;text-transform:uppercase;color:#8A919B;white-space:nowrap;pointer-events:none;will-change:transform}
411
+ body.lq-light .lmc{color:#5A6470}
412
+ .lmring{position:absolute;left:0;top:0;border-radius:50%;box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.35);pointer-events:none}
413
+ body.lq-light .lmring{box-shadow:inset 0 0 0 1.5px rgba(21,26,33,.35)}
414
+ .lmtip{position:absolute;left:0;top:0;width:300px;max-width:calc(100% - 20px);padding:12px 14px;border-radius:16px;background:rgba(16,18,21,.94);border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 50px -20px rgba(0,0,0,.8);color:#ECE9E4;opacity:0;pointer-events:none;transition:opacity .15s ease;z-index:3}
415
+ .lmtip.on{opacity:1}
416
+ .lmtip b{font:600 13.5px var(--sans)}
417
+ .lmtip .lmts{display:block;font-size:12px;color:#8A919B;margin-top:1px}
418
+ .lmtip .lmtn{margin-top:8px;display:flex;flex-direction:column;gap:4px;font-size:12.5px;color:#C9CDD3}
419
+ .lmtip .lmtn b{font-size:12.5px;color:#F2A541}
420
+ body.lq-light .lmtip{background:rgba(255,255,255,.96);border-color:rgba(21,26,33,.1);color:#151A21;box-shadow:0 24px 50px -24px rgba(30,45,60,.45)}
421
+ body.lq-light .lmtip .lmtn{color:#2B3644}body.lq-light .lmtip .lmtn b{color:#A85F0A}
422
+ .lmzoom{position:absolute;right:12px;bottom:12px;display:flex;flex-direction:column;gap:6px;z-index:2}
423
+ .lmzoom .iconbtn{width:32px;height:32px;font:500 16px var(--sans)}
424
+ body.lq-liquid #lmtools{display:flex;flex-direction:column;gap:8px;margin-top:14px}
425
+ .lmrow{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px}
426
+ .lmk{font:500 11px var(--sans);letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin-right:6px;min-width:110px}
427
+ .lmt{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;color:var(--text);font:500 13px var(--sans);padding:4px 8px;border-radius:8px;cursor:pointer}
428
+ .lmt:hover{background:rgba(255,255,255,.05);color:var(--bone)}body.lq-light .lmt:hover{background:rgba(21,26,33,.05)}
429
+ .lmt small{font-size:11px;color:var(--faint)}
430
+ .lmnear{list-style:none;margin:6px 0 0;padding:0;display:flex;flex-direction:column;gap:2px}
431
+ .lmnear button{all:unset;box-sizing:border-box;display:flex;flex-direction:column;width:100%;padding:6px 8px;margin-left:-8px;border-radius:10px;cursor:pointer}
432
+ .lmnear button:hover{background:rgba(255,255,255,.05)}body.lq-light .lmnear button:hover{background:rgba(21,26,33,.05)}
433
+ .lmnear button:focus-visible{outline:2px solid currentColor}
434
+ .lmnear b{font:600 13px var(--sans);color:var(--bone)}.lmnear span{font-size:12px;color:var(--faint)}
435
+ .lmweeks{display:flex;align-items:flex-end;gap:3px;height:30px;margin-top:6px}
436
+ .lmweeks i{flex:1;min-width:4px;border-radius:2px;background:var(--faint);opacity:.6}
437
+ .lmweeks i:last-child{background:var(--bone);opacity:1}
438
+ body.lq-liquid .detail{width:310px;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:16px;max-height:64vh}
439
+ body.lq-liquid.lq-light .detail{background:rgba(255,255,255,.6);border-color:rgba(21,26,33,.08)}
440
+ body.lq-liquid .detail h3{font:600 17px var(--sans);letter-spacing:-.01em}
441
+ body.lq-liquid .detail .k{font:500 11px var(--sans);letter-spacing:.1em;text-transform:uppercase;margin-top:14px}
442
+ body.lq-liquid .detail .tag{border:0;background:rgba(255,255,255,.06);border-radius:999px;padding:3px 9px;font-size:12px;color:var(--text)}
443
+ body.lq-liquid.lq-light .detail .tag{background:rgba(21,26,33,.05)}
444
+ body.lq-liquid .profile{font-size:14px;color:var(--text);margin-bottom:10px}
445
+ .screensbox{margin-top:22px;border-top:1px solid var(--line);padding-top:12px}
446
+ .screensbox > summary{cursor:pointer;list-style:none;display:flex;align-items:baseline;gap:8px}
447
+ .screensbox > summary::-webkit-details-marker{display:none}
448
+ .screensbox > summary::before{content:'';width:5px;height:5px;border-right:1.5px solid var(--faint);border-bottom:1.5px solid var(--faint);transform:rotate(-45deg);transition:transform .2s ease;align-self:center}
449
+ .screensbox[open] > summary::before{transform:rotate(45deg)}
450
+ .screensbox .note{max-width:80ch}
451
+ @media (max-width:720px){body.lq-liquid .maprow{flex-direction:column}body.lq-liquid .detail{width:auto;max-height:none}.lmk{min-width:0;width:100%}}
452
+ i[class^=ic-]{display:inline-block;width:1.05em;height:1.05em;vertical-align:-.17em;background:currentColor;-webkit-mask:var(--ic) center/contain no-repeat;mask:var(--ic) center/contain no-repeat;font-style:normal}
453
+ .ic-chat{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5h16v11H9l-5 4z'/%3E%3C/svg%3E")}
454
+ .ic-plug{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 2v6M15 2v6M6 8h12v3a6 6 0 0 1-12 0zM12 17v5'/%3E%3C/svg%3E")}
455
+ .ic-person{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='8' r='4'/%3E%3Cpath d='M4 21a8 8 0 0 1 16 0'/%3E%3C/svg%3E")}
456
+ .ic-bot{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='8' width='16' height='12' rx='3'/%3E%3Cpath d='M12 3v5M9 14h.01M15 14h.01'/%3E%3C/svg%3E")}
457
+ .ic-archive{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='5' rx='1'/%3E%3Cpath d='M5 9v11h14V9M10 13h4'/%3E%3C/svg%3E")}
458
+ .ic-idea{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18h6M10 21h4M12 3a6 6 0 0 0-4 10.5c.8.9 1 1.5 1 2.5h6c0-1 .2-1.6 1-2.5A6 6 0 0 0 12 3z'/%3E%3C/svg%3E")}
459
+ .ic-ask{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.7.4-1 1-1 1.7M12 17h.01'/%3E%3C/svg%3E")}
460
+ .ic-pause{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 5v14M15 5v14'/%3E%3C/svg%3E")}
461
+ .ic-warn{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l10 18H2zM12 10v4M12 18h.01'/%3E%3C/svg%3E")}
462
+ .ic-x{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 6l12 12M18 6L6 18'/%3E%3C/svg%3E")}
463
+
464
+ /* ---- every panel, in the look ---- */
465
+ body.lq-liquid input[type=checkbox],body.lq-liquid input[type=radio]{-webkit-appearance:none;appearance:none;flex:none;width:18px;height:18px;margin:0;border-radius:50%;border:1.5px solid rgba(255,255,255,.3);background:transparent;cursor:pointer;display:inline-grid;place-items:center;transition:background-color .15s ease,border-color .15s ease;vertical-align:-4px}
466
+ body.lq-liquid input[type=checkbox]:checked{border-color:transparent;background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#C9CFD9 20%,#5B626D 58%,#121418 100%)}
467
+ body.lq-liquid input[type=radio]:checked{border-color:#F3F0EA;background:radial-gradient(circle,#F3F0EA 0 4px,transparent 4.5px)}
468
+ body.lq-liquid.lq-light input[type=checkbox],body.lq-liquid.lq-light input[type=radio]{border-color:rgba(21,26,33,.3)}
469
+ body.lq-liquid.lq-light input[type=checkbox]:checked{background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#E3E6EA 22%,#9BA2AC 55%,#3A3F46 100%)}
470
+ body.lq-liquid.lq-light input[type=radio]:checked{border-color:#151A21;background:radial-gradient(circle,#151A21 0 4px,transparent 4.5px)}
471
+ body.lq-liquid input[type=checkbox]:focus-visible,body.lq-liquid input[type=radio]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
472
+ body.lq-liquid .switch input[type=checkbox],body.lq-liquid input[type=checkbox].sw{width:34px;height:20px;border-radius:999px;background:rgba(255,255,255,.1);border:0;position:relative}
473
+ body.lq-liquid .switch input[type=checkbox]::after,body.lq-liquid input[type=checkbox].sw::after{content:'';position:absolute;left:3px;top:3px;width:14px;height:14px;border-radius:50%;background:#8A919B;transition:transform .18s ease,background-color .18s ease}
474
+ body.lq-liquid .switch input[type=checkbox]:checked,body.lq-liquid input[type=checkbox].sw:checked{background:#F3F0EA}
475
+ body.lq-liquid .switch input[type=checkbox]:checked::after,body.lq-liquid input[type=checkbox].sw:checked::after{transform:translateX(14px);background:#0A0B0D}
476
+ body.lq-liquid.lq-light .switch input[type=checkbox],body.lq-liquid.lq-light input[type=checkbox].sw{background:rgba(21,26,33,.12)}
477
+ body.lq-liquid.lq-light .switch input[type=checkbox]:checked,body.lq-liquid.lq-light input[type=checkbox].sw:checked{background:#151A21}
478
+ body.lq-liquid.lq-light .switch input[type=checkbox]:checked::after,body.lq-liquid.lq-light input[type=checkbox].sw:checked::after{background:#FFFFFF}
479
+ body.lq-liquid .fchip{border:1px solid rgba(255,255,255,.1);background:transparent;color:var(--text);font:500 12.5px var(--sans);padding:5px 11px}
480
+ body.lq-liquid .fchip:hover{border-color:rgba(255,255,255,.22);color:var(--bone)}
481
+ body.lq-liquid.lq-light .fchip{border-color:rgba(21,26,33,.12)}
482
+ body.lq-liquid .tag{background:rgba(255,255,255,.06);border:0;color:var(--text)}
483
+ body.lq-liquid.lq-light .tag{background:rgba(21,26,33,.05)}
484
+ body.lq-liquid .note{border-radius:12px}
485
+ /* write-ups */
486
+ .wuhead{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:12px;max-width:680px;margin:4px auto 18px}
487
+ .wut{margin:0;font:600 26px/1.15 var(--sans);letter-spacing:-.02em;color:var(--bone)}
488
+ .wus{font-size:13px;color:var(--faint);margin-top:4px}
489
+ .wua{display:flex;align-items:center;gap:8px}
490
+ #copy.done{color:#F2A541}
491
+ .swl{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px}.swl small{color:var(--faint)}
492
+ body.lq-liquid .out{max-width:680px;margin:0 auto;min-height:0;padding:0;background:transparent;border:0;border-radius:0;white-space:normal;font-size:15.5px;line-height:1.7;color:var(--text)}
493
+ body.lq-liquid .out.muted{padding:48px 0;text-align:center;color:var(--faint)}
494
+ body.lq-liquid .out h3,body.lq-liquid .out h4{margin:22px 0 6px;font:600 15px var(--sans);letter-spacing:.01em;color:var(--bone)}
495
+ body.lq-liquid .out h3{font-size:17px}
496
+ body.lq-liquid .out p{margin:0 0 10px}
497
+ body.lq-liquid .out ul{margin:4px 0 12px;padding-left:20px}
498
+ body.lq-liquid .out li{margin:3px 0}
499
+ body.lq-liquid .out li::marker{color:var(--faint)}
500
+ body.lq-liquid .out b{color:var(--bone);font-weight:600}
501
+ body.lq-liquid .rfoot{max-width:680px;margin:18px auto 0;font-family:var(--sans);border-top:1px solid var(--line)}
502
+ /* tasks */
503
+ body.lq-liquid .task{gap:12px;padding:11px 14px;border-radius:14px;border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.03);margin-top:6px;transition:background-color .15s ease}
504
+ body.lq-liquid .task:hover{background:rgba(255,255,255,.055)}
505
+ body.lq-liquid.lq-light .task{border-color:rgba(21,26,33,.07);background:rgba(255,255,255,.55)}
506
+ body.lq-liquid.lq-light .task:hover{background:rgba(255,255,255,.85)}
507
+ body.lq-liquid .task .t{font-size:14px;line-height:1.5;color:var(--text)}
508
+ body.lq-liquid .task .rp{background:transparent;border:0;padding:0;font-size:12px;color:var(--faint);white-space:nowrap}
509
+ body.lq-liquid .task .ask,body.lq-liquid .task .rm{display:inline-flex;align-items:center;justify-content:center;gap:4px;white-space:nowrap;min-width:28px;padding:0 6px;height:28px;border-radius:8px;font-size:15px;color:var(--faint)}
510
+ body.lq-liquid .task .rm{opacity:0;transition:opacity .15s ease}
511
+ body.lq-liquid .task:hover .rm,body.lq-liquid .task .rm:focus-visible,body.lq-touch .task .rm{opacity:1}
512
+ body.lq-liquid .task .ask:hover,body.lq-liquid .task .rm:hover{background:rgba(255,255,255,.07);color:var(--bone)}
513
+ body.lq-liquid .task .ask.has,body.lq-liquid .task .ask.on{color:var(--bone)}
514
+ body.lq-liquid #panel-tasks > .note,body.lq-liquid #panel-tasks .tnote{font-size:12.5px}
515
+ /* agents' questions */
516
+ body.lq-liquid .aq{border:1px solid rgba(255,255,255,.08);border-left:2px solid var(--amber);border-radius:14px;background:rgba(255,255,255,.03);padding:12px 16px}
517
+ body.lq-liquid.lq-light .aq{border-color:rgba(21,26,33,.08);border-left-color:var(--amber);background:rgba(255,255,255,.55)}
518
+ body.lq-liquid .aq h4{font:600 11px var(--sans);letter-spacing:.1em;display:flex;align-items:center;gap:6px}
519
+ body.lq-liquid .aq .q{border-bottom-color:var(--line)}
520
+ body.lq-liquid .aq label.opt{gap:10px;padding:4px 0}
521
+ body.lq-liquid .aq .opt input{margin-top:1px}
522
+ body.lq-liquid .who{display:inline-flex;align-items:center;gap:4px;padding:1px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:12px;font-weight:600;color:var(--bone);white-space:nowrap}
523
+ body.lq-liquid.lq-light .who{background:rgba(21,26,33,.06)}
524
+ /* drift */
525
+ body.lq-liquid .drift{border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.03);border-radius:14px;padding:14px 16px}
526
+ body.lq-liquid.lq-light .drift{border-color:rgba(21,26,33,.07);background:rgba(255,255,255,.55)}
527
+ .dbead{flex:none;width:12px;height:12px;border-radius:50%;background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#C9CFD9 20%,#5B626D 58%,#121418 100%)}
528
+ body.lq-light .dbead{background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#E3E6EA 22%,#9BA2AC 55%,#3A3F46 100%)}
529
+ .drift.risk .dbead{background:radial-gradient(circle at 34% 28%,#FFF4E2 0,#F2A541 45%,#7A4A10 100%);box-shadow:0 0 10px rgba(242,165,65,.4)}
530
+ body.lq-liquid .drift .dn{font-size:14.5px}
531
+ body.lq-liquid .drift ul{margin:8px 0 0 20px}
532
+ body.lq-liquid .drift li{position:relative;padding:3px 0 3px 14px;font-size:13.5px;color:var(--text)}
533
+ body.lq-liquid .drift li::before{content:'';position:absolute;left:0;top:11px;width:5px;height:5px;border-radius:50%;background:var(--faint)}
534
+ body.lq-liquid .drift li.warn{color:var(--bone)}
535
+ body.lq-liquid .drift li.warn::before{background:var(--amber);box-shadow:0 0 6px rgba(242,165,65,.6)}
536
+ body.lq-liquid .drift .ev{font-family:var(--sans);font-size:11.5px}
537
+
538
+ /* links in the look's ink */
539
+ body.lq-liquid main a{color:var(--bone);text-decoration:underline;text-decoration-color:rgba(255,255,255,.3);text-underline-offset:3px;cursor:pointer}
540
+ body.lq-liquid.lq-light main a{text-decoration-color:rgba(21,26,33,.3)}
541
+ /* Settings: one column of titled cards */
542
+ body.lq-liquid #panel-settings{max-width:780px;margin:0 auto;display:flex;flex-direction:column;gap:14px;padding-bottom:20px}
543
+ body.lq-liquid .sset{border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.03);border-radius:18px;padding:18px 20px}
544
+ body.lq-liquid.lq-light .sset{border-color:rgba(21,26,33,.07);background:rgba(255,255,255,.55)}
545
+ body.lq-liquid .sset.hidden{display:none}
546
+ .ssh{margin:0 0 10px;font:600 16px var(--sans);letter-spacing:-.01em;color:var(--bone);display:flex;align-items:baseline;gap:8px}
547
+ .ssh .muted{font-weight:400;font-size:12.5px}
548
+ body.lq-liquid .sset > label:not(.check):not(.swl){display:block;font:500 12.5px var(--sans);color:var(--faint);margin:12px 0 6px}
549
+ body.lq-liquid .sset label.check{display:flex;align-items:center;gap:10px;margin:8px 0;color:var(--text);font-size:13.5px}
550
+ body.lq-liquid .sset .note{font-size:12.5px;max-width:70ch}
551
+ body.lq-liquid .sset .cxh{margin-top:0}
552
+ /* the Close bar carries the panel's own ground, so what scrolls under it stays clean */
553
+ 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}
554
+ body.lq-liquid.lq-light.lq-pooled #lqsinkrow{background:linear-gradient(180deg,rgba(246,248,251,.98) 70%,rgba(246,248,251,0))}
321
555
  /* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
322
556
  .bstream{position:relative;margin-top:10px;border-radius:18px;overflow:hidden;isolation:isolate;background:#08090B;box-shadow:0 0 0 1px rgba(255,255,255,.08)}
323
557
  body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
@@ -325,7 +559,7 @@ body.lq-look-pearl .bstream{background:#EDECE9}
325
559
  .bstream.none{display:none}
326
560
  #bsc{position:absolute;inset:0;width:100%;height:100%;display:block}
327
561
  #bsov{position:absolute;inset:0}
328
- body.lq-liquid .bcard.blane{position:absolute;left:16px;width:150px;padding:0;border:0;background:transparent;box-shadow:none;transform:translateY(-50%)}
562
+ body.lq-liquid .bcard.blane{position:absolute;left:16px;width:186px;padding:0;border:0;background:transparent;box-shadow:none;transform:translateY(-50%)}
329
563
  .blane .bln{font:600 14px/1.2 var(--sans);color:var(--bone);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
330
564
  .blane .blc{font-size:12px;color:var(--faint);margin-top:1px}
331
565
  .blane .blc b{color:var(--amber);font-weight:600}
@@ -400,13 +634,15 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
400
634
  <button class="tab" data-tab="settings">Settings</button>
401
635
  </div>
402
636
  <main>
403
- <div id="lqsinkrow"><button class="ghost" type="button" id="lqsink" title="close it: it sinks back into the liquid (Esc)">Sink back</button></div>
637
+ <div id="lqsinkrow"><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>
404
638
  <section id="panel-map">
405
639
  <div class="profile muted" id="profile">Mapping your work&hellip;</div>
406
640
  <div class="maprow">
641
+ <div id="lmap" class="lmap"><canvas id="lmc" aria-hidden="true"></canvas><svg id="lmlines" aria-hidden="true"></svg><div id="lmov"></div><div id="lmtip" class="lmtip" role="status" aria-live="polite"></div><div class="lmzoom"><button type="button" class="iconbtn" id="lmin" aria-label="Zoom in">+</button><button type="button" class="iconbtn" id="lmout" aria-label="Zoom out">&minus;</button><button type="button" class="iconbtn" id="lmfit" aria-label="Fit the map" title="Fit the map"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M1 5V1h4M13 5V1H9M1 9v4h4M13 9v4H9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button></div></div>
407
642
  <svg id="graph" viewBox="0 0 960 620" preserveAspectRatio="xMidYMid meet"></svg>
408
643
  <aside id="detail" class="detail hidden"></aside>
409
644
  </div>
645
+ <div id="lmtools" class="lmtools"></div>
410
646
  <div id="mapbar" class="mapbar">Hover a node for a summary &middot; click for details &middot; drag to move</div>
411
647
  <div class="legend">
412
648
  <span class="lg"><i style="background:var(--green)"></i>you</span>
@@ -420,8 +656,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
420
656
  <button class="ghost" id="remap" style="margin-left:auto">Rescan</button>
421
657
  </div>
422
658
  <div id="review" class="review hidden"></div>
423
- <div style="margin-top:22px;border-top:1px solid var(--line);padding-top:4px">
424
- <div class="tgroup">Screens <span class="tcount">experimental &middot; blueprints for screen automation</span></div>
659
+ <details id="screensbox" class="screensbox"><summary><span class="tgroup">Screens</span> <span class="tcount">experimental &middot; blueprints for screen automation</span></summary>
425
660
  <div class="note muted" style="margin-top:2px">Capture a screen, then drag a box over each part that matters (a button, a field, a menu) and name it. Each region keeps its pixel coordinates and its centre, ready for automation to aim at. With more than one display, pick which one to capture, or one screen per display. <b>Click here</b> on a region clicks its centre on your real screen, after you confirm. Or let Symbiot do it all for a web page: <b>Map page</b> opens it in a hidden browser, takes its screenshot and marks every button, link and field by itself, and <b>Press</b> on one of them follows it there and maps the next page; <b>Type</b> on a field types into it there. A map only has what fits in the window, so on a longer page <b>Scroll down</b> maps the next part, or <b>Whole page</b> maps all of it in one tall screenshot (where a list scrolls inside the page, like Gmail's mail, that list opened out: a whole inbox on one screen). Only a mapped page is typed into: nothing types on your real screen. <b>Watch</b> on a mapped page (your inbox, say) has Symbiot read it again every few minutes while it runs and tell you what's new there.</div>
426
661
  <div class="row" style="margin-top:10px"><input id="pagesite" placeholder="a web page to map by itself: gmail, github.com/pulls or a web address" style="flex:1"><button class="act" id="mappage">Map page</button><button class="ghost" id="pagesignin" title="open this site in Symbiot's own browser as a window, to sign in once; close it when you're done">Sign in</button></div>
427
662
  <div class="row" style="margin-top:10px"><input id="screenname" placeholder="name the screen, e.g. GitHub PR page" style="flex:1"><select id="screenwhich" class="hidden" title="which display to capture" style="flex:0 0 auto;width:auto"></select><select id="screendelay" title="wait first, so you can bring the right window to the front" style="flex:0 0 auto;width:auto"><option value="0">now</option><option value="3">in 3s</option><option value="5">in 5s</option><option value="10">in 10s</option></select><button class="ghost" id="capture">Capture screen</button><button class="ghost" id="screenload" title="use a PNG screenshot you already have">Load image</button><input type="file" id="screenfile" accept="image/png" class="hidden"></div>
@@ -429,10 +664,10 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
429
664
  <div id="watchbox"></div>
430
665
  <div id="screenlist" class="taskfilter"></div>
431
666
  <div id="screenview"></div>
432
- </div>
667
+ </details>
433
668
  </section>
434
669
  <section id="panel-board" class="hidden">
435
- <div class="row"><span class="muted" id="boardsum" style="flex:1">Everything you watch, side by side.</span><select id="boardhours" title="how far back" style="flex:0 0 auto;width:auto"><option value="24">last 24 hours</option><option value="72">last 3 days</option><option value="168">last 7 days</option></select><button class="ghost" id="boardrefresh">Refresh</button></div>
670
+ <div class="bhead"><div class="bsumw"><div id="boardsum" class="bsum">Everything you watch, side by side.</div><div id="boardsub" class="bsub"></div></div><div class="seg" id="boardwin" role="radiogroup" aria-label="How far back"><button type="button" role="radio" data-h="24" aria-checked="true">24 hours</button><button type="button" role="radio" data-h="72" aria-checked="false">3 days</button><button type="button" role="radio" data-h="168" aria-checked="false">7 days</button></div><button type="button" class="iconbtn" id="boardrefresh" 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>
436
671
  <div id="boardmsg"></div>
437
672
  <div id="bstream" class="bstream"><canvas id="bsc" aria-hidden="true"></canvas><div id="bsov"></div></div>
438
673
  <div id="boardtalk"></div>
@@ -440,15 +675,13 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
440
675
  <div id="boardlinks"></div>
441
676
  </section>
442
677
  <section id="panel-run" class="hidden">
443
- <div class="row"><button class="act" id="write">Write my <span id="what">week</span></button>
444
- <button class="ghost hidden" id="copy">Copy</button>
445
- <span class="muted">Reads your local git and writes it up.</span></div>
446
- <div class="out muted" id="out">Nothing yet - hit the button.</div>
678
+ <div class="wuhead"><div><h2 class="wut" id="wutitle">Your week</h2><div class="wus" id="wusub">The last 7 days, from your local git</div></div><div class="wua"><button class="iconbtn hidden" id="copy" title="Copy" aria-label="Copy"><svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><rect x="5" y="5" width="9" height="9" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3 11V3a1 1 0 0 1 1-1h7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button><button class="act" id="write">Write my <span id="what">week</span></button></div></div>
679
+ <div class="out muted" id="out">Nothing written yet. Symbiot reads your local git and writes it up for you.</div>
447
680
  <div class="rfoot" id="outfoot" style="display:none"></div>
448
681
  </section>
449
682
  <section id="panel-tasks" class="hidden">
450
683
  <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>
451
- <div class="note muted" style="margin-top:2px">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>&#128172;</b> on a task to ask questions about it.</div>
684
+ <div class="note muted" style="margin-top:2px">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>
452
685
  <div id="pushout"></div>
453
686
  <div id="reviewout"></div>
454
687
  <div id="reviewlist"></div>
@@ -463,12 +696,13 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
463
696
  </section>
464
697
  <section id="panel-drift" class="hidden">
465
698
  <div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
466
- <label class="muted" style="margin-left:auto"><input type="checkbox" id="driftfetch"> fetch latest</label>
467
- <label class="muted"><input type="checkbox" id="driftci"> check CI (needs gh, experimental)</label>
699
+ <label class="muted swl" style="margin-left:auto"><input type="checkbox" class="sw" id="driftfetch"> Fetch latest</label>
700
+ <label class="muted swl"><input type="checkbox" class="sw" id="driftci"> Check CI <small>(needs gh, experimental)</small></label>
468
701
  <button class="ghost" id="driftrun">Rescan</button></div>
469
702
  <div id="driftout"></div>
470
703
  </section>
471
704
  <section id="panel-settings" class="hidden">
705
+ <div class="sset"><h3 class="ssh">Your AI</h3>
472
706
  <label>Which AI should Symbiot write with?</label>
473
707
  <select id="provider">
474
708
  <option value="anthropic">Claude (Anthropic)</option>
@@ -484,19 +718,20 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
484
718
  <input id="model" type="text" placeholder="(blank = default)">
485
719
  <div class="row" style="margin-top:16px"><button class="act" id="save">Save &amp; connect</button>
486
720
  <span class="note" id="saveMsg"></span></div>
487
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
488
- <label>Folders to scan for repos</label>
721
+ </div>
722
+ <div class="sset">
723
+ <h3 class="ssh">Folders to scan for repos</h3>
489
724
  <div id="scanroots"></div>
490
725
  <div class="row" style="margin-top:6px"><input id="newroot" placeholder="/path/to/folder (or ~/work) — where your projects live" style="flex:1"><button class="ghost" id="addroot">Add folder</button></div>
491
726
  <div class="note muted" id="scanrootnote">Point Symbiot at where your work lives — inside or outside your home folder. Defaults to your home folder.</div>
492
727
  </div>
493
- <div id="phonebox" class="hidden" style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
494
- <label>Projects in Termux</label>
728
+ <div id="phonebox" class="hidden sset">
729
+ <h3 class="ssh">Projects in Termux</h3>
495
730
  <div class="note muted" id="phonenote"></div>
496
731
  <div class="row" style="margin-top:6px"><button class="ghost" id="phonebtn"></button></div>
497
732
  </div>
498
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
499
- <label>Hand off to your agent when you "Send to repos"</label>
733
+ <div class="sset">
734
+ <h3 class="ssh">Hand off to your agent when you "Send to repos"</h3>
500
735
  <input id="agentcmd" type="text" placeholder="e.g. aider --message &quot;{prompt}&quot; --yes · code {dir} · leave blank to just write the file">
501
736
  <div class="note muted">Runs in each repo after tasks are written. Use <b>{dir}</b> = repo path, <b>{prompt}</b> = the task instruction. Works with any agent or editor &mdash; it's your command.</div>
502
737
  <div id="agentpresets" style="margin-top:8px"></div>
@@ -508,26 +743,24 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
508
743
  <div class="note" id="grantnote"></div>
509
744
  </div>
510
745
  </div>
511
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
512
- <label>Link your work</label>
513
- <div class="note muted" style="margin-top:2px">One click per site you work in: it opens the site in Symbiot's own browser, where you sign in the usual way (its own login, SSO and 2FA; Symbiot never sees your password). Symbiot then trusts the site and watches its inbox or notifications, so what arrives there shows on the Dashboard and in Week and Standup. Close the window when you're signed in.</div>
746
+ <div class="sset">
514
747
  <div id="links"></div>
515
748
  <div id="linksmsg"></div>
516
749
  <div class="note muted" id="linksnote">For a whole team, one <b>links.json</b> in Symbiot's config folder adds your company's own sites and hides the ones you don't use, so everyone gets the same buttons.</div>
517
750
  </div>
518
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
519
- <label>What Symbiot remembers</label>
751
+ <div class="sset">
752
+ <h3 class="ssh">What Symbiot remembers</h3>
520
753
  <div class="note muted" style="margin-top:2px">Every chat in the app is the same Symbiot. What's worth knowing on another page (who someone is, which account is what, what you decided) is kept here, on this computer only, and each chat gets just the parts its question touches.</div>
521
754
  <div id="mindlist"></div>
522
755
  </div>
523
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
524
- <label>Trusted sites for Screens <span class="muted">(experimental)</span></label>
756
+ <div class="sset">
757
+ <h3 class="ssh">Trusted sites for Screens <span class="muted">(experimental)</span></h3>
525
758
  <div id="trustedsites"></div>
526
759
  <div class="row" style="margin-top:6px"><input id="newtrusted" placeholder="a site, e.g. mail.google.com or github.com" style="flex:1"><button class="ghost" id="addtrusted">Trust site</button></div>
527
760
  <div class="note muted" id="trustednote">On a mapped page from one of these sites, <b>Press</b> and <b>Type</b> go ahead without asking, for you and for agents (<b>symbiot screens press</b> / <b>type</b>), signed in as you. A site covers its subdomains: google.com covers mail.google.com. Everywhere else, each one asks first. Only you add sites, here: Symbiot gives agents no command for it.</div>
528
761
  </div>
529
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
530
- <label>Email &mdash; add what you sent to Week and Standup <span class="muted">(experimental)</span></label>
762
+ <div class="sset">
763
+ <h3 class="ssh">Email &mdash; add what you sent to Week and Standup <span class="muted">(experimental)</span></h3>
531
764
  <label class="check"><input type="checkbox" id="mailon"> Use my sent email (subjects &amp; recipients only)</label>
532
765
  <div id="mailsources"></div>
533
766
  <div class="row" style="margin-top:6px"><input id="newmail" placeholder="add a mail folder or .mbox file (e.g. a Google Takeout export)" style="flex:1"><button class="ghost" id="addmail">Add</button><button class="ghost" id="mailpreview">Preview</button></div>
@@ -535,19 +768,19 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
535
768
  <div class="note muted" id="mailnote">No API, no OAuth, no password: Symbiot reads the mail your desktop mail app (Thunderbird, Apple Mail, Evolution, mutt&hellip;) already keeps on this computer, or an exported .mbox &mdash; so anyone can link theirs. Headers only, never a message body; off until you tick it. Experimental: tested with mbox and Maildir, not yet on real Apple Mail, Evolution or KMail stores.</div>
536
769
  <div id="mailout"></div>
537
770
  </div>
538
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
539
- <label>Weekly write-up and start at login</label>
771
+ <div class="sset">
772
+ <h3 class="ssh">Weekly write-up and start at login</h3>
540
773
  <label class="check"><input type="checkbox" id="weeklyon"> Write my week and send me a desktop notification every</label>
541
774
  <div class="row" style="margin-top:2px"><select id="weeklyday" style="width:auto"><option value="1">Monday</option><option value="2">Tuesday</option><option value="3">Wednesday</option><option value="4">Thursday</option><option value="5">Friday</option><option value="6">Saturday</option><option value="0">Sunday</option></select><span class="muted">at</span><select id="weeklyhour" style="width:auto"></select><button class="ghost" id="weeklynow" title="write it now and send the notification, to check it works">Write it now</button></div>
542
775
  <label class="check"><input type="checkbox" id="autostart"> <span id="autostartlbl">Start Symbiot in the background when I log in (no window)</span></label>
543
776
  <div class="note muted" id="desktopnote"></div>
544
777
  </div>
545
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
546
- <label>Watch on your phone <span class="muted">(experimental)</span></label>
778
+ <div class="sset">
779
+ <h3 class="ssh">Watch on your phone <span class="muted">(experimental)</span></h3>
547
780
  <div id="phonelink"></div>
548
781
  </div>
549
- <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
550
- <label>Local models <span class="muted">(experimental)</span></label>
782
+ <div class="sset">
783
+ <h3 class="ssh">Local models <span class="muted">(experimental)</span></h3>
551
784
  <button class="ghost" id="recbtn">Recommend models for my machine</button>
552
785
  <button class="ghost" id="setuplocal" style="margin-left:8px">Set up a free local model</button>
553
786
  <div id="setupout" class="note muted" style="margin-top:10px"></div>
@@ -574,7 +807,7 @@ $('panel-settings').classList.toggle('hidden',!isSet);
574
807
  $('panel-tasks').classList.toggle('hidden',!isTasks);
575
808
  $('panel-drift').classList.toggle('hidden',!isDrift);
576
809
  $('panel-agents').classList.toggle('hidden',!isAgents);
577
- if(isRun){$('what').textContent=tab;$('out').textContent='Nothing yet - hit the button.';$('out').classList.add('muted');$('copy').classList.add('hidden');$('outfoot').style.display='none';if(tab==='week')showLatestWeek();}
810
+ 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();}
578
811
  if(isMap&&!mapLoaded)loadMap();
579
812
  if(isTasks){fillTaskRepos();loadTasks();}
580
813
  if(isDrift&&!driftLoaded)loadDrift();
@@ -586,12 +819,13 @@ function firstTab(){api('/api/watch/board').then(function(b){if(b&&b.cards&&b.ca
586
819
  function refresh(){api('/api/status').then(function(s){$('status').textContent=s.connected?s.line:'Not connected - open Settings';});}
587
820
  // the status opens Settings: on a phone its tab is scrolled out of sight
588
821
  $('status').style.cursor='pointer';$('status').addEventListener('click',function(){setTab('settings');var t=document.querySelector('.tab[data-tab=settings]');if(t&&t.scrollIntoView)t.scrollIntoView({block:'nearest',inline:'nearest'});});
589
- $('write').addEventListener('click',function(){$('out').textContent='Writing...';$('out').classList.add('muted');$('copy').classList.add('hidden');
822
+ var WUEMPTY='Nothing written yet. Symbiot reads your local git and writes it up for you.',WUTEXT='';
823
+ $('write').addEventListener('click',function(){$('out').textContent='Writing it up from your local git…';$('out').classList.add('muted');$('copy').classList.add('hidden');
590
824
  api('/api/run',{cmd:current}).then(function(r){var f=$('outfoot');if(r.error==='not-connected'){$('out').textContent='Not connected yet - open Settings and pick an AI.';f.style.display='none';return;}
591
825
  // a week is saved to weeks/ as well (r.file), like Settings' Write it now
592
826
  var foot=[r.footer,r.file?'Saved to '+r.file:'',r.error&&r.text&&r.error!==r.text?r.error:''].filter(Boolean).join(' · ');
593
- $('out').textContent=r.text||'(no output)';$('out').classList.remove('muted');$('copy').classList.remove('hidden');if(foot){f.textContent=foot;f.style.display='block';}else{f.style.display='none';}});});
594
- $('copy').addEventListener('click',function(){navigator.clipboard.writeText($('out').textContent);$('copy').textContent='Copied';setTimeout(function(){$('copy').textContent='Copy';},1400);});
827
+ WUTEXT=r.text||'';$('out').innerHTML=r.text?mdLite(r.text):'(no output)';$('out').classList.remove('muted');$('copy').classList.remove('hidden');if(foot){f.textContent=foot;f.style.display='block';}else{f.style.display='none';}});});
828
+ $('copy').addEventListener('click',function(){var c=$('copy');try{navigator.clipboard.writeText(WUTEXT||$('out').textContent);}catch(e){}c.setAttribute('title','Copied');c.classList.add('done');setTimeout(function(){c.setAttribute('title','Copy');c.classList.remove('done');},1400);});
595
829
  function syncP(){var p=$('provider').value;var local=p==='ollama';$('keyWrap').classList.toggle('hidden',local);$('baseWrap').classList.toggle('hidden',!local);
596
830
  $('getkey').textContent=local?'About Ollama':'Where do I get a key?';$('modelHint').textContent='(default '+DEFMODEL[p]+')';$('model').placeholder='(blank = '+DEFMODEL[p]+')';}
597
831
  $('provider').addEventListener('change',syncP);
@@ -604,7 +838,7 @@ var GRAPH=null,sel=null,view={k:1,x:0,y:0},GW=960,GH=620;
604
838
  function esc(s){return String(s).replace(/[&<>]/g,function(ch){return ch==='&'?'&amp;':ch==='<'?'&lt;':'&gt;';});}
605
839
  // An agent's option, with its who-acts markers as badges: 👤 You (you do this
606
840
  // step) and 🤖 Agent (picking it is enough; the next run does it). Escaped first.
607
- function whoHtml(o){return esc(o).replace(/👤[ ]*(You:|You(?=[ ]))?/g,"<span class='who you'>&#128100; You</span> ").replace(/🤖[ ]*(Agent:|Agent(?=[ ]))?/g,"<span class='who agent'>&#129302; Agent</span> ");}
841
+ function whoHtml(o){return esc(o).replace(/👤[ ]*(You:|You(?=[ ]))?/g,"<span class='who you'><i class=ic-person></i> You</span> ").replace(/🤖[ ]*(Agent:|Agent(?=[ ]))?/g,"<span class='who agent'><i class=ic-bot></i> Agent</span> ");}
608
842
  function layout(nodes,edges){var idx={};nodes.forEach(function(n){n.x=GW/2+(Math.random()-0.5)*GW*0.8;n.y=GH/2+(Math.random()-0.5)*GH*0.8;n.vx=0;n.vy=0;idx[n.id]=n;});
609
843
  for(var it=0;it<340;it++){for(var i=0;i<nodes.length;i++)for(var j=i+1;j<nodes.length;j++){var a=nodes[i],b=nodes[j];var dx=a.x-b.x,dy=a.y-b.y;var d2=dx*dx+dy*dy+0.01;var d=Math.sqrt(d2);var f=4600/d2;a.vx+=f*dx/d;a.vy+=f*dy/d;b.vx-=f*dx/d;b.vy-=f*dy/d;}
610
844
  edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y;var d=Math.sqrt(dx*dx+dy*dy)+0.01;var f=(d-115)*0.03;a.vx+=f*dx/d;a.vy+=f*dy/d;b.vx-=f*dx/d;b.vy-=f*dy/d;});
@@ -617,7 +851,97 @@ GRAPH.nodes.forEach(function(n){var r=Math.max(5,Math.sqrt(n.weight)*2);var col=
617
851
  s+="<g class='node' data-id='"+esc(n.id)+"' opacity='"+op+"'><circle cx='"+n.x.toFixed(1)+"' cy='"+n.y.toFixed(1)+"' r='"+r.toFixed(1)+"' fill='url(#lqsilver)' stroke='"+col+"' stroke-width='"+(n.id===sel?2.5:1.2)+"'"+(n.id===sel?" stroke-opacity='1'":" stroke-opacity='0.7'")+"><title>"+title+"</title></circle>";
618
852
  var cls=n.type==="person"?"lbl-me":"";s+="<text x='"+n.x.toFixed(1)+"' y='"+(n.y+r+12).toFixed(1)+"' text-anchor='middle' class='"+cls+"'>"+esc(n.label)+"</text></g>";});
619
853
  document.getElementById("graph").innerHTML=s+"</g>";}
620
- function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});var base=g.stats.base||"your home folder";
854
+
855
+ // ---- the Map, in the liquid -------------------------------------------------------
856
+ // Your repos and project folders as droplets in the look, placed by their nearest
857
+ // neighbours (mapknn.mjs: what they're built with, the weeks you work on them, what
858
+ // they're about). The closest merge into one shape, the next are joined by a tendril,
859
+ // the rest by a faint line; each cluster is named over its region; what you haven't
860
+ // touched in a while is smaller and sinks; you sit where your recent work is.
861
+ // Point at a droplet: its neighbours light up and say why. Click: its details.
862
+ var LM={G:null,on:false,items:[],edges:[],clusters:[],view:{k:1,x:0,y:0},hot:null,pan:null,built:false};
863
+ function lmTilde(p){p=String(p||'');var parts=p.split('/');return (parts[1]==='home'||parts[1]==='Users')&&parts.length>2?'~'+(parts.length>3?'/'+parts.slice(3).join('/'):''):p;}
864
+ function lmName(id){var n=GRAPH&&nodeById(id);return n?n.label:String(id).split('/').pop();}
865
+ function lmNearHtml(id){var k=GRAPH&&GRAPH.knn,ns=k&&k.neighbours&&k.neighbours[id];if(!ns||!ns.length)return '';
866
+ return "<div class='k'>Most like it</div><ul class='lmnear'>"+ns.map(function(n){return "<li><button type='button' data-id='"+escQ(n.id)+"'><b>"+esc(lmName(n.id))+"</b><span>"+esc(n.why.join('; ')||'a little alike')+"</span></button></li>";}).join('')+"</ul>";}
867
+ function lmNearWire(el){if(el&&el.querySelectorAll)el.querySelectorAll('.lmnear button').forEach(function(b){b.addEventListener('click',function(){selectNode(b.getAttribute('data-id'));});});}
868
+ function lmWeeks(d){var n=GRAPH&&nodeById('repo:'+d.path),w=n&&n.meta&&n.meta.weeks;if(!w||!w.length)return '';var mx=Math.max.apply(null,w.concat([1]));
869
+ return "<div class='k'>Your last 12 weeks</div><div class='lmweeks' role='img' aria-label='"+w.join(', ')+" commits a week'>"+w.map(function(x){return "<i style='height:"+Math.max(2,Math.round(28*x/mx))+"px' title='"+x+" commits'></i>";}).join('')+"</div>";}
870
+ function lmBuild(g){var el=$('lmap');if(!el||!g)return;LM.g=g;var K=g.knn||{neighbours:{},edges:[],clusters:[],vitality:{}};
871
+ var items=g.nodes.filter(function(n){return n.type==='repo'||n.type==='folder';}),mx=1;items.forEach(function(n){var c=(n.meta&&n.meta.commits)||0;if(c>mx)mx=c;});
872
+ var byId={};LM.items=items.map(function(n){var v=K.vitality[n.id]||0,c=(n.meta&&n.meta.commits)||0,fold=n.type==='folder';
873
+ var it={id:n.id,n:n,v:v,fold:fold,r:fold?10:(14+30*Math.sqrt(c/mx))*(0.7+0.3*v),x:0,y:0};byId[n.id]=it;return it;});
874
+ LM.edges=(K.edges||[]).filter(function(e){return byId[e.a]&&byId[e.b];}).map(function(e){return {a:byId[e.a],b:byId[e.b],sim:e.sim};});
875
+ // seed by cluster: each cluster (and each repo on its own) gets its own arc
876
+ var groups=(K.clusters||[]).map(function(c){return c.ids.filter(function(id){return byId[id];}).map(function(id){return byId[id];});}).filter(function(m){return m.length;}),inG={};
877
+ groups.forEach(function(m){m.forEach(function(it){inG[it.id]=1;});});LM.items.forEach(function(it){if(!inG[it.id])groups.push([it]);});
878
+ var tot=0;groups.forEach(function(m){tot+=m.length+0.6;});var acc=-Math.PI/2;
879
+ groups.forEach(function(m){var span=(m.length+0.6)/tot*2*Math.PI,mid=acc+span/2;acc+=span;var R=180+40*m.length;m.forEach(function(it,i){var a=mid+(i-(m.length-1)/2)*Math.min(0.5,span/m.length);it.x=Math.cos(a)*R*(i%2?1.12:0.92);it.y=Math.sin(a)*R*0.8*(i%2?1.12:0.92);});});
880
+ var link={};LM.edges.forEach(function(e){link[e.a.id+'|'+e.b.id]=link[e.b.id+'|'+e.a.id]=e.sim;});
881
+ var L=LM.items;for(var it=0;it<700;it++){
882
+ LM.edges.forEach(function(e){var dx=e.b.x-e.a.x,dy=e.b.y-e.a.y,d=Math.sqrt(dx*dx+dy*dy)||1,rest=e.sim>=0.45?(e.a.r+e.b.r)*1.15:e.a.r+e.b.r+40+150*(1-e.sim),f=(d-rest)*0.05*Math.min(1,e.sim*2.2)/d;e.a.x+=dx*f;e.a.y+=dy*f;e.b.x-=dx*f;e.b.y-=dy*f;});
883
+ for(var i=0;i<L.length;i++){var a=L[i];for(var j=i+1;j<L.length;j++){var b=L[j],dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)||1,sm=link[a.id+'|'+b.id],mn=sm>=0.45?(a.r+b.r)*1.08:a.r+b.r+(sm?30:70);if(d<mn){var f=(mn-d)*0.3/d;a.x-=dx*f;a.y-=dy*f;b.x+=dx*f;b.y+=dy*f;}}
884
+ a.x*=0.996;a.y=a.y*0.996+(1-a.v)*0.5;}}
885
+ // you: where your recent work is
886
+ var sx=0,sy=0,sw=0;L.forEach(function(a){var w=a.fold?0:a.v*(1+((a.n.meta&&a.n.meta.commits)||0));sx+=a.x*w;sy+=a.y*w;sw+=w;});
887
+ LM.me={id:'me',x:sw?sx/sw:0,y:sw?sy/sw-10:-10,r:14};for(var q=0;q<200;q++)L.forEach(function(a){var dx=LM.me.x-a.x,dy=LM.me.y-a.y,d=Math.sqrt(dx*dx+dy*dy)||1,mn=a.r+LM.me.r+28;if(d<mn){LM.me.x+=dx/d*(mn-d)*0.5;LM.me.y+=dy/d*(mn-d)*0.5;}});
888
+ LM.clusters=(K.clusters||[]).map(function(c){return {label:c.label,m:c.ids.map(function(id){return byId[id];}).filter(Boolean)};}).filter(function(c){return c.m.length>1;});
889
+ // labels
890
+ var ov=$('lmov');if(ov){ov.innerHTML=L.map(function(a,i){var m=a.n.meta||{},sub=a.fold?'folder, no git':(m.commits||0)+((m.commits||0)===1?' commit':' commits')+(m.last?' &middot; '+lmAgo(m.last):'');return "<button type='button' class='lml"+(a.v<0.25&&!a.fold?' dormant':'')+(a.fold?' fold':'')+"' data-i='"+i+"'><b>"+esc(a.n.label)+"</b><span>"+sub+"</span></button>";}).join('')+
891
+ "<div class='lmme' id='lmme' aria-hidden='true'><i></i><b>you are here</b></div>"+LM.clusters.map(function(c,i){return "<div class='lmc' data-c='"+i+"'>"+esc(c.label)+"</div>";}).join('')+L.filter(function(a){return a.fold;}).map(function(a){return "<i class='lmring' data-id='"+escQ(a.id)+"'></i>";}).join('');
892
+ LM.labs=[].slice.call(ov.querySelectorAll('.lml[data-i]'));LM.cls=[].slice.call(ov.querySelectorAll('.lmc'));LM.rings=[].slice.call(ov.querySelectorAll('.lmring'));
893
+ LM.labs.forEach(function(b){var a=L[+b.getAttribute('data-i')];b.addEventListener('click',function(){selectNode(a.id);});b.addEventListener('pointerenter',function(){lmHot(a);});b.addEventListener('focus',function(){lmHot(a);});b.addEventListener('pointerleave',function(){lmHot(null);});b.addEventListener('blur',function(){lmHot(null);});});}
894
+ // the rest of what you build with, as a quiet row under the map
895
+ var tl=$('lmtools');if(tl){var langs={};L.forEach(function(a){((a.n.meta&&a.n.meta.langs)||[]).forEach(function(x){langs[x]=(langs[x]||0)+1;});});
896
+ var lg=Object.keys(langs).sort(function(x,y){return langs[y]-langs[x];});var tools=g.nodes.filter(function(n){return n.type==='tool'||n.type==='agent'||n.type==='ai';});
897
+ tl.innerHTML=(lg.length?"<div class='lmrow'><span class='lmk'>Languages</span>"+lg.map(function(x){return "<button type='button' class='lmt' data-id='lang:"+escQ(x)+"'>"+esc(x)+"<small>"+langs[x]+"</small></button>";}).join('')+"</div>":"")+
898
+ (tools.length?"<div class='lmrow'><span class='lmk'>You build with</span>"+tools.map(function(n){return "<button type='button' class='lmt' data-id='"+escQ(n.id)+"'>"+esc(n.label)+"</button>";}).join('')+"</div>":"");
899
+ tl.querySelectorAll('.lmt').forEach(function(b){b.addEventListener('click',function(){selectNode(b.getAttribute('data-id'));});});}
900
+ lmFit();if(!LM.built){LM.built=true;lmWire();}lmStart();}
901
+ function lmAgo(day){var t=Date.parse(day);if(!t)return day;var d=Math.round((Date.now()-t)/86400000);return d<=0?'today':d===1?'yesterday':d<14?d+' days ago':d<60?Math.round(d/7)+' weeks ago':Math.round(d/30)+' months ago';}
902
+ function lmFit(){var el=$('lmap'),L=LM.items;if(!el||!L.length)return;var x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;L.concat([LM.me]).forEach(function(a){x0=Math.min(x0,a.x-a.r);y0=Math.min(y0,a.y-a.r);x1=Math.max(x1,a.x+a.r);y1=Math.max(y1,a.y+a.r+30);});
903
+ var w=el.clientWidth||900,h=el.clientHeight||520,k=Math.min(1.6,(w-160)/Math.max(1,x1-x0),(h-150)/Math.max(1,y1-y0));LM.view={k:k,x:w/2-(x0+x1)/2*k,y:h/2-(y0+y1)/2*k+20};}
904
+ function lmHot(a){LM.hot=a;var tip=$('lmtip');var near={};if(a&&GRAPH&&GRAPH.knn)(GRAPH.knn.neighbours[a.id]||[]).forEach(function(n){near[n.id]=n;});
905
+ (LM.labs||[]).forEach(function(b){var it=LM.items[+b.getAttribute('data-i')];if(b.classList){b.classList.toggle('near',!!(a&&near[it.id]));b.classList.toggle('far',!!(a&&it!==a&&!near[it.id]));}});
906
+ if(!tip)return;if(!a){tip.className='lmtip';return;}var m=a.n.meta||{},ns=(GRAPH&&GRAPH.knn&&GRAPH.knn.neighbours[a.id])||[];
907
+ tip.innerHTML="<b>"+esc(a.n.label)+"</b><span class='lmts'>"+(a.fold?'a project folder, not a git repo':(m.commits||0)+((m.commits||0)===1?' commit':' commits')+(m.last?' &middot; last '+lmAgo(m.last):'')+(m.branch?' &middot; on '+esc(m.branch):''))+"</span>"+(ns.length?"<div class='lmtn'>"+ns.map(function(n){return "<div><b>"+esc(lmName(n.id))+"</b> "+esc(n.why.join('; ')||'a little alike')+"</div>";}).join('')+"</div>":"<div class='lmtn'>Nothing else is much like it.</div>");tip.className='lmtip on';}
908
+ function lmS(p){return {x:p.x*LM.view.k+LM.view.x,y:p.y*LM.view.k+LM.view.y};}
909
+ function lmWire(){var el=$('lmap');if(!el||!el.addEventListener)return;
910
+ el.addEventListener('wheel',function(e){if(!e)return;e.preventDefault();var r=el.getBoundingClientRect(),mx=e.clientX-r.left,my=e.clientY-r.top,k0=LM.view.k,k=Math.max(0.3,Math.min(4,k0*Math.exp(-e.deltaY*0.0015)));LM.view.x=mx-(mx-LM.view.x)*k/k0;LM.view.y=my-(my-LM.view.y)*k/k0;LM.view.k=k;},{passive:false});
911
+ el.addEventListener('pointerdown',function(e){if(e.target&&e.target.closest&&e.target.closest('button,.lmtip'))return;LM.pan={x:e.clientX,y:e.clientY,vx:LM.view.x,vy:LM.view.y,moved:false};if(el.setPointerCapture)el.setPointerCapture(e.pointerId);});
912
+ el.addEventListener('pointermove',function(e){if(LM.pan){var dx=e.clientX-LM.pan.x,dy=e.clientY-LM.pan.y;if(Math.abs(dx)+Math.abs(dy)>3)LM.pan.moved=true;LM.view.x=LM.pan.vx+dx;LM.view.y=LM.pan.vy+dy;return;}
913
+ var r=el.getBoundingClientRect(),mx=e.clientX-r.left,my=e.clientY-r.top,hit=null;LM.items.forEach(function(a){var p=lmS(a);if(!hit&&Math.hypot(mx-p.x,my-p.y)<=a.r*LM.view.k+6)hit=a;});if(hit!==LM.hot&&!(e.target&&e.target.closest&&e.target.closest('.lml')))lmHot(hit);});
914
+ el.addEventListener('pointerup',function(e){var p=LM.pan;LM.pan=null;if(p&&!p.moved&&LM.hot&&!(e.target&&e.target.closest&&e.target.closest('button')))selectNode(LM.hot.id);});
915
+ el.addEventListener('pointerleave',function(){if(!LM.pan)lmHot(null);});
916
+ var zi=$('lmin'),zo=$('lmout'),zf=$('lmfit');function z(f){var w=el.clientWidth/2,h=el.clientHeight/2,k0=LM.view.k,k=Math.max(0.3,Math.min(4,k0*f));LM.view.x=w-(w-LM.view.x)*k/k0;LM.view.y=h-(h-LM.view.y)*k/k0;LM.view.k=k;}
917
+ if(zi)zi.addEventListener('click',function(){z(1.25);});if(zo)zo.addEventListener('click',function(){z(0.8);});if(zf)zf.addEventListener('click',lmFit);}
918
+ function lmStart(){if(LM.on)return;var raf=typeof window.requestAnimationFrame==='function'?function(f){return window.requestAnimationFrame(f);}:null;if(!raf)return;LM.on=true;
919
+ var loop=function(){var p=$('panel-map'),el=$('lmap');if(!p||!el||!el.offsetParent){LM.on=false;return;}lmFrame();raf(loop);};raf(loop);}
920
+ function lmFrame(){var el=$('lmap'),cv=$('lmc');if(!el||!cv||!LM.items.length)return;if(!LM.G)LM.G=lqGL(cv,64);var w=el.clientWidth,h=el.clientHeight,t=LM.t=(LM.t||0)+0.016,still=LQ.theme&&LQ.theme.still,k=LM.view.k;
921
+ var P=LM.items.map(function(a,i){var p=lmS(a);if(!still){p.x+=Math.sin(t*0.4+i*1.7)*2;p.y+=Math.cos(t*0.35+i*2.3)*2;}return p;}),ME=lmS(LM.me);
922
+ // labels, rings, cluster names, tip
923
+ (LM.labs||[]).forEach(function(b){var i=+b.getAttribute('data-i'),a=LM.items[i],p=P[i];if(!b.style)return;b.style.transform='translate('+Math.round(p.x-(b.offsetWidth||0)/2)+'px,'+Math.round(p.y+a.r*k+4)+'px)';});
924
+ var me=$('lmme');if(me&&me.style)me.style.transform='translate('+Math.round(ME.x-(me.offsetWidth||0)/2)+'px,'+Math.round(ME.y-7)+'px)';
925
+ (LM.rings||[]).forEach(function(rg){var i=-1;LM.items.forEach(function(a,j){if(a.id===rg.getAttribute('data-id'))i=j;});if(i<0||!rg.style)return;var R=Math.round(LM.items[i].r*k);rg.style.width=rg.style.height=(2*R)+'px';rg.style.transform='translate('+Math.round(P[i].x-R)+'px,'+Math.round(P[i].y-R)+'px)';});
926
+ (LM.cls||[]).forEach(function(c){var cl=LM.clusters[+c.getAttribute('data-c')],sx=0,top=1e9;cl.m.forEach(function(a){var i=LM.items.indexOf(a);sx+=P[i].x;top=Math.min(top,P[i].y-a.r*k);});if(c.style)c.style.transform='translate('+Math.round(sx/cl.m.length-(c.offsetWidth||0)/2)+'px,'+Math.round(top-30)+'px)';});
927
+ var tip=$('lmtip');if(tip&&LM.hot){var hi=LM.items.indexOf(LM.hot),hp=P[hi],tw=tip.offsetWidth||280,th=tip.offsetHeight||100,tx=Math.max(10,Math.min(w-tw-10,hp.x+LM.hot.r*k+16)),ty=Math.max(10,Math.min(h-th-10,hp.y-th/2));if(tx<hp.x&&hp.x+LM.hot.r*k+16+tw>w)tx=Math.max(10,hp.x-LM.hot.r*k-16-tw);tip.style.transform='translate('+Math.round(tx)+'px,'+Math.round(ty)+'px)';}
928
+ // faint lines for the weak links
929
+ var sv=$('lmlines');if(sv){var ln='';LM.edges.forEach(function(e){if(e.sim>=0.3)return;var a=P[LM.items.indexOf(e.a)],b=P[LM.items.indexOf(e.b)],hot=LM.hot&&(LM.hot===e.a||LM.hot===e.b);ln+="<line x1='"+a.x.toFixed(1)+"' y1='"+a.y.toFixed(1)+"' x2='"+b.x.toFixed(1)+"' y2='"+b.y.toFixed(1)+"' class='"+(hot?'hot':'')+"'/>";});sv.innerHTML=ln;}
930
+ var G=LM.G;if(!G)return;var gl=G.gl,d=Math.min(window.devicePixelRatio||1,2),W=Math.max(1,Math.floor(w*d)),H=Math.max(1,Math.floor(h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
931
+ var o=G.out;for(var z=0;z<o.length;z++)o[z]=0;var n=0,put=function(x,y,r,a){if(n>=64||r<=0)return;o[n*4]=x/w;o[n*4+1]=y/h;o[n*4+2]=r;o[n*4+3]=a||0;n++;};
932
+ LM.items.forEach(function(a,i){if(!a.fold)put(P[i].x,P[i].y,a.r*k*(LM.hot===a?1.08:1),0);});
933
+ // tendrils: a chain of small drops, close enough to always bridge, for the middling links
934
+ LM.edges.forEach(function(e){if(e.sim<0.3||e.a.fold||e.b.fold)return;var A=P[LM.items.indexOf(e.a)],B=P[LM.items.indexOf(e.b)],dx=B.x-A.x,dy=B.y-A.y,D=Math.sqrt(dx*dx+dy*dy)||1,gap=D-(e.a.r+e.b.r)*k;if(gap<=4)return;var m=Math.min(20,Math.ceil(gap/8)),sp=gap/(m+1),rr=Math.max(1.8,sp*0.4);for(var c=1;c<=m;c++){var f=(e.a.r*k+sp*c)/D;put(A.x+dx*f,A.y+dy*f,rr,0);}});
935
+ var th=LQ.theme||{},U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,0,0,-10);gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.uContrast,th.contrast?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);gl.drawArrays(gl.TRIANGLES,0,3);}
936
+
937
+ // The write-ups as a document: # headings, - bullets, **bold**; everything escaped first.
938
+ function mdLite(t){var out=[],list=false;String(t||'').split(String.fromCharCode(10)).forEach(function(line){var l=line.trim(),b=function(x){var p=esc(x).split('**');return p.map(function(s,i){return i%2?'<b>'+s+'</b>':s;}).join('');};
939
+ if(l.indexOf('- ')===0||l.indexOf('* ')===0||l.indexOf('• ')===0){if(!list){out.push('<ul>');list=true;}out.push('<li>'+b(l.slice(2))+'</li>');return;}
940
+ if(list){out.push('</ul>');list=false;}if(!l){return;}var h=0;while(l.charAt(h)==='#')h++;if(h&&l.charAt(h)===' '){out.push('<h'+Math.min(4,h+2)+'>'+b(l.slice(h+1))+'</h'+Math.min(4,h+2)+'>');return;}
941
+ out.push('<p>'+b(l)+'</p>');});if(list)out.push('</ul>');return out.join('');}
942
+ function wuHead(){var t=$('wutitle'),u=$('wusub');if(!t||!u)return;var c=typeof current==='string'?current:'week',d=new Date(),f=function(x){return x.toLocaleDateString(undefined,{day:'numeric',month:'short'});};
943
+ if(c==='standup'){t.textContent='Your standup';u.textContent='Yesterday and today, from your local git';}else if(c==='todo'){t.textContent='Still on your plate';u.textContent='What your repos say is unfinished';}else{var a=new Date(d.getTime()-6*86400000);t.textContent='Your week';u.textContent=f(a)+' to '+f(d)+', from your local git';}}
944
+ function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});var base=lmTilde(g.stats.base)||"your home folder";
621
945
  if(g.stats.noStorage)return "Symbiot can't see your phone's files yet, so it can't find your projects. Android calls this <b>All files access</b>."+(window.SymbiotAndroid?" <button class='act' id='allowfiles'>Allow file access</button>":"");
622
946
  if(!repos.length)return "No git repositories found under "+esc(base)+" yet."+(g.stats.android?" Projects in Termux's home folder (<code>~</code>) are private to Termux, so no other app can see them."+(phoneTermux().installed?" Symbiot running in Termux can, and this app can show it: <button class='act' id='usetermux'>Open Termux</button> copies the command that starts it. Paste it there, and Termux opens it here. Or keep":" Keep")+" your projects in shared storage: in Termux, run <code>termux-setup-storage</code> and work under <code>~/storage/shared</code>.":"");var langs=g.nodes.filter(function(n){return n.type==="lang";}).map(function(n){return n.label;});var top=repos.slice().sort(function(a,b){return (b.meta.commits||0)-(a.meta.commits||0);})[0];var more=(g.stats.roots||[]).filter(function(r){return r!==base;}).length;var s="Scanned <b>"+esc(base)+"</b>"+(more?" and "+more+" more folder"+(more>1?"s":""):"")+" &middot;<b>"+g.stats.repos+"</b> repos &middot; <b>"+g.stats.commits+"</b> of your commits &middot; <b>"+(g.stats.files||0)+"</b> files &middot; ";s+=langs.length?("mostly <b>"+esc(langs.slice(0,3).join(", "))+"</b>"):"no languages detected";if(top)s+=" &middot; most active: <b>"+esc(top.label)+"</b>";return s;}
623
947
  function nodeById(id){for(var i=0;i<GRAPH.nodes.length;i++)if(GRAPH.nodes[i].id===id)return GRAPH.nodes[i];return null;}
@@ -629,7 +953,7 @@ function agentBadge(d){if(!d.agents||!d.agents.length)return'';return d.agents.m
629
953
  function showDetail(d){var el=document.getElementById('detail');el.classList.remove('hidden');
630
954
  if(d.error){el.innerHTML="<h3>&mdash;</h3><div class='k'>"+esc(d.error)+"</div>";return;}
631
955
  if(d.type==='repo'){var chips="";['branch: '+(d.branch||'?'),d.commits+' commits',(d.dirty?d.dirty+' uncommitted':'clean'),(d.last?'last '+d.last:'')].concat(d.langs||[]).concat(d.tools||[]).forEach(function(x){if(x)chips+="<span class='tag'>"+esc(x)+"</span>";});
632
- el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div>"+agentBadge(d)+"<button class='act' id='suggest' style='margin-top:12px'>Suggest next steps</button><div id='sugout'></div>";
956
+ el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div>"+lmWeeks(d)+lmNearHtml('repo:'+d.path)+agentBadge(d)+"<button class='act' id='suggest' style='margin-top:12px'>Suggest next steps</button><div id='sugout'></div>";lmNearWire(el);
633
957
  document.getElementById('suggest').addEventListener('click',function(){var o=document.getElementById('sugout');o.innerHTML="<div class='out2'>Thinking...</div>";api('/api/suggest',{path:d.path}).then(function(r){if(r.error==='not-connected'){o.innerHTML="<div class='out2'>Connect a model in Settings to get suggestions - Ollama is free and runs locally.</div>";return;}o.innerHTML="<div class='out2'>"+esc(r.text||'(no output)')+"</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");});});return;}
634
958
  if(d.type==='lang'||d.type==='tool'){var lis=(d.repos||[]).map(function(r){return "<li>"+esc(r)+"</li>";}).join("");el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Used in "+((d.repos||[]).length)+" repos</div><ul>"+lis+"</ul>";return;}
635
959
  if(d.type==='agent'){var m=d.meta||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+(m.kind==='agent'?'AI coding agent you have installed':'editor you have installed')+"</div>"+(m.cmd?"<div class='out2' style='font-size:11px'>"+esc(m.cmd)+"</div>":"");return;}
@@ -668,7 +992,7 @@ var rm=row.querySelector('.rm');if(rm&&rm.classList.contains('restore'))rm.addEv
668
992
  else if(rm)rm.addEventListener('click',function(){api('/api/tasks/remove',{id:id}).then(function(){var nx=row.nextElementSibling;if(nx&&nx.classList.contains('tchat'))nx.remove();row.remove();});});});}
669
993
  // Per-task Q&A: the button opens a thread under the row; answers come from the
670
994
  // connected model, grounded in the task's repo (and its pending changes).
671
- function askBtn(t){var n=(t.chat||[]).length;return "<button class='ask"+(n?" has":"")+"' title='"+(n?"questions &amp; answers about this task":"ask a question about this task")+"'>&#128172;"+(n?" "+Math.ceil(n/2):"")+"</button>";}
995
+ function askBtn(t){var n=(t.chat||[]).length;return "<button class='ask"+(n?" has":"")+"' title='"+(n?"questions &amp; answers about this task":"ask a question about this task")+"'><i class=ic-chat></i>"+(n?" "+Math.ceil(n/2):"")+"</button>";}
672
996
  function taskById(id){var all=PENDTASKS.concat(ALLTASKS);for(var i=0;i<all.length;i++)if(all[i].id===id)return all[i];return null;}
673
997
  function stepsHtml(st){return st&&st.length?"<details class='steps'><summary>"+st.length+(st.length>1?' steps':' step')+" &middot; "+esc(String(st[0]).charAt(0).toLowerCase()+String(st[0]).slice(1))+"</summary><ul>"+st.map(function(x){return "<li>"+esc(x)+"</li>";}).join('')+"</ul></details>":"";}
674
998
  function chatMsgs(chat){return (chat||[]).map(function(m){return "<div class='msg "+(m.role==='user'?'q':'a')+"'>"+esc(m.text)+(m.role==='user'?'':stepsHtml(m.steps))+"</div>";}).join("");}
@@ -685,9 +1009,9 @@ msgs.insertAdjacentHTML('beforeend',chatMsgs([{role:'user',text:q}])+"<div class
685
1009
  api('/api/tasks/chat',{id:id,question:q}).then(function(r){send.disabled=false;var th=msgs.querySelector('.thinking');if(th)th.remove();
686
1010
  if(r.error==='not-connected'){msgs.insertAdjacentHTML('beforeend',"<div class='msg a'>Connect a model in Settings to ask questions - Ollama is free and runs locally.</div>");return;}
687
1011
  if(r.error){msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(r.error)+"</div>");return;}
688
- t.chat=r.chat||[];msgs.innerHTML=chatMsgs(t.chat);btn.className='ask on has';btn.innerHTML="&#128172; "+Math.ceil(t.chat.length/2);}).catch(function(e){send.disabled=false;var th=msgs.querySelector('.thinking');if(th)th.remove();msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(String((e&&e.message)||e))+"</div>");});}
1012
+ t.chat=r.chat||[];msgs.innerHTML=chatMsgs(t.chat);btn.className='ask on has';btn.innerHTML="<i class=ic-chat></i> "+Math.ceil(t.chat.length/2);}).catch(function(e){send.disabled=false;var th=msgs.querySelector('.thinking');if(th)th.remove();msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(String((e&&e.message)||e))+"</div>");});}
689
1013
  send.addEventListener('click',ask);inp.addEventListener('keydown',function(e){if(e.key==='Enter')ask();});
690
- box.querySelector('.askclear').addEventListener('click',function(){api('/api/tasks/chat/clear',{id:id}).then(function(){t.chat=[];msgs.innerHTML=CHATHINT;btn.className='ask on';btn.innerHTML='&#128172;';});});
1014
+ box.querySelector('.askclear').addEventListener('click',function(){api('/api/tasks/chat/clear',{id:id}).then(function(){t.chat=[];msgs.innerHTML=CHATHINT;btn.className='ask on';btn.innerHTML='<i class=ic-chat></i>';});});
691
1015
  inp.focus();}
692
1016
  function fchip(dim,val,label){var on=(TFILTER[dim]||'')===val;return "<button class='fchip"+(on?' on':'')+"' data-dim='"+dim+"' data-val='"+esc(val)+"'>"+esc(label)+"</button>";}
693
1017
  function renderFilter(){var box=document.getElementById('taskfilter');
@@ -695,7 +1019,7 @@ if(TARCH){box.innerHTML="<button class='fchip on' id='archtoggle'>&#8617; back t
695
1019
  var types={},repos={};ALLTASKS.forEach(function(t){types[t.type||'Features & other']=1;if(t.repo)repos[t.repo]=1;});
696
1020
  var fb="<span class='fl'>Type</span>"+fchip('type','','All')+Object.keys(types).sort(function(a,b){return TORDER.indexOf(a)-TORDER.indexOf(b);}).map(function(t){return fchip('type',t,t);}).join('');
697
1021
  fb+="<span class='fl'>Repo</span>"+fchip('repo','','All')+Object.keys(repos).sort().map(function(r){return fchip('repo',r,r);}).join('');
698
- fb+="<button class='fchip' id='archtoggle' style='margin-left:auto'>&#128451; archived</button>";
1022
+ fb+="<button class='fchip' id='archtoggle' style='margin-left:auto'><i class=ic-archive></i> archived</button>";
699
1023
  box.innerHTML=fb;
700
1024
  box.querySelectorAll('.fchip[data-dim]').forEach(function(c){c.addEventListener('click',function(){TFILTER[c.getAttribute('data-dim')]=c.getAttribute('data-val');renderTasks();});});
701
1025
  document.getElementById('archtoggle').addEventListener('click',loadArchived);}
@@ -779,12 +1103,12 @@ function relHtml(rl){return "<div class='qrel"+(rl.waiting?" wait":"")+"'>Needs
779
1103
  (rl.waiting==='npm'?" &middot; not published yet, so this can't be done yet":rl.waiting==='install'?" &middot; "+(rl.name==='symbiot'?"update Symbiot first":"install it first"):"")+"</div>";}
780
1104
  function askHtml(a){var k=a.ask;if(!k)return '';var qs=k.questions||[],ss=k.suggestions||[];if(!qs.length&&!ss.length)return '';
781
1105
  var h="<div class='aq' data-id='"+esc(a.id)+"'>";
782
- if(qs.length){h+="<h4>&#10067; "+qs.length+" question"+(qs.length>1?"s":"")+" for you</h4>";
1106
+ if(qs.length){h+="<h4><i class=ic-ask></i> "+qs.length+" question"+(qs.length>1?"s":"")+" for you</h4>";
783
1107
  qs.forEach(function(q,i){var rl=q.release;h+="<div class='q' data-i='"+i+"'><div class='qt'>"+esc(q.q)+"</div>"+(q.context?"<div class='qc'>"+esc(q.context)+"</div>":"")+(rl?relHtml(rl):"");
784
1108
  (q.options||[]).forEach(function(o,j){var off=rl&&rl.waiting&&DONEOPT.test(o);h+="<label class='opt"+(off?" off":"")+"'><input type='radio' name='q_"+esc(a.id)+"_"+i+"' value='"+j+"'"+(off?" disabled":"")+"><span>"+whoHtml(o)+(off?" <i>(once "+esc(rl.name+" "+rl.needs)+" is installed)</i>":"")+"</span></label>";});
785
1109
  h+="<input class='qother' placeholder='"+((q.options&&q.options.length)?"or answer in your own words":"your answer")+"'></div>";});
786
1110
  h+="<div class='row'><button class='act qsend' title='save the answers and hand the repo back to your agent'>Send answers &amp; continue</button><button class='ghost qsave' title='save the answers for the next run'>Save only</button></div>";}
787
- if(ss.length){var shown=IDEASOPEN[a.path]?ss.length:(k.ideasShown||ss.length);h+="<h4>&#128161; Ideas from the agent</h4>";ss.forEach(function(s,i){if(i>=shown)return;h+="<div class='idea'><span style='flex:1'>"+esc(s.text)+(s.other?" <span class='tag' title='this idea is for another project, so + task adds it to that one'>for "+esc(s.repo)+"</span>":"")+"</span>"+(s.added?"<span class='tag'>in Tasks</span>":"<button class='ghost qidea' data-i='"+i+"' style='padding:3px 9px;font-size:12px'>+ task</button><button class='ghost qskip' data-i='"+i+"' title='turn this idea down: the next one moves up, and the agent is told not to suggest it again' style='padding:3px 9px;font-size:12px'>Skip</button>")+"</div>";});
1111
+ if(ss.length){var shown=IDEASOPEN[a.path]?ss.length:(k.ideasShown||ss.length);h+="<h4><i class=ic-idea></i> Ideas from the agent</h4>";ss.forEach(function(s,i){if(i>=shown)return;h+="<div class='idea'><span style='flex:1'>"+esc(s.text)+(s.other?" <span class='tag' title='this idea is for another project, so + task adds it to that one'>for "+esc(s.repo)+"</span>":"")+"</span>"+(s.added?"<span class='tag'>in Tasks</span>":"<button class='ghost qidea' data-i='"+i+"' style='padding:3px 9px;font-size:12px'>+ task</button><button class='ghost qskip' data-i='"+i+"' title='turn this idea down: the next one moves up, and the agent is told not to suggest it again' style='padding:3px 9px;font-size:12px'>Skip</button>")+"</div>";});
788
1112
  if(ss.length>shown)h+="<div class='row'><button class='ghost qmore' title='the agent ranks its ideas best first: weigh these two before adding more'>"+(ss.length-shown)+" more idea"+(ss.length-shown>1?"s":"")+", ranked lower</button></div>";}
789
1113
  return h+"</div>";}
790
1114
  var IDEASOPEN={};
@@ -799,7 +1123,7 @@ if(!ans.length){msg.innerHTML="<div class='note err'>Pick an option or type an a
799
1123
  box.querySelectorAll('button').forEach(function(b){b.disabled=true;});
800
1124
  api('/api/agents/answer',{path:a.path,answers:ans,rerun:rerun}).then(function(r){
801
1125
  if(!r||r.error){box.querySelectorAll('button').forEach(function(b){b.disabled=false;});msg.innerHTML="<div class='note err'>"+esc((r&&r.error)||'failed')+"</div>";return;}
802
- msg.innerHTML="<div class='note ok'>&#10003; Saved "+r.saved+" answer"+(r.saved>1?"s":"")+" for <b>"+esc(a.name)+"</b> in .symbiot/ANSWERS.md"+(r.rerun?" &middot; your agent is picking them up now.":".")+(r.yours&&r.yours.length?"<div class='err'>&#128100; Still yours to do: "+esc(r.yours.join(' '))+"</div>":"")+(r.note?"<div class='muted'>"+esc(r.note)+"</div>":"")+"</div>";loadAgents();});}
1126
+ msg.innerHTML="<div class='note ok'>&#10003; Saved "+r.saved+" answer"+(r.saved>1?"s":"")+" for <b>"+esc(a.name)+"</b> in .symbiot/ANSWERS.md"+(r.rerun?" &middot; your agent is picking them up now.":".")+(r.yours&&r.yours.length?"<div class='err'><i class=ic-person></i> Still yours to do: "+esc(r.yours.join(' '))+"</div>":"")+(r.note?"<div class='muted'>"+esc(r.note)+"</div>":"")+"</div>";loadAgents();});}
803
1127
  var s1=box.querySelector('.qsend'),s2=box.querySelector('.qsave');
804
1128
  if(s1)s1.addEventListener('click',function(){send(true);});if(s2)s2.addEventListener('click',function(){send(false);});
805
1129
  var more=box.querySelector('.qmore');if(more)more.addEventListener('click',function(){IDEASOPEN[a.path]=true;loadAgents();});
@@ -810,7 +1134,7 @@ box.querySelectorAll('.qskip').forEach(function(btn){btn.addEventListener('click
810
1134
  api('/api/agents/skip',{path:a.path,text:s.text}).then(function(r){if(!r||r.error){btn.disabled=false;document.getElementById('agentsmsg').innerHTML="<div class='note err'>"+esc((r&&r.error)||'failed')+"</div>";return;}loadAgents();});});});});}
811
1135
  // A step of yours an answer picked (👤 You): the next run waits for it (agents.mjs waitingFor)
812
1136
  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";
813
- return "<div class='aq'><h4>&#9208; Waiting on your step</h4><div class='qt'>&#128100; "+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>";}
1137
+ 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>";}
814
1138
  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;
815
1139
  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>";});});});}
816
1140
  function answering(){var f=document.activeElement;return !!(f&&f.closest&&f.closest('.aq'));}
@@ -859,8 +1183,8 @@ if(a.waiting)st+=" &middot; <span style='color:var(--amber)'>waiting on your ste
859
1183
  if(nQs(a))st+=" &middot; <span style='color:var(--amber)'>needs your answers</span>";
860
1184
  if(a.held!=null){var hn=a.held.length,ht=a.held.map(function(t){return "&bull; "+esc(t).replace(/'/g,'&#39;');}).join('&#10;');
861
1185
  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.");
862
- st+=" &middot; <span style='color:var(--amber);cursor:help' title='"+ht+"'>&#9208; "+(hn?hn+" task"+(hn>1?"s":"")+" held":"tasks held")+"</span>";}
863
- var b="<div class='dh'><span class='orb "+cls+"'></span><span class='dn'>"+esc(a.name)+"</span><span class='dd'>"+st+"</span></div>";
1186
+ st+=" &middot; <span style='color:var(--amber);cursor:help' title='"+ht+"'><i class=ic-pause></i> "+(hn?hn+" task"+(hn>1?"s":"")+" held":"tasks held")+"</span>";}
1187
+ 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></div>";
864
1188
  b+=workHtml(a);
865
1189
  var ch=a.changed;
866
1190
  if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
@@ -883,8 +1207,8 @@ var ci=document.getElementById('driftci').checked?'1':'0';var ft=document.getEle
883
1207
  api('/api/drift?ci='+ci+'&fetch='+ft).then(function(d){driftLoaded=true;var repos=d.repos||[];var risky=repos.filter(function(r){return r.flags.some(function(f){return f.level==='warn';});});
884
1208
  var h="<div class='k' style='margin:10px 0'><b>"+repos.length+"</b> repos &middot; <b>"+risky.length+"</b> with risks"+(d.partial?" &middot; <span class='err'>partial &mdash; the scan hit its time limit</span>":"")+"</div>";
885
1209
  repos.forEach(function(r){if(!r.flags.length)return;var warn=r.flags.some(function(f){return f.level==='warn';});
886
- h+="<div class='drift'><div class='dh'><span class='"+(warn?'dot-w':'dot-c')+"'></span><span class='dn'>"+esc(r.name)+"</span><span class='dd'>"+esc(r.def)+(r.fetchAgeDays!=null&&r.fetchAgeDays>3?" &middot; fetch "+r.fetchAgeDays+"d old":"")+"</span></div><ul>";
887
- r.flags.forEach(function(f){h+="<li class='"+esc(f.level)+"'>"+(f.level==='warn'?'&#9888; ':'&middot; ')+esc(f.text)+(f.evidence?" <span class='ev'>["+esc(f.evidence)+"]</span>":"")+"</li>";});
1210
+ h+="<div class='drift"+(warn?' risk':'')+"'><div class='dh'><span class='dbead' aria-hidden='true'></span><span class='dn'>"+esc(r.name)+"</span><span class='dd'>"+esc(r.def)+(r.fetchAgeDays!=null&&r.fetchAgeDays>3?" &middot; fetch "+r.fetchAgeDays+"d old":"")+"</span></div><ul>";
1211
+ r.flags.forEach(function(f){h+="<li class='"+esc(f.level)+"'>"+esc(f.text)+(f.evidence?" <span class='ev'>["+esc(f.evidence)+"]</span>":"")+"</li>";});
888
1212
  h+="</ul></div>";});
889
1213
  var clean=repos.filter(function(r){return !r.flags.length;}).map(function(r){return r.name;});
890
1214
  if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clean.join(", "))+"</div>";
@@ -897,9 +1221,9 @@ o.innerHTML="<div class='muted' style='margin-top:10px'><span class='dot-c' styl
897
1221
  api('/api/tasks/push',{type:TFILTER.type||'',repo:TFILTER.repo||''}).then(function(r){
898
1222
  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;}
899
1223
  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>";
900
- 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'>&#9208; held: an agent is still running there, so these land when it finishes</span>":"")+"</li>";});h+="</ul>";}
901
- 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'>&#9888; 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>";}
902
- 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="&#9208; <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>.":"&#9888; <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="&#9888; <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'>&#129302; 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);}
1224
+ 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>";}
1225
+ 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>";}
1226
+ 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);}
903
1227
  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>";}
904
1228
  h+="</div>";
905
1229
  o.innerHTML=h;
@@ -912,9 +1236,9 @@ function linkGap(){var off=LINKCONN.filter(function(x){return !x.ready;});if(!of
912
1236
  return "&#9993;&#65039; "+n+(one?" is":" are")+" linked in Symbiot, but not "+(off.some(function(x){return x.connector;})?"ready ":"")+"as a Claude connector, so your agent&#39;s runs have no tools for "+(one?"it":"them")+": linking a site here signs Symbiot&#39;s browser in, not Claude. Connect "+n+" in claude.ai &rarr; Settings &rarr; Connectors, and the next run gets "+(one?"it":"them")+" by itself. Until then a run sees only what&#39;s new there (Watch).";}
913
1237
  function renderConnectors(){var el=document.getElementById('agentconnectors'),gap=linkGap();if(!CONNECTORS.length){el.innerHTML=/^\\s*claude\\b/.test(document.getElementById('agentcmd').value)?gap:'';return;}
914
1238
  var nm=function(c){return "<b>"+esc(c.name.replace(/^claude\\.ai\\s+/i,''))+"</b>";},ready=CONNECTORS.filter(function(c){return c.ready;}),wait=CONNECTORS.filter(function(c){return !c.ready;});
915
- var cmd=document.getElementById('agentcmd').value;if(/orca-ide/.test(cmd)&&/claude/.test(cmd)){el.innerHTML="&#128268; Claude in Orca&#39;s tab asks you before it uses your connectors ("+CONNECTORS.map(nm).join(", ")+").";return;}
916
- if(!/^\\s*claude\\b/.test(cmd)){el.innerHTML="&#128268; Your Claude connectors ("+CONNECTORS.map(nm).join(", ")+") reach Claude Code runs only. This command isn&#39;t Claude, so its runs can&#39;t use them.";return;}
917
- el.innerHTML=(ready.length?"&#128268; Your agent&#39;s runs can use "+ready.map(nm).join(", ")+": linked to Claude, so each run allows their tools.":"")+(wait.length?(ready.length?" ":"&#128268; ")+wait.map(nm).join(", ")+(wait.length===1?" needs":" need")+" authorizing in your claude.ai connector settings first.":"")+(gap?" "+gap:"");}
1239
+ var cmd=document.getElementById('agentcmd').value;if(/orca-ide/.test(cmd)&&/claude/.test(cmd)){el.innerHTML="<i class=ic-plug></i> Claude in Orca&#39;s tab asks you before it uses your connectors ("+CONNECTORS.map(nm).join(", ")+").";return;}
1240
+ if(!/^\\s*claude\\b/.test(cmd)){el.innerHTML="<i class=ic-plug></i> Your Claude connectors ("+CONNECTORS.map(nm).join(", ")+") reach Claude Code runs only. This command isn&#39;t Claude, so its runs can&#39;t use them.";return;}
1241
+ el.innerHTML=(ready.length?"<i class=ic-plug></i> Your agent&#39;s runs can use "+ready.map(nm).join(", ")+": linked to Claude, so each run allows their tools.":"")+(wait.length?(ready.length?" ":"<i class=ic-plug></i> ")+wait.map(nm).join(", ")+(wait.length===1?" needs":" need")+" authorizing in your claude.ai connector settings first.":"")+(gap?" "+gap:"");}
918
1242
  function loadAgentCfg(){api('/api/agentcfg').then(function(d){document.getElementById('agentcmd').value=d.cmd||'';
919
1243
  CONNECTORS=(d.connectors&&d.connectors.list)||[];LINKCONN=(d.connectors&&d.connectors.links)||[];renderConnectors();
920
1244
  var chips=[];(d.agents||[]).forEach(function(a){chips.push(a);});(d.editors||[]).forEach(function(e){chips.push(e);});
@@ -969,20 +1293,20 @@ var pf=$('pforget');if(pf)pf.addEventListener('click',function(){if(typeof confi
969
1293
  // linked, amber waiting for you to sign in (or signed out), green linked. Shown
970
1294
  // in Settings, and on the Dashboard until something is linked.
971
1295
  var LINKS=null;
972
- var LINKWORD={ok:'linked',signin:'sign in, then close the window',signedout:'signed out',error:'needs a fix'};
1296
+ var LINKWORD={ok:'linked',signin:'sign in, then close it',signedout:'signed out',error:'needs a fix'};
973
1297
  function escQ(t){return esc(t).replace(/'/g,'&#39;');}
974
- function linksHtml(d,board){var h=board?"<div class='note muted'>Link your work to see what arrives there here, and in Week and Standup:</div>":"";
1298
+ // Connections: a column per kind of site; each site a bead in the look: solid when
1299
+ // linked, amber when it wants you to sign in, hollow when it isn't linked. Link,
1300
+ // check (↻) and Unlink show on hover (always on touch).
1301
+ 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>";
975
1302
  if(d.error)h+="<div class='note err'>"+esc(d.error)+"</div>";
976
- d.groups.forEach(function(g){h+="<div class='lgroup'><span class='fl'>"+esc(g)+"</span>";
977
- d.items.filter(function(x){return x.group===g;}).forEach(function(it){var on=it.state!=='off';
978
- h+="<span class='lnk "+escQ(it.state)+"' data-id='"+escQ(it.id)+"'><button class='fchip lbtn' title='"+escQ(it.note||(on?'open '+it.url+' to sign in again':'sign in at '+it.url+' and link it'))+"'><span class='dot-c' style='background:"+(it.state==='ok'?'var(--green)':on?'var(--amber)':'var(--line)')+"'></span>"+esc(it.name)+"</button>"+
979
- (on?"<span class='lst'>"+esc(LINKWORD[it.state]||'')+"</span><button class='lmore' title='check it now'>&#8635;</button><button class='lrm' title='unlink: stop watching it and stop trusting it'>&times;</button>":"")+"</span>";});
980
- h+="</div>";});return h;}
1303
+ 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';
1304
+ 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>"+
1305
+ (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;}
981
1306
  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;}
982
1307
  function renderLinks(){if(!LINKS)return;['links','boardlinks'].forEach(function(id){var el=document.getElementById(id);if(!el)return;
983
- if(id==='boardlinks'&&LINKS.linked){el.innerHTML='';return;}
984
1308
  el.innerHTML=linksHtml(LINKS,id==='boardlinks');
985
- el.querySelectorAll('.lnk').forEach(function(sp){var lid=sp.getAttribute('data-id'),name=sp.querySelector('.lbtn').textContent;
1309
+ el.querySelectorAll('.lnk').forEach(function(sp){var lid=sp.getAttribute('data-id'),nm=sp.querySelector('.cxn'),name=nm?nm.textContent:sp.querySelector('.lbtn').textContent;
986
1310
  sp.querySelector('.lbtn').addEventListener('click',function(){linksOut("<div class='note muted'>Opening "+esc(name)+"&hellip;</div>");
987
1311
  api('/api/links/link',{id:lid}).then(function(r){if(!r||r.error){linksOut("<div class='note err'>"+esc(name)+": "+esc((r&&r.error)||'failed')+"</div>");loadLinks();return;}
988
1312
  linksOut("<div class='note ok'>Opened "+esc(name)+" in Symbiot's browser. Sign in there as you normally do, then close that window: Symbiot checks it within a minute.</div>");loadLinks();
@@ -1044,7 +1368,7 @@ function scanLine(s){return 'Mapping your work… '+s.phase+(s.total?' '+s.done+
1044
1368
  function loadMap(){var p=document.getElementById("profile");p.textContent="Mapping your work...";document.getElementById("graph").innerHTML="";sel=null;hideDetail();hideReview();var done=false;
1045
1369
  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(){});}
1046
1370
  setTimeout(poll,400);
1047
- 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);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();});}
1371
+ 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();});}
1048
1372
  // The Android app calls this when you come back having allowed file access, so
1049
1373
  // the map rescans instead of staying empty.
1050
1374
  window.symbiotStorageGranted=function(){if(current==='map')loadMap();else mapLoaded=false;};
@@ -1158,14 +1482,15 @@ api('/api/watch/open-chat',{id:n.id}).then(function(x){b.disabled=false;if(!x||x
1158
1482
  say('ok',"&#10003; Opened WhatsApp in Symbiot's browser, in a window of its own. The chat shows your agent's reply in its message box: read it, change it if you like, and send it there. Close the window when you're done, so Watch can read WhatsApp again.");}).catch(function(e){b.disabled=false;say('err',esc(String((e&&e.message)||e)));});}
1159
1483
  var DRAFTING="&#10003; Your agent is drafting a reply to it. It opens the email in your inbox through Screens, writes the reply and leaves it in Drafts for you to read and send: it never presses Send. Follow it, and answer anything it asks, in the Agents tab.";
1160
1484
  // ---- Dashboard: a card per page you watch (your inbox, GitHub, WhatsApp…), what's new on each ----
1161
- var BOARD=null;var BOARDICON={mail:'&#9993;&#65039;',github:'&#128276;',chat:'&#128172;',page:'&#127760;'};
1485
+ var BOARD=null;
1162
1486
  // a chat's line says who its last message is from (watch.mjs fromOf): only unread ones count as waiting on you
1163
1487
  var CHATFROM={them:'unread from them',you:'you sent the last one',unknown:'nothing unread, maybe yours'};
1164
1488
  // Talk it over (watch.mjs boardChat): a chat on a card, open by its id, with what's half typed kept across a refresh
1165
1489
  var BTALK={},BTALKDRAFT={},BTALKBUSY=0;
1166
1490
  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>";
1167
1491
  function talking(){var f=document.activeElement;return BTALKBUSY>0||!!(f&&f.closest&&f.closest('.btalk'));}
1168
- function loadBoard(){api('/api/watch/board?hours='+(+$('boardhours').value||24)).then(function(b){if(b&&b.cards){BOARD=b;if(!talking())renderBoard();}});}
1492
+ var BOARDH=24;
1493
+ function loadBoard(){api('/api/watch/board?hours='+BOARDH).then(function(b){if(b&&b.cards){BOARD=b;if(!talking())renderBoard();}});}
1169
1494
  function boardMsg(cls,html){$('boardmsg').innerHTML="<div class='note "+cls+"'>"+html+"</div>";}
1170
1495
 
1171
1496
  // ---- the Dashboard as a stream ----------------------------------------------------
@@ -1175,38 +1500,41 @@ function boardMsg(cls,html){$('boardmsg').innerHTML="<div class='note "+cls+"'>"
1175
1500
  // the list below, wired by renderBoard's wire); a feed's name carries its Seen,
1176
1501
  // Talk and Check now. Drawn in the look's liquid, with its own canvas.
1177
1502
  var BS={G:null,beads:[],hot:null,on:false,b:null},BWIRE=null;
1503
+ // A feed's short name: "Inbox - you@x.com - x Mail" reads as Inbox, with the address under it.
1504
+ function bsName(c){var parts=String(c.name||'').split(' - '),acct=parts.filter(function(x){return x.indexOf('@')>=0;})[0]||'';return {n:parts[0]||c.name||'',sub:acct};}
1178
1505
  function bsGeom(){var el=$('bstream'),w=(el&&el.clientWidth)||900,n=((BS.b&&BS.b.cards)||[]).length,narrow=w<720;
1179
- return {w:w,h:Math.max(280,70+n*96+40),x0:narrow?120:200,x1:w-(narrow?28:270),narrow:narrow,n:n};}
1506
+ return {w:w,h:Math.max(300,80+n*118+44),x0:narrow?130:220,x1:w-(narrow?36:280),narrow:narrow,n:n};}
1180
1507
  function bsBuild(b){BS.b=b;var ov=$('bsov'),el=$('bstream');if(!ov||!el)return;var cs=b.cards||[];if(!cs.length){ov.innerHTML='';return;}
1181
- var g=bsGeom();if(el.style)el.style.height=g.h+'px';var now=Date.now(),H=(b.hours||24)*3600000,laneY=function(i){return 60+(g.h-110)*(i+0.5)/Math.max(1,g.n);};
1508
+ var g=bsGeom();BS.gw=g.w;if(el.style)el.style.height=g.h+'px';var now=Date.now(),H=(b.hours||24)*3600000,laneY=function(i){return 60+(g.h-110)*(i+0.5)/Math.max(1,g.n);};
1182
1509
  var html='';cs.forEach(function(c,i){var y=laneY(i);
1183
- html+="<div class='bline' style='left:"+(g.x0-24)+"px;width:"+(g.x1-g.x0+48)+"px;top:"+Math.round(y)+"px'></div>";
1184
- html+="<div class='bcard blane' data-i='"+i+"' style='top:"+Math.round(y)+"px'><div class='bln' title='"+esc(c.url||'')+"'>"+esc(c.name)+"</div><div class='blc'>"+(c.count?"<b>"+esc(c.label)+"</b>":"nothing new")+"</div><div class='bla'>"+(c.count?"<button class='bseen' title='set this feed back to 0: only what comes in after counts'>Seen</button>":"")+"<button class='btalkbtn' title='talk what&#39;s new here over with your AI before you draft a reply'>Talk"+((c.chat||[]).length?" "+Math.ceil(c.chat.length/2):"")+"</button><button class='bcheck' title='read it now'>Check now</button></div>"+(c.error?"<div class='ble'>"+esc(c.error)+"</div>":"")+"</div>";
1510
+ html+="<div class='bline' style='left:"+(g.x0-24)+"px;width:"+(g.x1-g.x0+24)+"px;top:"+Math.round(y)+"px'></div>";
1511
+ var nm=bsName(c);html+="<div class='bcard blane' data-i='"+i+"' style='top:"+Math.round(y)+"px'><div class='bln' title='"+esc(c.name+(c.url?' \u00b7 '+c.url:''))+"'>"+esc(nm.n)+"</div>"+(nm.sub?"<div class='blsub'>"+esc(nm.sub)+"</div>":"")+"<div class='blc' title='"+(c.checked?"read "+agoTxt(c.checked):"")+"'>"+(c.count?"<b>"+esc(c.label)+"</b>":"quiet")+"</div><div class='bla'>"+(c.count?"<button class='bseen' title='set this feed back to 0: only what comes in after counts'>Seen</button>":"")+"<button class='btalkbtn' title='talk what&#39;s new here over with your AI before you draft a reply'>Talk"+((c.chat||[]).length?" "+Math.ceil(c.chat.length/2):"")+"</button><button class='bcheck' title='read it now'>Check now</button></div>"+(c.error?"<div class='ble'>"+esc(c.error)+"</div>":"")+"</div>";
1185
1512
  if(c.brief&&!g.narrow)html+="<div class='blb' style='top:"+Math.round(y)+"px'>"+esc(c.brief.text.length>150?c.brief.text.slice(0,149)+'…':c.brief.text)+"</div>";});
1186
1513
  var marks=b.hours===24?[18,12,6]:b.hours===72?[48,24]:[120,72,24];
1187
1514
  marks.forEach(function(m){html+="<span class='btick' style='left:"+Math.round(g.x0+(1-m/(b.hours||24))*(g.x1-g.x0))+"px'>"+(m<48?m+'h ago':Math.round(m/24)+' days ago')+"</span>";});
1188
1515
  html+="<span class='btick' style='left:"+g.x1+"px'>now</span><div class='bnow' style='left:"+g.x1+"px'></div>";
1189
1516
  var old={};BS.beads.forEach(function(x){old[x.key]=x;});BS.beads=[];
1190
1517
  cs.forEach(function(c,i){(c.items||[]).forEach(function(n,j){var age=Math.max(0,now-(n.ts||now));if(age>H)return;var key=c.id+':'+(n.id||j),o=old[key];
1191
- BS.beads.push({key:key,i:i,j:j,n:n,need:!!n.need&&!!(n.mail||n.chat),tx:g.x0+(1-age/H)*(g.x1-g.x0),ty:laneY(i),tr:n.need&&(n.mail||n.chat)?13:n.read?6:9,x:o?o.x:g.x1,y:o?o.y:laneY(i),r:o?o.r:0});});});
1518
+ BS.beads.push({key:key,i:i,j:j,n:n,need:!!n.need&&!!(n.mail||n.chat),tx:g.x0+(1-age/H)*(g.x1-g.x0),ty:laneY(i),tr:n.need&&(n.mail||n.chat)?13:n.read?6:9,x:o?o.x:g.x1,y:o?o.y:laneY(i),r:o?o.r:0});});
1519
+ (c.past||[]).forEach(function(n,j){var age=Math.max(0,now-(n.ts||now));if(age>H)return;var key=c.id+':p:'+(n.id||j),o=old[key];BS.beads.push({key:key,i:i,j:-1-j,n:n,past:true,need:false,tx:g.x0+(1-age/H)*(g.x1-g.x0),ty:laneY(i),tr:4.5,x:o?o.x:g.x1,y:o?o.y:laneY(i),r:o?o.r:0});});});
1192
1520
  html+=BS.beads.map(function(x,k){return "<button type='button' class='bbead' data-k='"+k+"' aria-label='"+esc(x.n.text||'')+"'></button>";}).join('');
1193
1521
  html+="<div class='btip' role='dialog' aria-live='polite'></div>";ov.innerHTML=html;
1194
1522
  BS.btns=[];ov.querySelectorAll('.bbead').forEach(function(bt){BS.btns.push(bt);var k=+bt.getAttribute('data-k');bt.addEventListener('pointerenter',function(){bsShow(k);});bt.addEventListener('focus',function(){bsShow(k);});bt.addEventListener('click',function(){bsShow(k);});});
1195
1523
  if(el.addEventListener&&!BS.wired){BS.wired=true;el.addEventListener('pointerleave',function(){bsShow(null);});el.addEventListener('keydown',function(e){if(e&&e.key==='Escape')bsShow(null);});}
1196
1524
  BS.hot=null;bsStart();}
1197
1525
  function bsShow(k){var tip=document.querySelector('#bsov .btip');if(!tip)return;BS.hot=k;if(k==null||!BS.beads[k]){tip.className='btip';return;}
1198
- var x=BS.beads[k],c=BS.b.cards[x.i],n=x.n;
1199
- tip.innerHTML="<div class='bcard' data-i='"+x.i+"'><div class='bi' data-j='"+x.j+"'><div class='btf'>"+esc(n.name||c.name)+" <span>&middot; "+esc(c.name)+" &middot; "+agoTxt(n.ts)+(n.from&&CHATFROM[n.from]?" &middot; "+CHATFROM[n.from]:"")+(n.read?" &middot; read":"")+"</span></div><div class='btx'>"+esc(n.text||'')+"</div><div class='bta'>"+(n.mail||n.chat?"<button class='bdraft'>"+(n.drafted?"Drafted &middot; again":"Draft a reply")+"</button>":"")+(n.href?"<button class='bopen"+(n.mail||n.chat?" quiet":"")+"'>Open</button>":"")+(n.chat&&n.drafted?"<button class='bopenwa quiet'>Open in WhatsApp</button>":"")+"</div></div></div>";
1526
+ var x=BS.beads[k],c=BS.b.cards[x.i],n=x.n;if(x.past){tip.innerHTML="<div class='bcard'><div class='bi'><div class='btf'>"+esc(n.name||bsName(c).n)+" <span>&middot; "+esc(bsName(c).n)+" &middot; "+agoTxt(n.ts)+" &middot; seen</span></div><div class='btx' style='margin-bottom:0'>"+esc(n.text||'')+"</div></div></div>";tip.className='btip on';return;}
1527
+ tip.innerHTML="<div class='bcard' data-i='"+x.i+"'><div class='bi' data-j='"+x.j+"'><div class='btf'>"+esc(n.name||bsName(c).n)+" <span>&middot; "+esc(bsName(c).n)+" &middot; "+agoTxt(n.ts)+(n.from&&CHATFROM[n.from]?" &middot; "+CHATFROM[n.from]:"")+(n.read?" &middot; read":"")+"</span></div><div class='btx'>"+esc(n.text||'')+"</div><div class='bta'>"+(n.mail||n.chat?"<button class='bdraft'>"+(n.drafted?"Drafted &middot; again":"Draft a reply")+"</button>":"")+(n.href?"<button class='bopen"+(n.mail||n.chat?" quiet":"")+"'>Open</button>":"")+(n.chat&&n.drafted?"<button class='bopenwa quiet'>Open in WhatsApp</button>":"")+"</div></div></div>";
1200
1528
  tip.className='btip on';if(BWIRE)BWIRE(tip);}
1201
1529
  function bsStart(){if(BS.on)return;var raf=typeof window.requestAnimationFrame==='function'?function(f){return window.requestAnimationFrame(f);}:null;if(!raf)return;BS.on=true;
1202
1530
  var loop=function(){var p=$('panel-board'),el=$('bstream');if(!p||!el||!p.offsetParent||(el.classList&&el.classList.contains('none'))){BS.on=false;return;}bsFrame();raf(loop);};raf(loop);}
1203
- function bsFrame(){var el=$('bstream'),cv=$('bsc');if(!el||!cv)return;if(!BS.G)BS.G=lqGL(cv,40);var g=bsGeom(),t=LQ.t||0,still=LQ.theme&&LQ.theme.still;
1531
+ function bsFrame(){var el=$('bstream'),cv=$('bsc');if(!el||!cv)return;if(!BS.G)BS.G=lqGL(cv,40);var g=bsGeom();if(BS.b&&BS.gw!==g.w){bsBuild(BS.b);g=bsGeom();}var t=LQ.t||0,still=LQ.theme&&LQ.theme.still;
1204
1532
  BS.beads.forEach(function(x,k){var hot=BS.hot===k;x.x+=(x.tx-x.x)*0.12;x.y+=((x.ty+(still?0:Math.sin(t*0.8+k*1.3)*2))-x.y)*0.12;x.r+=((x.tr*(hot?1.35:1))-x.r)*0.15;
1205
1533
  var bt=BS.btns&&BS.btns[k];if(bt&&bt.style){var R=Math.max(14,x.r+6);bt.style.width=bt.style.height=(2*R)+'px';bt.style.transform='translate('+Math.round(x.x-R)+'px,'+Math.round(x.y-R)+'px)';}});
1206
1534
  var tip=document.querySelector('#bsov .btip');if(tip&&BS.hot!=null&&BS.beads[BS.hot]){var x=BS.beads[BS.hot],tw=tip.offsetWidth||300,th=tip.offsetHeight||120,tx=Math.max(12,Math.min(g.w-tw-12,x.x-tw/2)),ty=x.y-x.r-th-14;if(ty<8)ty=x.y+x.r+14;tip.style.transform='translate('+Math.round(tx)+'px,'+Math.round(ty)+'px)';}
1207
1535
  var G=BS.G;if(!G)return;var gl=G.gl,d=Math.min(window.devicePixelRatio||1,3),W=Math.max(1,Math.floor(g.w*d)),H=Math.max(1,Math.floor(g.h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
1208
1536
  var o=G.out;for(var i=0;i<o.length;i++)o[i]=0;var k=0,put=function(px,py,r,a){if(k>=40)return;o[k*4]=px/g.w;o[k*4+1]=py/g.h;o[k*4+2]=Math.max(0,r);o[k*4+3]=a;k++;};
1209
- (BS.b.cards||[]).forEach(function(c,i){put(g.x1+16,60+(g.h-110)*(i+0.5)/Math.max(1,g.n),c.count?9+2*Math.min(c.count,6):5,0);});
1537
+ (BS.b.cards||[]).forEach(function(c,i){put(g.x1,60+(g.h-110)*(i+0.5)/Math.max(1,g.n),c.count?9+2*Math.min(c.count,6):5,0);});
1210
1538
  BS.beads.forEach(function(x){put(x.x,x.y,x.r,x.need?1:0);});
1211
1539
  var th=LQ.theme||{},U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,0,0,-10);gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.uContrast,th.contrast?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);gl.drawArrays(gl.TRIANGLES,0,3);}
1212
1540
  function renderBoard(){var b=BOARD,cs=b.cards,el=$('board');
@@ -1214,12 +1542,13 @@ var tab=$('boardtab');if(tab)tab.textContent='Dashboard'+(b.total?' · '+b.total
1214
1542
  var bst=$('bstream');if(bst&&bst.classList)bst.classList.toggle('none',!cs.length);var bl=$('blist');if(bl&&!cs.length)bl.open=true;
1215
1543
  if(!cs.length){$('boardsum').textContent='Everything you watch, side by side.';
1216
1544
  el.innerHTML="<div style='grid-column:1/-1'><div class='note muted' style='margin-top:0'>Nothing watched yet. Map your inbox, a chat (web.whatsapp.com) or any page under <b>Screens</b> on the Map tab and click <b>Watch</b> on it, or click <b>Watch GitHub</b> there. Each one gets a card here with what's new on it.</div><div class='row' style='margin-top:8px'><button class='ghost' id='boardgo'>Go to Screens</button></div></div>";
1217
- $('boardgo').addEventListener('click',function(){setTab('map');var w=$('watchbox');if(w&&w.scrollIntoView)w.scrollIntoView({block:'center'});});return;}
1545
+ $('boardgo').addEventListener('click',function(){setTab('map');var sbx=$('screensbox');if(sbx)sbx.open=true;var w=$('watchbox');if(w&&w.scrollIntoView)w.scrollIntoView({block:'center'});});return;}
1218
1546
  var when=b.hours===24?'since yesterday':b.hours===72?'in the last 3 days':'in the last 7 days';
1547
+ var lastRead=0;cs.forEach(function(c){if(c.checked>lastRead)lastRead=c.checked;});var sub=$('boardsub');if(sub)sub.textContent=(lastRead?'Read '+agoTxt(lastRead)+' \u00b7 ':'')+cs.length+(cs.length===1?' feed':' feeds');
1219
1548
  var waiting=cs.filter(function(c){return c.count;}).map(function(c){return c.label+(c.source==='page'?' on '+c.name:'');});
1220
1549
  $('boardsum').innerHTML=waiting.length?"<b style='color:var(--bone)'>Waiting on you "+when+":</b> "+esc(waiting.join(', ')):"Nothing new "+when+" on the "+cs.length+" thing"+(cs.length===1?"":"s")+" you watch.";
1221
1550
  el.innerHTML=cs.map(function(c,i){var nt=Math.ceil(((c.chat||[]).length)/2);
1222
- return "<div class='bcard"+(c.count?" has":"")+"' data-i='"+i+"'><div class='bh'><span>"+(BOARDICON[c.source]||BOARDICON.page)+"</span><span class='bn' title='"+esc(c.url)+"'>"+esc(c.name)+"</span>"+(c.count?"<button class='ghost bseen' title='set this card back to 0: only what comes in after counts. It stays under Watching, and the other cards keep theirs' style='padding:4px 9px;font-size:12px'>Seen</button>":"")+"<button class='ghost btalkbtn' title='talk what&#39;s new here over with your AI before you draft a reply' style='padding:4px 9px;font-size:12px'>&#128172;"+(nt?" "+nt:"")+"</button><button class='ghost bcheck' title='read it now' style='padding:4px 9px;font-size:12px'>Check now</button></div>"
1551
+ return "<div class='bcard"+(c.count?" has":"")+"' data-i='"+i+"'><div class='bh'><span class='bn' title='"+esc(c.url)+"'>"+esc(bsName(c).n)+"</span>"+(c.count?"<button class='ghost bseen' title='set this card back to 0: only what comes in after counts. It stays under Watching, and the other cards keep theirs' style='padding:4px 9px;font-size:12px'>Seen</button>":"")+"<button class='ghost btalkbtn' title='talk what&#39;s new here over with your AI before you draft a reply' style='padding:4px 9px;font-size:12px'>Talk"+(nt?" "+nt:"")+"</button><button class='ghost bcheck' title='read it now' style='padding:4px 9px;font-size:12px'>Check now</button></div>"
1223
1552
  +"<div class='bc'>"+c.count+"</div><div class='bl'>"+esc(c.count?c.label.replace(/^\\d+ /,''):'nothing new')+" &middot; "+(c.checked?"read "+agoTxt(c.checked):c.last?"tried "+agoTxt(c.last):"first read within a minute")+(c.via==='gh'?" &middot; through gh":"")+"</div>"
1224
1553
  +(c.error?"<div class='note err'>"+esc(c.error)+"</div>":"")
1225
1554
  +(c.brief?"<div class='bbrief'>"+esc(c.brief.text)+"</div>":"")
@@ -1240,9 +1569,9 @@ function done(){BTALKBUSY=Math.max(0,BTALKBUSY-1);send.disabled=false;var th=msg
1240
1569
  api('/api/watch/chat',{id:c.id,question:q}).then(function(r){done();
1241
1570
  if(r.error==='not-connected'){msgs.insertAdjacentHTML('beforeend',"<div class='msg a'>Connect a model in Settings to talk it over - Ollama is free and runs locally.</div>");return;}
1242
1571
  if(r.error){msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(r.error)+"</div>");return;}
1243
- c.chat=r.chat||[];msgs.innerHTML=chatMsgs(c.chat);var tb=box.closest('.bcard').querySelector('.btalkbtn');if(tb)tb.innerHTML='&#128172; '+Math.ceil(c.chat.length/2);}).catch(function(e){done();msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(String((e&&e.message)||e))+"</div>");});}
1572
+ c.chat=r.chat||[];msgs.innerHTML=chatMsgs(c.chat);var tb=box.closest('.bcard').querySelector('.btalkbtn');if(tb)tb.innerHTML='<i class=ic-chat></i> '+Math.ceil(c.chat.length/2);}).catch(function(e){done();msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(String((e&&e.message)||e))+"</div>");});}
1244
1573
  send.addEventListener('click',ask);inp.addEventListener('keydown',function(e){if(e.key==='Enter')ask();});
1245
- box.querySelector('.talkclear').addEventListener('click',function(){api('/api/watch/chat/clear',{id:c.id}).then(function(){c.chat=[];msgs.innerHTML=TALKHINT;var tb=box.closest('.bcard').querySelector('.btalkbtn');if(tb)tb.innerHTML='&#128172;';});});});
1574
+ box.querySelector('.talkclear').addEventListener('click',function(){api('/api/watch/chat/clear',{id:c.id}).then(function(){c.chat=[];msgs.innerHTML=TALKHINT;var tb=box.closest('.bcard').querySelector('.btalkbtn');if(tb)tb.innerHTML='<i class=ic-chat></i>';});});});
1246
1575
  el.querySelectorAll('.bopenwa').forEach(function(btn){btn.addEventListener('click',function(){openChatUI(item(btn),btn,boardMsg);});});
1247
1576
  function item(btn){return card(btn).items[+btn.closest('.bi').getAttribute('data-j')];}
1248
1577
  el.querySelectorAll('.bcheck').forEach(function(btn){btn.addEventListener('click',function(){var c=card(btn);btn.disabled=true;btn.textContent='Reading…';
@@ -1253,7 +1582,8 @@ el.querySelectorAll('.bopen').forEach(function(btn){btn.addEventListener('click'
1253
1582
  el.querySelectorAll('.bdraft').forEach(function(btn){btn.addEventListener('click',function(){var n=item(btn);btn.disabled=true;
1254
1583
  api('/api/watch/draft',{id:n.id}).then(function(x){btn.disabled=false;if(!x||x.error){boardMsg('err',esc((x&&x.error)||'failed'));return;}boardMsg('ok',x.chat?DRAFTING_CHAT:DRAFTING);loadWatchUI();}).catch(function(e){btn.disabled=false;boardMsg('err',esc(String((e&&e.message)||e)));});});});}
1255
1584
  wire(el);wire($('boardtalk'));wire($('bsov'));BWIRE=wire;}
1256
- $('boardrefresh').addEventListener('click',loadBoard);$('boardhours').addEventListener('change',loadBoard);
1585
+ $('boardrefresh').addEventListener('click',loadBoard);
1586
+ var bw=$('boardwin');if(bw&&bw.querySelectorAll)bw.querySelectorAll('button').forEach(function(b){b.addEventListener('click',function(){BOARDH=+b.getAttribute('data-h')||24;bw.querySelectorAll('button').forEach(function(x){x.setAttribute('aria-checked',x===b?'true':'false');});loadBoard();});});
1257
1587
  // A web page, mapped by itself in the hidden browser (headless.mjs): no capture,
1258
1588
  // no dragging. It takes a few seconds, so the button says so meanwhile.
1259
1589
  function mapUI(site,b){site=(site||'').trim();if(!site){screenErr('Type a site to map first: gmail, github.com/pulls or a web address.');return;}b.disabled=true;
@@ -1315,8 +1645,8 @@ function saveWeekly(){api('/api/desktop/weekly',{on:$('weeklyon').checked,day:$(
1315
1645
  function saveAutostart(){api('/api/desktop/autostart',{on:$('autostart').checked}).then(function(a){loadDesktop(a&&a.error);});}
1316
1646
  function weeklyNow(){var b=$('weeklynow');b.disabled=true;b.textContent='Writing...';
1317
1647
  api('/api/desktop/weekly/run',{}).then(function(r){b.disabled=false;b.textContent='Write it now';loadDesktop(r&&r.error?(r.error==='not-connected'?'Connect an AI above first.':r.error):'');});}
1318
- function showLatestWeek(){api('/api/desktop').then(function(d){var l=d&&d.weekly&&d.weekly.latest;if(!l||current!=='week'||$('out').textContent!=='Nothing yet - hit the button.')return;
1319
- $('out').textContent=l.text;$('out').classList.remove('muted');$('copy').classList.remove('hidden');var f=$('outfoot');f.textContent='Written automatically '+new Date(l.at).toLocaleString()+' · '+l.file+(l.footer?' · '+l.footer:'');f.style.display='block';});}
1648
+ function showLatestWeek(){api('/api/desktop').then(function(d){var l=d&&d.weekly&&d.weekly.latest;if(!l||current!=='week'||$('out').textContent!==WUEMPTY)return;
1649
+ WUTEXT=l.text;$('out').innerHTML=mdLite(l.text);$('out').classList.remove('muted');$('copy').classList.remove('hidden');var f=$('outfoot');f.textContent='Written automatically '+new Date(l.at).toLocaleString()+' · '+l.file+(l.footer?' · '+l.footer:'');f.style.display='block';});}
1320
1650
  function fitBadge(m){return m.fits?"<span class='tag'>fits your RAM</span>":"<span class='tag' style='background:#3a2a12;border-color:#6b4a1f;color:#F2A541'>needs more RAM</span>";}
1321
1651
  function loadRec(){var out=document.getElementById('recout');out.innerHTML="<div class='muted'>Reading your hardware...</div>";
1322
1652
  api('/api/models').then(function(d){var hw=d.hardware,rec=d.rec;
@@ -1421,7 +1751,7 @@ function lqWork(){lqRemember();LQ.scene='work';LQ.mode='aware';var b=document.bo
1421
1751
  if(LQ.workTimer)clearInterval(LQ.workTimer);LQ.workTimer=setInterval(function(){if(LQ.scene==='work'&&LQ.mode!=='pool')lqLoadWork();},5000);}
1422
1752
  function lqHome(){lqRemember();LQ.scene='home';var b=document.body;if(b&&b.classList){b.classList.remove('lq-work');b.classList.remove('lq-pooled');}if(LQ.workTimer){clearInterval(LQ.workTimer);LQ.workTimer=null;}LQ.mode='aware';lqLoad(true);}
1423
1753
  function lqLoadWork(){api('/api/work').then(function(w){LQ.workData=w||{};lqBuildWork();}).catch(function(){});}
1424
- function lqBuildWork(){var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list=[];LQ.bw=S.w;LQ.bh=S.h;
1754
+ function lqBuildWork(){if(LQ.workBy==='project'){lqBuildProjects();return;}var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list=[];LQ.bw=S.w;LQ.bh=S.h;
1425
1755
  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-160)/400)),rs=Math.min(S.s,(vx+vy)/1.5);
1426
1756
  var run=(w.running||[]).slice(0,3),ready=(w.ready||[]).slice(0,2),wait=(w.waiting||[]).slice(0,5),extra=(w.waitingCount||0)-wait.length;
1427
1757
  run.forEach(function(r,i){var ang=Math.PI+(i+1)*Math.PI/(run.length+1);var pg=r.progress?' · '+r.progress.done+' of '+r.progress.total+' done':'';
@@ -1437,6 +1767,22 @@ var nr=(w.running||[]).length,nw=w.waitingCount||0,nk=(w.ready||[]).length;
1437
1767
  LQ.coreText=(nr?nr+' at work':'Nobody at work')+(nw?' · '+nw+' waiting':'')+(nk?' · '+nk+' ready for your OK':'');
1438
1768
  var go=$('lqgo');if(go){var n=w.canGo||0;go.textContent=n?'Go \u00b7 start '+n+' task'+(n>1?'s':''):'';if(go.classList)go.classList.toggle('on',n>0);}
1439
1769
  lqLabels();}
1770
+ // Projects: a sphere per repo with work on it, sized by how much is going on, its
1771
+ // state under its name; spheres orbit while an agent works, amber when its work waits
1772
+ // for your OK. A click opens that project's tasks.
1773
+ function lqBuildProjects(){var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list=[];LQ.bw=S.w;LQ.bh=S.h;
1774
+ 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-160)/400)),rs=Math.min(S.s,(vx+vy)/1.5);
1775
+ var ps=(w.projects||[]).slice(0,8);ps.forEach(function(p,i){var ang=-2.2+i*2.399963,d=250+50*(i%3),load=p.waiting+2*p.ready+(p.running?3:0);
1776
+ var sub=p.running?(p.running.ask?'has a question for you':'agent at work: '+String(p.running.doing||'').toLowerCase()):p.ready?'ready for your OK':p.waiting+' task'+(p.waiting>1?'s':'')+' waiting';
1777
+ list.push({id:'proj:'+p.repo,kind:'proj',repo:p.repo,shape:'tasks',title:p.repo,sub:sub,orbit:!!p.running,ask:!!(p.ready||(p.running&&p.running.ask)),r:(30+7*Math.min(6,load))*rs,tx:cx+Math.cos(ang)*d*vx,ty:cy+Math.sin(ang)*d*vy});});
1778
+
1779
+ list.forEach(function(d){d.ax=d.tx;d.ay=d.ty;});lqRelax(list,S,cx,cy,92*rs);
1780
+ var old={};LQ.drops.forEach(function(d){old[d.id]=d;});
1781
+ 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;});
1782
+ var n=w.projectCount||list.length,nr=(w.running||[]).length,nw=w.waitingCount||0;
1783
+ 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.';
1784
+ 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);}
1785
+ lqLabels();}
1440
1786
  function lqGoWork(){var go=$('lqgo');if(go)go.disabled=true;api('/api/work/go',{}).then(function(r){if(go)go.disabled=false;
1441
1787
  var t=r&&r.error?r.error:r.started||r.queued?(r.started?'Started '+r.started:'')+(r.started&&r.queued?', ':'')+(r.queued?r.queued+' queued behind work already going':'')+'.':(r&&r.note)||'Nothing waiting to start.';
1442
1788
  LQ.talk.push({me:false,text:t});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.5,LQ.t];lqLoadWork();}).catch(function(){if(go)go.disabled=false;});}
@@ -1471,7 +1817,7 @@ var PARA_OF={tasks:'p',agents:'p',todo:'p',board:'a',week:'a',standup:'a',map:'r
1471
1817
  var PARA_NAME={p:'Projects',a:'Areas',r:'Resources',x:'Archive'},PARA_SUB={p:'work with an end',a:'what you keep up with',r:'to look things up in',x:'what’s done'};
1472
1818
  var PARA_ANG={p:-2.4,a:-0.65,r:2.45,x:0.8},PARA_LIVE={p:0.45,a:0.3,r:0.12,x:0};
1473
1819
  function lqPara(d){return d.kind==='archive'?'x':d.kind==='feed'?'a':PARA_OF[d.shape]||'p';}
1474
- function lqParaGo(g){lqAct();if(g==='p'){lqWork();return;}if(g==='a'){lqUse('board');lqPool('board');return;}if(g==='r'){lqUse('map');lqPool('map');return;}lqArchive();}
1820
+ function lqParaGo(g){lqAct();if(g==='p'){lqWorkBy('project');return;}if(g==='a'){lqUse('board');lqPool('board');return;}if(g==='r'){lqUse('map');lqPool('map');return;}lqArchive();}
1475
1821
  function lqArchive(){lqPool('tasks');LQ.arch=true;if(typeof loadArchived==='function')loadArchived();}
1476
1822
  function lqOrg(id,k){var h=k*977;id=String(id);for(var i=0;i<id.length;i++)h=(h*31+id.charCodeAt(i))|0;return ((h>>>0)%1000)/1000;}
1477
1823
  function lqGrow(list,cx,cy,vx,vy){list.forEach(function(d){var x=(d.tx-cx)/vx,y=(d.ty-cy)/vy,an=Math.atan2(y,x)+(lqOrg(d.id,1)-0.5)*0.7,rr=Math.sqrt(x*x+y*y)*(0.78+0.44*lqOrg(d.id,2));d.tx=cx+Math.cos(an)*rr*vx;d.ty=cy+Math.sin(an)*rr*vy;});}
@@ -1542,7 +1888,7 @@ a.tx=Math.max(a.r+60,Math.min(S.w-a.r-60,a.tx));a.ty=Math.max(140+a.r,Math.min(S
1542
1888
  var bad=false;for(var i2=0;i2<n&&!bad;i2++)for(var j2=i2+1;j2<n;j2++){var p=L[i2],q=L[j2],ddx=p.tx-q.tx,ddy=p.ty-q.ty;if(Math.sqrt(ddx*ddx+ddy*ddy)<lqSep(p,q,ddx,ddy)-1){bad=true;break;}}
1543
1889
  if(!bad)return true;L.forEach(function(x){x.r*=0.9;});coreR*=0.9;}return false;}
1544
1890
  function lqLabels(){var el=$('lqdrops');if(!el)return;
1545
- el.innerHTML=LQ.drops.map(function(d,i){return "<div class='lqd lq-"+d.kind+(LQ.openTag===d.id?' open':'')+"' data-i='"+i+"'><button type='button' class='lt' aria-expanded='"+(LQ.openTag===d.id)+"'><span>"+esc(d.title)+"</span></button>"+(d.sub?"<small>"+esc(d.sub)+"</small>":"")+"<button type='button' class='lgo'>Open &rsaquo;</button></div>";}).join('');
1891
+ el.innerHTML=LQ.drops.map(function(d,i){return "<div class='lqd lq-"+d.kind+(LQ.openTag===d.id?' open':'')+"' data-i='"+i+"'><button type='button' class='lt' aria-expanded='"+(LQ.openTag===d.id)+"'><span>"+esc(d.title)+"</span>"+(d.kind==='proj'&&d.sub?"<span class='lm'>"+esc(d.sub)+"</span>":"")+"</button>"+(d.sub?"<small>"+esc(d.sub)+"</small>":"")+"<button type='button' class='lgo'>Open &rsaquo;</button></div>";}).join('');
1546
1892
  LQ.btns=[];LQ.lts=[];el.querySelectorAll('.lqd').forEach(function(b){LQ.btns.push(b);var d=LQ.drops[+b.getAttribute('data-i')],lt=b.querySelector('.lt'),go=b.querySelector('.lgo');LQ.lts.push(lt);
1547
1893
  if(lt)lt.addEventListener('click',function(ev){lqOpen(d,ev);});
1548
1894
  if(go)go.addEventListener('click',function(ev){LQ.openTag=null;lqOpen(d,ev);});});
@@ -1557,20 +1903,23 @@ function lqUse(shape,ev,via){api('/api/adapt/use',{shape:shape,from:LQ.last,via:
1557
1903
  function lqOpen(d,ev){if(!d)return;lqAct();
1558
1904
  if(d.kind==='more'){LQ.moreOpen=!LQ.moreOpen;lqMore(d);return;}
1559
1905
  if(d.kind==='archive'){lqArchive();return;}
1906
+ if(d.kind==='proj'){TFILTER.repo=d.repo;TFILTER.type='';lqPool('tasks');if(typeof loadTasks==='function')loadTasks();return;}
1560
1907
  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');}
1561
1908
  function lqMore(d){var el=$('lqmore');if(!el)return;if(!LQ.moreOpen||!d){el.innerHTML='';return;}
1562
1909
  el.innerHTML=(d.more||[]).map(function(s){return "<button type='button' class='lqmb lqm' data-s='"+s+"'>"+esc(LQNAMES[s]||s)+"</button>";}).join('');
1563
1910
  el.querySelectorAll('.lqm').forEach(function(b){b.addEventListener('click',function(ev){var s=b.getAttribute('data-s');LQ.moreOpen=false;el.innerHTML='';lqUse(s,ev);lqGo(s);});});}
1564
- function lqGo(shape){if(shape==='tasks'||shape==='agents'){lqWork();return;}lqPool(shape);}
1911
+ function lqGo(shape){if(shape==='tasks'||shape==='agents'){lqWorkBy('task');return;}lqPool(shape);}
1912
+ // The work, by task (Tasks, Agents) or by project (the Projects group): the same scene, a step in the history each.
1913
+ function lqWorkBy(by){if(LQ.scene==='work'&&LQ.workBy===by&&LQ.mode!=='pool')return;lqRemember();LQ.back=true;LQ.workBy=by;try{lqWork();}finally{LQ.back=false;}}
1565
1914
  // Back, one step: every move between screens (home, the Tasks scene, a panel, the
1566
1915
  // Archive) notes where you were; a right-click (or the mouse's back button) returns
1567
1916
  // there. In a text field or over selected text the browser's own menu stays.
1568
- function lqState(){return {scene:LQ.scene==='work'?'work':'home',pool:LQ.mode==='pool'?(LQ.poolShape||null):null,arch:LQ.mode==='pool'&&!!LQ.arch};}
1917
+ function lqState(){return {by:LQ.workBy||'task',scene:LQ.scene==='work'?'work':'home',pool:LQ.mode==='pool'?(LQ.poolShape||null):null,arch:LQ.mode==='pool'&&!!LQ.arch};}
1569
1918
  function lqRemember(){if(LQ.back)return;LQ.hist=LQ.hist||[];var s=lqState(),top=LQ.hist[LQ.hist.length-1];if(top&&top.scene===s.scene&&top.pool===s.pool&&top.arch===s.arch)return;LQ.hist.push(s);if(LQ.hist.length>40)LQ.hist.shift();}
1570
1919
  function lqBack(){var h=LQ.hist||[],s=h.pop(),cur=lqState();if(!s){if(cur.pool)s={scene:cur.scene,pool:null};else if(cur.scene==='work')s={scene:'home',pool:null};else return;}
1571
- LQ.back=true;try{if(s.scene==='work'&&cur.scene!=='work')lqWork();else if(s.scene==='home'&&cur.scene==='work')lqHome();
1920
+ LQ.back=true;try{if(s.scene==='work'&&(cur.scene!=='work'||cur.by!==s.by)){LQ.workBy=s.by||'task';lqWork();}else if(s.scene==='home'&&cur.scene==='work')lqHome();
1572
1921
  if(s.pool){if(s.arch)lqArchive();else lqPool(s.pool);}else if(LQ.mode==='pool')lqSink();}finally{LQ.back=false;}}
1573
- function lqPool(shape){lqRemember();LQ.poolShape=shape;LQ.arch=false;tabPicked=true;setTab(shape);var b=document.body;if(b&&b.classList)b.classList.add('lq-pooled');LQ.mode='pool';var m=document.querySelector('main');if(m&&m.focus)m.focus();}
1922
+ function lqPool(shape){if(shape==='map'&&LM.items&&LM.items.length)setTimeout(function(){lmFit();lmStart();},60);lqRemember();LQ.poolShape=shape;LQ.arch=false;tabPicked=true;setTab(shape);var b=document.body;if(b&&b.classList)b.classList.add('lq-pooled');LQ.mode='pool';var m=document.querySelector('main');if(m&&m.focus)m.focus();}
1574
1923
  function lqSink(){lqRemember();var b=document.body;if(b&&b.classList)b.classList.remove('lq-pooled');LQ.mode='aware';lqAct();lqLoad(false);}
1575
1924
  function lqAct(){LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
1576
1925
  function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}