@nanocollective/roster 0.1.0-alpha.45 → 0.1.0-alpha.46

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/docs/portal.md CHANGED
@@ -96,6 +96,10 @@ Where the portal opens. An Ask box at the top, then these, in order.
96
96
 
97
97
  Answers go out as you, with the staff member's `@handle`, so they act on them straight away.
98
98
  When nothing is waiting, it says so.
99
+ - **Unread.** Anything on a staff member's own tracker, or elsewhere, with activity you have not
100
+ seen: a peer's ask, a status issue, their own work. Unread comes from your GitHub
101
+ notifications, as on Trackers. Cards and rows anywhere on Home carry the same blue mark, and
102
+ opening one marks it read here and on GitHub.
99
103
  - **Working now.** Each staff member: what they are running and what started it (the daily run,
100
104
  a follow-on, answering an issue, a peer's ask), for how long, with a link to the log. When
101
105
  they are idle, how their last run ended. Peer and follow-on runs today are counted against
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.45",
3
+ "version": "0.1.0-alpha.46",
4
4
  "description": "An agent-run org, powered by GitHub. Scaffold AI staff members whose brain is a repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -283,6 +283,10 @@ dialog.sheet{border:1px solid var(--border-strong);border-radius:var(--r-lg);bac
283
283
  color:var(--ink);padding:0;width:min(860px,calc(100vw - 32px));max-height:calc(100vh - 48px);
284
284
  box-shadow:var(--shadow-lg)}
285
285
  dialog.sheet[open]{animation:dlgin .18s var(--ease)}
286
+ /* Pinned to the window. The graph's own `.sheet` panel is position:absolute, and that rule
287
+ reached this dialog too, so one opened on a scrolled page sat at the top of the document,
288
+ half off the screen. */
289
+ dialog.sheet{position:fixed}
286
290
  dialog.sheet::backdrop{background:rgba(0,0,0,.35);backdrop-filter:blur(8px)}
287
291
  .sheetbody{padding:22px 24px 26px;overflow:auto;max-height:calc(100vh - 50px)}
288
292
 
