symbiot 0.49.0 → 0.49.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
4
4
 
5
+ ## 0.49.2
6
+
7
+ - **Projects shows your projects.** Click **Projects** on the liquid and each repo with work on it is a sphere: what's going on there under its name (an agent at work, ready for your OK, or how many tasks are waiting), spheres orbiting while an agent works, amber when its work waits for you. Click one to open just that project's tasks; right-click comes back.
8
+ - **The Dashboard, in detail.** A headline that says what's waiting, with when it was last read; a 24 hours / 3 days / 7 days switch instead of the old dropdown; a round refresh; **Close** instead of "Sink back". Feeds go by their short names ("Inbox", with your address under it). What you've already seen stays on the stream as small faint beads, so a quiet day isn't an empty line, and the stream lines up with "now" however wide the window is.
9
+ - **Connections.** The link chips are now a grid by kind of site, each with a bead: solid when linked, amber when it wants you to sign in, hollow when it isn't linked. Link, check and Unlink show when you point at one. It's on the Dashboard as well as in Settings.
10
+ - **Every panel's controls, in the look.** Dropdowns have a dark menu in Ferrofluid instead of the white one, buttons are solid in the look's ink (Pearl keeps its chrome), and the card list drops its emoji and green borders.
11
+ - **README** brought up to date: the looks, P.A.R.A., the stream, Connections, click to open and right-click to go back.
12
+
13
+ ## 0.49.1
14
+
15
+ - **Click a sphere, it opens.** A sphere (or its name) opens its screen straight away.
16
+ - **Right-click goes back.** One step at a time: from a panel to the screen you opened it from, from the Tasks scene to home. Your mouse's back button does the same. In a text box, or over text you've selected, right-click still gives the usual menu.
17
+
5
18
  ## 0.49.0
6
19
 
7
20
  - **Home by P.A.R.A.** The liquid groups everything Symbiot holds four ways: **Projects** (work with an end: Tasks, Agents, Todo, and what's asking you), **Areas** (what you keep up with: the Dashboard, your feeds, Week, Standup), **Resources** (to look things up in: Map, Drift, Settings) and the **Archive** (what's done). Each group has its own part of the screen and its name on the liquid; tap a name to open it. Projects move the most, the Archive is still, and Agents has spheres orbiting it while one works.
package/README.md CHANGED
@@ -41,9 +41,9 @@ visual app in your browser — in a clean, chrome-less window if you have
41
41
  Chrome/Chromium/Edge/Brave (`--app` mode), otherwise a normal tab. No Electron,
42
42
  no install — it's the same CLI. Press Ctrl+C (or click Quit) to stop.
43
43
 
44
- Tabs:
44
+ It opens on [the liquid](#the-liquid-a-home-that-shapes-itself-to-you); each of these parts opens from it:
45
45
 
46
- - **Map** (the landing view) — a live node graph of your work from your local git:
46
+ - **Map** — a live node graph of your work from your local git:
47
47
  you at the centre, your repos, the languages and tools they share (so related
48
48
  projects cluster), the coding **agents and editors** you have installed, and the
49
49
  **AI** powering Symbiot. Project folders that **aren't git repos** show up too
@@ -55,19 +55,23 @@ Tabs:
55
55
  **no AI key**; reviews and suggestions use your chosen model. While the scan runs
56
56
  the Map shows its progress. Under the graph is the [experimental](#screens-blueprints-for-screen-automation)
57
57
  **Screens** view.
58
- - **Dashboard** — everything you [watch](#screens-blueprints-for-screen-automation)
59
- side by side: a card for each page (your inbox, GitHub, WhatsApp, any other) with
60
- how many new since yesterday (or in the last 3 or 7 days), its latest brief, the
61
- newest items, and **Open**, **Draft a reply** and **Check now** on them. **Seen**
62
- on a card sets its count back to 0 (what it found stays under Watching, and the
63
- other cards keep theirs). **💬** on a card opens a chat with your AI about what's
64
- new there: what needs you, and what to say to whom. Agree it there, then **Draft
65
- a reply** on one, and your agent gets that talk with the message, so it writes
66
- what you agreed. The tab shows the count, so you see what's waiting from
67
- any tab, and once you watch something the app opens on the Dashboard instead of
68
- the Map. `symbiot watch board` prints the same cards as JSON, and
58
+ - **Dashboard** — everything you [watch](#screens-blueprints-for-screen-automation),
59
+ as a stream: time runs left to right, one current per feed (your inbox, GitHub,
60
+ WhatsApp, any other page), "now" on the right where each feed pools. Every message
61
+ is a bead placed when it arrived; the ones waiting for your reply are bigger and
62
+ amber, and what you've already seen stays as small faint beads. Point at or tap a
63
+ bead to read it and **Draft a reply** or **Open** it. Under each feed's name:
64
+ **Seen** (sets its count back to 0; what it found stays under Watching, and the
65
+ other feeds keep theirs), **Talk** (a chat with your AI about what's new there:
66
+ what needs you, and what to say to whom; agree it there, then **Draft a reply**
67
+ on one, and your agent gets that talk with the message, so it writes what you
68
+ agreed) and **Check now**. The headline says what's waiting; switch between 24
69
+ hours, 3 days and 7 days. Each feed's latest brief sits at the end of its
70
+ current, the cards are still there under **All of it as a list**, and
71
+ **Connections** below shows every site you can [link](#link-your-work).
72
+ `symbiot watch board` prints the same feeds as JSON, and
69
73
  `symbiot watch board --line` as one line for a status bar (`2 emails · 1 WhatsApp message`).
70
- `symbiot watch chat <id> "question"` is the card's 💬 chat from a terminal, so an
74
+ `symbiot watch chat <id> "question"` is a feed's Talk from a terminal, so an
71
75
  agent can go over what's new with you too.
72
76
  - **Drift** — the [`symbiot drift`](#whats-out-of-sync-symbiot-drift) report, with
73
77
  a "fetch latest" toggle (and an [experimental](#experimental) "check CI").
@@ -77,7 +81,12 @@ Tabs:
77
81
  [`symbiot push`](#hand-tasks-to-your-coding-agent-symbiot-push)). What your agent
78
82
  finishes lands in **Awaiting your review** (see
79
83
  [Review and approve](#review-and-approve-the-agents-work)). A task you tick
80
- yourself is done and **auto-archives**; the archived view can restore it.
84
+ yourself is done and **auto-archives**; the archived view (the liquid's
85
+ **Archive**) can restore it. Opening Tasks or Agents from the liquid shows the
86
+ work first: a sphere per task an agent is on, named in plain words, with small
87
+ spheres orbiting it while it works; what's done and waiting for your OK; and what's
88
+ waiting its turn, gathered round the agent in its repo. One **Go** (or saying
89
+ "go") starts everything waiting. Tap a sphere for the details.
81
90
  **💬** on any task opens a Q&A thread: ask what it means, how to approach it, or
82
91
  (once it's awaiting review) what the agent changed. Answers use your chosen AI,
83
92
  grounded in that repo's commits, README, rules and pending diff; the thread is
@@ -475,13 +484,39 @@ in the app.
475
484
 
476
485
  ## The liquid: a home that shapes itself to you
477
486
 
478
- `symbiot app` opens on one surface of liquid silver. Its droplets are what only
479
- you can do (an Approve waiting, an agent's question to you), what's new on what
480
- you watch, and the parts of the app. Open a droplet and that part pools open over
481
- the liquid; **Sink back** (or Esc) returns it. Talk to it in the bar at the bottom:
482
- "open tasks" opens Tasks, anything else goes to the same Symbiot as every chat.
483
- Leave it alone and it rests as one orb. There are no settings: it adapts to you.
484
-
487
+ `symbiot app` opens on one surface of liquid. Pick its look at the top, next to the
488
+ name: **Ferrofluid** (glossy black liquid metal, lit like a studio; the default),
489
+ **Glass** (clear droplets that bend the colours behind them) or **Pearl** (silver
490
+ lit like a product photo). Every panel follows the look, and the typeface (Geist)
491
+ ships with Symbiot, so it looks the same offline.
492
+
493
+ Its droplets are what only you can do (an Approve waiting, an agent's question to
494
+ you), what's new on what you watch, and the parts of the app, grouped the
495
+ **P.A.R.A.** way, each group in its own part of the screen with its name over it:
496
+
497
+ - **Projects** (work with an end): Tasks, Agents, Todo and what's asking you. They
498
+ move the most; Agents has spheres orbiting it while an agent works. Click the
499
+ **Projects** name for your projects themselves: a sphere per repo with work on it,
500
+ saying what's going on there, amber when its work waits for your OK; click one for
501
+ just its tasks.
502
+ - **Areas** (what you keep up with): the Dashboard, your feeds, Week and Standup.
503
+ - **Resources** (to look things up in): Map, Drift and Settings.
504
+ - **Archive** (what's done): your archived tasks. It sits still, low on the right.
505
+
506
+ **Click a droplet** and its part opens over the liquid; click a group's name to open
507
+ that group. **Right-click** (or your mouse's back button) goes back one step at a
508
+ time; **Close** or Esc closes a part. In a text box, or over text you've selected,
509
+ right-click still gives the usual menu. Whatever needs you glows **amber**, here and
510
+ on the Dashboard. Talk to it in the bar at the bottom: "open tasks" opens Tasks,
511
+ "go" in the work view starts what's waiting, anything else goes to the same Symbiot
512
+ as every chat. Leave it alone and it rests as one orb. There are no settings for any
513
+ of this: it adapts to you.
514
+
515
+ - **Where things sit: nearest neighbours.** Inside a group, the parts you move
516
+ between most sit together (k-nearest neighbours on the same Markov chain as
517
+ below, counting only links stronger than chance), so the layout has clusters and
518
+ gaps instead of a ring. Each droplet keeps a steady offset of its own, so it
519
+ reads as grown, not ruled, and stays where it was from one visit to the next.
485
520
  - **How you work** (`adapt.mjs`). Use decays with a 3-day half-life; what you open
486
521
  next comes from a Markov chain (with a Dirichlet prior, so a few moves can't
487
522
  swing it) and your hour of day. The more predictable you are (low Shannon
@@ -503,7 +538,7 @@ Symbiot's config folder, readable by you only.
503
538
 
504
539
  ## Link your work
505
540
 
506
- Settings → **Link your work** has one button per standard work site: Gmail,
541
+ **Connections** (on the Dashboard, and in Settings) has one entry per standard work site: Gmail,
507
542
  Outlook, Google and Outlook Calendar, GitHub, GitLab, Slack, Microsoft Teams,
508
543
  WhatsApp, Jira & Confluence, Linear, Asana, Trello, Google Drive, Notion,
509
544
  HubSpot and Salesforce. One click:
@@ -517,8 +552,9 @@ HubSpot and Salesforce. One click:
517
552
  in Standup's "Waiting on you", and in **Week**. Someone who doesn't write code
518
553
  gets a Week from their linked sites too.
519
554
 
520
- Each button shows where it stands: grey (not linked), amber (sign in, or signed
521
- out), green (linked), with ↻ to check now and × to unlink. Unlinking stops
555
+ They're grouped by kind (Mail, Calendar, Code, Chat, Work, Docs, Sales), each with
556
+ a bead that shows where it stands: hollow (not linked), amber (sign in, or signed
557
+ out), solid (linked). Point at one for **Link**, **↻** (check now) and **Unlink**. Unlinking stops
522
558
  watching it and stops trusting what the link trusted, never a site you trusted
523
559
  yourself.
524
560
 
@@ -857,7 +893,7 @@ symbiot watch new --hours 24 # what's new, newest first (JSON)
857
893
  symbiot watch board # the Dashboard's cards (JSON; .total is the count, for a status bar)
858
894
  symbiot watch board --line # the same as one line: "2 emails · 1 WhatsApp message" (empty when nothing's new)
859
895
  symbiot watch seen <watch id> # set a card back to 0, like its Seen button
860
- symbiot watch chat <watch id> "what needs me?" # talk a card over with your AI, like its 💬 (--clear starts over)
896
+ symbiot watch chat <watch id> "what needs me?" # talk a card over with your AI, like its Talk (--clear starts over)
861
897
  symbiot watch check # read them all now (JSON)
862
898
  symbiot watch draft <id> # Draft a reply to a new email or chat message (see below)
863
899
  symbiot watch brief on # your AI says what needs you (off to stop)
package/home.mjs CHANGED
@@ -91,7 +91,13 @@ function workScene({ deps = {} } = {}) {
91
91
  const ready = pending().filter((r) => r.path && !r.running && ((r.tasks || []).length || (r.files || []).length)).map((r) => ({ id: "ready:" + r.repo, repo: r.repo, count: (r.tasks || []).length }));
92
92
  const busy = new Set(running.map((r) => r.name)), open = tasks().filter((t) => !t.done && !t.archived && !t.review && t.repo);
93
93
  const waiting = open.map((t) => ({ id: "task:" + t.id, text: plain(t.text, 56), repo: t.repo, busy: busy.has(t.repo) }));
94
- return { running, ready, waiting: waiting.slice(0, 12), waitingCount: waiting.length, canGo: waiting.filter((w) => !w.busy).length };
94
+ // Projects: each repo with work on it, what's going on there in one line's worth
95
+ const by = {}, at = (name) => (by[name] = by[name] || { repo: name, waiting: 0, ready: 0, running: null });
96
+ running.forEach((r) => { at(r.name).running = { doing: r.doing, progress: r.progress, ask: r.waiting }; });
97
+ ready.forEach((r) => { at(r.repo).ready = r.count || 1; });
98
+ open.forEach((t) => { at(t.repo).waiting++; });
99
+ const projects = Object.values(by).sort((a, b) => (b.running ? 1 : 0) - (a.running ? 1 : 0) || b.ready - a.ready || b.waiting - a.waiting || (a.repo < b.repo ? -1 : 1));
100
+ return { running, ready, waiting: waiting.slice(0, 12), waitingCount: waiting.length, canGo: waiting.filter((w) => !w.busy).length, projects: projects.slice(0, 10), projectCount: projects.length };
95
101
  }
96
102
  // Go: everything waiting goes to its repo's agent, the way Send to repos and an
97
103
  // agent per repo would: briefs written, an agent started in each (or queued
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.49.0",
3
+ "version": "0.49.2",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {
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,63 @@ 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
+ body.lq-liquid #lqsinkrow{margin:-6px -4px 8px}
321
387
  /* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
322
388
  .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
389
  body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
@@ -325,7 +391,7 @@ body.lq-look-pearl .bstream{background:#EDECE9}
325
391
  .bstream.none{display:none}
326
392
  #bsc{position:absolute;inset:0;width:100%;height:100%;display:block}
327
393
  #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%)}
394
+ body.lq-liquid .bcard.blane{position:absolute;left:16px;width:186px;padding:0;border:0;background:transparent;box-shadow:none;transform:translateY(-50%)}
329
395
  .blane .bln{font:600 14px/1.2 var(--sans);color:var(--bone);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
330
396
  .blane .blc{font-size:12px;color:var(--faint);margin-top:1px}
331
397
  .blane .blc b{color:var(--amber);font-weight:600}
@@ -400,7 +466,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
400
466
  <button class="tab" data-tab="settings">Settings</button>
401
467
  </div>
402
468
  <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>
469
+ <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
470
  <section id="panel-map">
405
471
  <div class="profile muted" id="profile">Mapping your work&hellip;</div>
406
472
  <div class="maprow">
@@ -432,7 +498,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
432
498
  </div>
433
499
  </section>
434
500
  <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>
501
+ <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
502
  <div id="boardmsg"></div>
437
503
  <div id="bstream" class="bstream"><canvas id="bsc" aria-hidden="true"></canvas><div id="bsov"></div></div>
438
504
  <div id="boardtalk"></div>
@@ -969,20 +1035,20 @@ var pf=$('pforget');if(pf)pf.addEventListener('click',function(){if(typeof confi
969
1035
  // linked, amber waiting for you to sign in (or signed out), green linked. Shown
970
1036
  // in Settings, and on the Dashboard until something is linked.
971
1037
  var LINKS=null;
972
- var LINKWORD={ok:'linked',signin:'sign in, then close the window',signedout:'signed out',error:'needs a fix'};
1038
+ var LINKWORD={ok:'linked',signin:'sign in, then close it',signedout:'signed out',error:'needs a fix'};
973
1039
  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>":"";
1040
+ // Connections: a column per kind of site; each site a bead in the look: solid when
1041
+ // linked, amber when it wants you to sign in, hollow when it isn't linked. Link,
1042
+ // check (↻) and Unlink show on hover (always on touch).
1043
+ 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
1044
  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;}
1045
+ 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';
1046
+ 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>"+
1047
+ (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
1048
  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
1049
  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
1050
  el.innerHTML=linksHtml(LINKS,id==='boardlinks');
985
- el.querySelectorAll('.lnk').forEach(function(sp){var lid=sp.getAttribute('data-id'),name=sp.querySelector('.lbtn').textContent;
1051
+ 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
1052
  sp.querySelector('.lbtn').addEventListener('click',function(){linksOut("<div class='note muted'>Opening "+esc(name)+"&hellip;</div>");
987
1053
  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
1054
  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();
@@ -1158,14 +1224,15 @@ api('/api/watch/open-chat',{id:n.id}).then(function(x){b.disabled=false;if(!x||x
1158
1224
  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
1225
  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
1226
  // ---- 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;'};
1227
+ var BOARD=null;
1162
1228
  // a chat's line says who its last message is from (watch.mjs fromOf): only unread ones count as waiting on you
1163
1229
  var CHATFROM={them:'unread from them',you:'you sent the last one',unknown:'nothing unread, maybe yours'};
1164
1230
  // 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
1231
  var BTALK={},BTALKDRAFT={},BTALKBUSY=0;
1166
1232
  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
1233
  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();}});}
1234
+ var BOARDH=24;
1235
+ function loadBoard(){api('/api/watch/board?hours='+BOARDH).then(function(b){if(b&&b.cards){BOARD=b;if(!talking())renderBoard();}});}
1169
1236
  function boardMsg(cls,html){$('boardmsg').innerHTML="<div class='note "+cls+"'>"+html+"</div>";}
1170
1237
 
1171
1238
  // ---- the Dashboard as a stream ----------------------------------------------------
@@ -1175,38 +1242,41 @@ function boardMsg(cls,html){$('boardmsg').innerHTML="<div class='note "+cls+"'>"
1175
1242
  // the list below, wired by renderBoard's wire); a feed's name carries its Seen,
1176
1243
  // Talk and Check now. Drawn in the look's liquid, with its own canvas.
1177
1244
  var BS={G:null,beads:[],hot:null,on:false,b:null},BWIRE=null;
1245
+ // A feed's short name: "Inbox - you@x.com - x Mail" reads as Inbox, with the address under it.
1246
+ 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
1247
  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};}
1248
+ 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
1249
  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);};
1250
+ 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
1251
  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>";
1252
+ html+="<div class='bline' style='left:"+(g.x0-24)+"px;width:"+(g.x1-g.x0+24)+"px;top:"+Math.round(y)+"px'></div>";
1253
+ 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
1254
  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
1255
  var marks=b.hours===24?[18,12,6]:b.hours===72?[48,24]:[120,72,24];
1187
1256
  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
1257
  html+="<span class='btick' style='left:"+g.x1+"px'>now</span><div class='bnow' style='left:"+g.x1+"px'></div>";
1189
1258
  var old={};BS.beads.forEach(function(x){old[x.key]=x;});BS.beads=[];
1190
1259
  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});});});
1260
+ 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});});
1261
+ (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
1262
  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
1263
  html+="<div class='btip' role='dialog' aria-live='polite'></div>";ov.innerHTML=html;
1194
1264
  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
1265
  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
1266
  BS.hot=null;bsStart();}
1197
1267
  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>";
1268
+ 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;}
1269
+ 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
1270
  tip.className='btip on';if(BWIRE)BWIRE(tip);}
1201
1271
  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
1272
  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;
1273
+ 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
1274
  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
1275
  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
1276
  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
1277
  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
1278
  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);});
1279
+ (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
1280
  BS.beads.forEach(function(x){put(x.x,x.y,x.r,x.need?1:0);});
1211
1281
  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
1282
  function renderBoard(){var b=BOARD,cs=b.cards,el=$('board');
@@ -1216,10 +1286,11 @@ if(!cs.length){$('boardsum').textContent='Everything you watch, side by side.';
1216
1286
  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
1287
  $('boardgo').addEventListener('click',function(){setTab('map');var w=$('watchbox');if(w&&w.scrollIntoView)w.scrollIntoView({block:'center'});});return;}
1218
1288
  var when=b.hours===24?'since yesterday':b.hours===72?'in the last 3 days':'in the last 7 days';
1289
+ 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
1290
  var waiting=cs.filter(function(c){return c.count;}).map(function(c){return c.label+(c.source==='page'?' on '+c.name:'');});
1220
1291
  $('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
1292
  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>"
1293
+ 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
1294
  +"<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
1295
  +(c.error?"<div class='note err'>"+esc(c.error)+"</div>":"")
1225
1296
  +(c.brief?"<div class='bbrief'>"+esc(c.brief.text)+"</div>":"")
@@ -1253,7 +1324,8 @@ el.querySelectorAll('.bopen').forEach(function(btn){btn.addEventListener('click'
1253
1324
  el.querySelectorAll('.bdraft').forEach(function(btn){btn.addEventListener('click',function(){var n=item(btn);btn.disabled=true;
1254
1325
  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
1326
  wire(el);wire($('boardtalk'));wire($('bsov'));BWIRE=wire;}
1256
- $('boardrefresh').addEventListener('click',loadBoard);$('boardhours').addEventListener('change',loadBoard);
1327
+ $('boardrefresh').addEventListener('click',loadBoard);
1328
+ 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
1329
  // A web page, mapped by itself in the hidden browser (headless.mjs): no capture,
1258
1330
  // no dragging. It takes a few seconds, so the button says so meanwhile.
1259
1331
  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;
@@ -1417,11 +1489,11 @@ window.addEventListener('focus',function(){heartbeat(true);}); // re-check for u
1417
1489
  // Tasks and agents, in the liquid: what each agent is doing (spheres revolve round
1418
1490
  // it while it works), what's done and waiting for your OK, and what's waiting its
1419
1491
  // turn. One Go starts what's waiting. A tap opens the details; nothing else to read.
1420
- function lqWork(){LQ.scene='work';LQ.mode='aware';var b=document.body;if(b&&b.classList){b.classList.add('lq-work');b.classList.remove('lq-pooled');}lqLoadWork();
1492
+ function lqWork(){lqRemember();LQ.scene='work';LQ.mode='aware';var b=document.body;if(b&&b.classList){b.classList.add('lq-work');b.classList.remove('lq-pooled');}lqLoadWork();
1421
1493
  if(LQ.workTimer)clearInterval(LQ.workTimer);LQ.workTimer=setInterval(function(){if(LQ.scene==='work'&&LQ.mode!=='pool')lqLoadWork();},5000);}
1422
- function lqHome(){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);}
1494
+ 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
1495
  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;
1496
+ 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
1497
  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
1498
  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
1499
  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 +1509,22 @@ var nr=(w.running||[]).length,nw=w.waitingCount||0,nk=(w.ready||[]).length;
1437
1509
  LQ.coreText=(nr?nr+' at work':'Nobody at work')+(nw?' · '+nw+' waiting':'')+(nk?' · '+nk+' ready for your OK':'');
1438
1510
  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
1511
  lqLabels();}
1512
+ // Projects: a sphere per repo with work on it, sized by how much is going on, its
1513
+ // state under its name; spheres orbit while an agent works, amber when its work waits
1514
+ // for your OK. A click opens that project's tasks.
1515
+ 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;
1516
+ 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);
1517
+ 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);
1518
+ 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';
1519
+ 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});});
1520
+
1521
+ list.forEach(function(d){d.ax=d.tx;d.ay=d.ty;});lqRelax(list,S,cx,cy,92*rs);
1522
+ var old={};LQ.drops.forEach(function(d){old[d.id]=d;});
1523
+ 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;});
1524
+ var n=w.projectCount||list.length,nr=(w.running||[]).length,nw=w.waitingCount||0;
1525
+ 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.';
1526
+ 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);}
1527
+ lqLabels();}
1440
1528
  function lqGoWork(){var go=$('lqgo');if(go)go.disabled=true;api('/api/work/go',{}).then(function(r){if(go)go.disabled=false;
1441
1529
  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
1530
  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,8 +1559,8 @@ var PARA_OF={tasks:'p',agents:'p',todo:'p',board:'a',week:'a',standup:'a',map:'r
1471
1559
  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
1560
  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
1561
  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();}
1475
- function lqArchive(){lqPool('tasks');if(typeof loadArchived==='function')loadArchived();}
1562
+ 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();}
1563
+ function lqArchive(){lqPool('tasks');LQ.arch=true;if(typeof loadArchived==='function')loadArchived();}
1476
1564
  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
1565
  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;});}
1478
1566
  // Nearest neighbours, in the liquid: each droplet is drawn towards the ones it's
@@ -1542,9 +1630,9 @@ 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
1630
  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
1631
  if(!bad)return true;L.forEach(function(x){x.r*=0.9;});coreR*=0.9;}return false;}
1544
1632
  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('');
1633
+ 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
1634
  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
- if(lt)lt.addEventListener('click',function(ev){if(d.kind==='more'){lqOpen(d,ev);return;}lqTag(d.id);});
1635
+ if(lt)lt.addEventListener('click',function(ev){lqOpen(d,ev);});
1548
1636
  if(go)go.addEventListener('click',function(ev){LQ.openTag=null;lqOpen(d,ev);});});
1549
1637
  var oe=$('lqorbits');if(oe){oe.innerHTML=LQ.drops.map(function(d,i){return d.kind==='run'||d.orbit?"<div class='lqorbit"+(d.ask?' ask':'')+"' data-i='"+i+"'><span><i></i></span><span><i></i></span><span><i></i></span></div>":'';}).join('');LQ.orbs=[];oe.querySelectorAll('.lqorbit').forEach(function(o){LQ.orbs.push(o);});}
1550
1638
  LQ.drops.forEach(function(d){if(d.x===d.tx&&d.y===d.ty)return;if(!LQ.frame){d.x=d.tx;d.y=d.ty;d.cr=d.r;}});lqStep();
@@ -1557,13 +1645,24 @@ function lqUse(shape,ev,via){api('/api/adapt/use',{shape:shape,from:LQ.last,via:
1557
1645
  function lqOpen(d,ev){if(!d)return;lqAct();
1558
1646
  if(d.kind==='more'){LQ.moreOpen=!LQ.moreOpen;lqMore(d);return;}
1559
1647
  if(d.kind==='archive'){lqArchive();return;}
1648
+ if(d.kind==='proj'){TFILTER.repo=d.repo;TFILTER.type='';lqPool('tasks');if(typeof loadTasks==='function')loadTasks();return;}
1560
1649
  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
1650
  function lqMore(d){var el=$('lqmore');if(!el)return;if(!LQ.moreOpen||!d){el.innerHTML='';return;}
1562
1651
  el.innerHTML=(d.more||[]).map(function(s){return "<button type='button' class='lqmb lqm' data-s='"+s+"'>"+esc(LQNAMES[s]||s)+"</button>";}).join('');
1563
1652
  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);}
1565
- function lqPool(shape){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();}
1566
- function lqSink(){var b=document.body;if(b&&b.classList)b.classList.remove('lq-pooled');LQ.mode='aware';lqAct();lqLoad(false);}
1653
+ function lqGo(shape){if(shape==='tasks'||shape==='agents'){lqWorkBy('task');return;}lqPool(shape);}
1654
+ // The work, by task (Tasks, Agents) or by project (the Projects group): the same scene, a step in the history each.
1655
+ 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;}}
1656
+ // Back, one step: every move between screens (home, the Tasks scene, a panel, the
1657
+ // Archive) notes where you were; a right-click (or the mouse's back button) returns
1658
+ // there. In a text field or over selected text the browser's own menu stays.
1659
+ 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};}
1660
+ 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();}
1661
+ 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;}
1662
+ 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();
1663
+ if(s.pool){if(s.arch)lqArchive();else lqPool(s.pool);}else if(LQ.mode==='pool')lqSink();}finally{LQ.back=false;}}
1664
+ 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();}
1665
+ function lqSink(){lqRemember();var b=document.body;if(b&&b.classList)b.classList.remove('lq-pooled');LQ.mode='aware';lqAct();lqLoad(false);}
1567
1666
  function lqAct(){LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
1568
1667
  function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}
1569
1668
  function lqTalkShow(wait){var el=$('lqtalk');if(!el)return;el.innerHTML=LQ.talk.map(function(m){return "<div class='lqmsg"+(m.me?' me':'')+"'>"+esc(m.text)+stepsHtml(m.steps)+"</div>";}).join('')+(wait?"<div class='lqmsg thinking'>Thinking: recalling what it knows, reading what's here&hellip;</div>":'');if(el.scrollHeight)el.scrollTop=el.scrollHeight;}
@@ -1660,7 +1759,9 @@ gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.
1660
1759
  gl.drawArrays(gl.TRIANGLES,0,3);}
1661
1760
  function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqTheme();bd.classList.add('lq-liquid');
1662
1761
  ['(prefers-color-scheme: light)','(prefers-contrast: more)','(forced-colors: active)','(prefers-reduced-transparency: reduce)','(prefers-reduced-motion: reduce)'].forEach(function(q){try{var mq=window.matchMedia&&window.matchMedia(q);if(mq&&mq.addEventListener)mq.addEventListener('change',lqTheme);else if(mq&&mq.addListener)mq.addListener(lqTheme);}catch(e){}});
1663
- var L=$('liquid');if(L&&L.addEventListener){L.addEventListener('click',function(ev){var t=ev&&ev.target;if(t&&t.closest&&t.closest('button,input,form,.lqd'))return;var x=ev.clientX,y=ev.clientY,hit=null;LQ.drops.forEach(function(d){if(!hit&&Math.hypot(x-d.x,y-d.y)<=d.cr+8)hit=d;});if(hit){if(hit.kind==='more')lqOpen(hit,ev);else lqTag(hit.id);}else if(LQ.openTag)lqTag(LQ.openTag);});L.addEventListener('pointermove',function(ev){LQ.pointer=[ev.clientX,ev.clientY];if(ev.pointerType==='touch')LQ.touch=true;lqAct();});L.addEventListener('pointerdown',function(ev){if(ev.pointerType==='touch')LQ.touch=true;lqAct();});}
1762
+ if(document.addEventListener){document.addEventListener('contextmenu',function(ev){var b=document.body,t=ev&&ev.target;if(!b||!b.classList||!b.classList.contains('lq-liquid'))return;if(t&&t.closest&&t.closest('input,textarea,select,[contenteditable]'))return;var sel=window.getSelection?String(window.getSelection()):'';if(sel)return;ev.preventDefault();lqBack();});
1763
+ document.addEventListener('mouseup',function(ev){var b=document.body;if(ev&&ev.button===3&&b&&b.classList&&b.classList.contains('lq-liquid')){ev.preventDefault();lqBack();}});}
1764
+ var L=$('liquid');if(L&&L.addEventListener){L.addEventListener('click',function(ev){var t=ev&&ev.target;if(t&&t.closest&&t.closest('button,input,form,.lqd'))return;var x=ev.clientX,y=ev.clientY,hit=null;LQ.drops.forEach(function(d){if(!hit&&Math.hypot(x-d.x,y-d.y)<=d.cr+8)hit=d;});if(hit)lqOpen(hit,ev);else if(LQ.openTag)lqTag(LQ.openTag);});L.addEventListener('pointermove',function(ev){LQ.pointer=[ev.clientX,ev.clientY];if(ev.pointerType==='touch')LQ.touch=true;lqAct();});L.addEventListener('pointerdown',function(ev){if(ev.pointerType==='touch')LQ.touch=true;lqAct();});}
1664
1765
  var f=$('lqform');if(f&&f.addEventListener)f.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();lqSay();});
1665
1766
  var ia=$('lqask');if(ia&&ia.addEventListener){ia.addEventListener('focus',function(){lqTalkMode(true);lqAct();});ia.addEventListener('blur',function(){if(!ia.value&&!LQ.talk.length)lqTalkMode(false);});ia.addEventListener('keydown',function(ev){if(ev&&ev.key==='Escape'){if(LQ.mode==='pool'){lqSink();return;}if(LQ.scene==='work'&&!ia.value){lqHome();return;}ia.value='';LQ.talk=[];lqTalkShow(false);lqTalkMode(false);if(ia.blur)ia.blur();}});}
1666
1767
  var co=$('lqcore');if(co&&co.addEventListener)co.addEventListener('click',function(){var i=$('lqask');if(i&&i.focus)i.focus();});
package/watch.mjs CHANGED
@@ -254,8 +254,9 @@ function watchBoard(hours = 24, now = Date.now()) {
254
254
  const cards = d.watches.map((w) => {
255
255
  const recent = d.news.filter((n) => n.watch === w.id && n.ts >= since && unseen(n, w)), k = kindOf(w.url), count = recent.filter((n) => needsYou(n, w)).length;
256
256
  const b = d.briefs.find((x) => x.watch === w.id && x.ts >= since && unseen(x, w));
257
+ const past = d.news.filter((n) => n.watch === w.id && n.ts >= since && !unseen(n, w)).slice(0, 12).map((n) => ({ id: n.id, ts: n.ts, name: n.name, text: n.text, seen: true }));
257
258
  return { ...view(w), source: sourceOf(w.url), count, label: k ? `${count} ${k[count === 1 ? 0 : 1]}` : `${count} new`,
258
- items: markNews(recent.slice(0, 8), [w]).map((n) => ({ ...n, need: needsYou(n, w) })), ...(b ? { brief: { text: b.text, ts: b.ts, count: b.count } } : {}), ...(w.chat && w.chat.length ? { chat: w.chat } : {}) };
259
+ items: markNews(recent.slice(0, 8), [w]).map((n) => ({ ...n, need: needsYou(n, w) })), past, ...(b ? { brief: { text: b.text, ts: b.ts, count: b.count } } : {}), ...(w.chat && w.chat.length ? { chat: w.chat } : {}) };
259
260
  });
260
261
  return { hours, total: cards.reduce((s, c) => s + c.count, 0), cards, brief: briefOn() };
261
262
  }