@@ -295,6 +299,12 @@ dialog.sheet.side .sheetbody{max-height:100vh;height:100%;box-sizing:border-box;
295
299
  @media (prefers-reduced-motion:reduce){dialog.sheet.side[open]{animation:none}}
296
300
  .sidebar-x{position:sticky;top:0;display:flex;justify-content:flex-end;margin:-6px -6px 4px;z-index:1}
297
301
  dialog.sheet.side .viewer{border:0;box-shadow:none;padding:0;min-height:0;background:none}
302
+ /* The title and its actions stay in view while a long thread scrolls under them. The close
303
+ button floats over the header's right edge, so the header leaves room for it. */
304
+ dialog.sheet.side .thead{position:sticky;top:-18px;z-index:1;background:var(--bg);
305
+ padding-top:18px;margin-top:-18px;padding-right:44px}
306
+ dialog.sheet.side .sidebar-x{position:sticky;top:0;height:0;margin:0;z-index:2;overflow:visible}
307
+ dialog.sheet.side .sidebar-x .iconbtn{margin-top:-4px}
298
308
  .sheettitle{margin:0 0 14px;font:600 18px var(--sans);letter-spacing:-.02em}
299
309
  .sheetloading{display:grid;place-items:center;min-height:260px}
300
310
  .spinner{width:22px;height:22px;border-radius:50%;border:2px solid var(--line);border-top-color:var(--accent);
@@ -67,6 +67,14 @@
67
67
  .homeclosed .meta{margin-top:3px;line-height:1.5;font-family:var(--sans);font-size:12px}
68
68
  .homerest{margin-top:26px}
69
69
 
70
+ /* Activity you have not seen, as Trackers marks it: a blue edge and a bold title. Opening the
71
+ thread marks it read, here and on GitHub. */
72
+ .homecard.unread{box-shadow:inset 3px 0 0 var(--blue)}
73
+ .homerow.unread{box-shadow:inset 3px 0 0 var(--blue);background:color-mix(in srgb,var(--blue) 6%,transparent)}
74
+ .unread .homeopen{font-weight:700}
75
+ .unread .homeopen::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
76
+ background:var(--blue);margin:0 7px 1px 0;vertical-align:middle}
77
+
70
78
  .homereports{display:flex;flex-direction:column;gap:14px;padding:14px 16px}
71
79
  .homereport{display:grid;grid-template-columns:190px 1fr;gap:12px;font-size:13px;line-height:1.5}
72
80
  .homereport p{margin:0 0 3px;color:var(--ink-dim)}
@@ -7,6 +7,10 @@
7
7
  * staff a staff member's own work: their status issue, a peer's ask, their own issues
8
8
  * elsewhere draft pull requests, and product or ops issues nobody has put on a person
9
9
  *
10
+ * `unread` is not a place: it lists what in `staff` and `elsewhere` has activity you have not
11
+ * seen (from your GitHub notifications), since those two are otherwise off Home. Items in the
12
+ * other places carry their own unread mark.
13
+ *
10
14
  * Closed items only matter when a staff member closed them today: that is "Closed today", with
11
15
  * Reopen beside each, because staff now close things without asking. */
12
16
 
@@ -45,7 +49,15 @@ export function sortHome(items, staff, humans, live = [], now = new Date()) {
45
49
  for (const r of l.running ?? []) if (s && r.issue) working.add(`${s.brain}#${r.issue}`);
46
50
  }
47
51
 
48
- const out = { reports: [], needs: [], requests: [], staff: [], elsewhere: [], closedToday: [] };
52
+ const out = {
53
+ reports: [],
54
+ needs: [],
55
+ requests: [],
56
+ staff: [],
57
+ elsewhere: [],
58
+ unread: [],
59
+ closedToday: [],
60
+ };
49
61
  const today = dayOf(now);
50
62
 
51
63
  for (const item of items) {
@@ -92,6 +104,9 @@ export function sortHome(items, staff, humans, live = [], now = new Date()) {
92
104
  out.needs.sort(oldest);
93
105
  out.requests.sort((a, b) => b.item.updatedAt.localeCompare(a.item.updatedAt));
94
106
  out.closedToday.sort((a, b) => b.item.closedAt.localeCompare(a.item.closedAt));
107
+ out.unread = [...out.staff, ...out.elsewhere]
108
+ .filter((e) => e.item.unread)
109
+ .sort((a, b) => b.item.updatedAt.localeCompare(a.item.updatedAt));
95
110
  return out;
96
111
  }
97
112
 
@@ -73,6 +73,11 @@ export function viewHome(m) {
73
73
  if (h.needs.length) for (const e of h.needs) page.append(needCard(e));
74
74
  else page.append(el("p", { className: "homeempty", textContent: "Nothing needs you." }));
75
75
 
76
+ if (h.unread.length) {
77
+ page.append(section("Unread", h.unread.length));
78
+ for (const e of h.unread) page.append(unreadRow(e));
79
+ }
80
+
76
81
  page.append(section("Working now"));
77
82
  page.append(working(staff));
78
83
 
@@ -147,7 +152,11 @@ function needCard(e) {
147
152
  const { item, kind } = e;
148
153
  /* The whole card opens the thread; the one quick action on it does not. Answering in words
149
154
  happens in the thread, where what was asked is in front of you. */
150
- const card = el("div", { className: "card homecard", tabIndex: 0, role: "button" });
155
+ const card = el("div", {
156
+ className: "card homecard" + (item.unread ? " unread" : ""),
157
+ tabIndex: 0,
158
+ role: "button",
159
+ });
151
160
  card.onclick = (ev) => {
152
161
  if (!ev.target.closest("button, a, textarea")) openThread(item);
153
162
  };
@@ -350,6 +359,24 @@ function poll() {
350
359
 
351
360
  const STATUS = { waiting: "Waiting", working: "Being worked on", answered: "Answered" };
352
361
 
362
+ const WHY = {
363
+ status: "Status",
364
+ peer: "Peer ask",
365
+ own: "Their work",
366
+ draft: "Draft",
367
+ contributor: "Issue",
368
+ other: "Issue",
369
+ };
370
+
371
+ /** Something on a staff member's tracker, or elsewhere, with activity you have not seen. */
372
+ function unreadRow(e) {
373
+ return clickable(e.item, "homerow", [
374
+ el("span", { className: "chip", textContent: WHY[e.why] ?? "Issue" }),
375
+ openLink(e.item),
376
+ el("span", { className: "meta", textContent: whereOf(e) + " · " + ago(e.item.updatedAt) }),
377
+ ]);
378
+ }
379
+
353
380
  function requestRow(e) {
354
381
  return clickable(e.item, "homerow", [
355
382
  el("span", { className: "chip " + (e.status === "answered" ? "cool" : e.status === "working" ? "warm" : ""), textContent: STATUS[e.status] }),
@@ -360,7 +387,11 @@ function requestRow(e) {
360
387
 
361
388
  /** A row that opens its thread from anywhere on it, except a button inside it. */
362
389
  function clickable(item, className, kids) {
363
- const row = el("div", { className: className + " homeclick", tabIndex: 0, role: "button" }, kids);
390
+ const row = el(
391
+ "div",
392
+ { className: className + " homeclick" + (item.unread ? " unread" : ""), tabIndex: 0, role: "button" },
393
+ kids,
394
+ );
364
395
  row.onclick = (ev) => {
365
396
  if (!ev.target.closest("button, a")) openThread(item);
366
397
  };