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

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
@@ -95,7 +95,8 @@ Where the portal opens. An Ask box at the top, then these, in order.
95
95
  - a pull request has **Merge**, unless its checks fail, are still running, or it conflicts.
96
96
 
97
97
  Answers go out as you, with the staff member's `@handle`, so they act on them straight away.
98
- When nothing is waiting, it says so.
98
+ Once you have had the last word, the card leaves Needs you for Your requests, since it is
99
+ waiting on them now. If they reply, it comes back. When nothing is waiting, it says so.
99
100
  - **Unread.** Anything on a staff member's own tracker, or elsewhere, with activity you have not
100
101
  seen: a peer's ask, a status issue, their own work. Unread comes from your GitHub
101
102
  notifications, as on Trackers. Cards and rows anywhere on Home carry the same blue mark, and
@@ -105,7 +106,7 @@ Where the portal opens. An Ask box at the top, then these, in order.
105
106
  they are idle, how their last run ended. Peer and follow-on runs today are counted against
106
107
  `max_runs_per_day`. This asks GitHub every few seconds while a run is going and every half
107
108
  minute otherwise, and only while Home is on screen.
108
- - **Your requests.** What you asked for: waiting, being worked on (a run for it is going), or
109
+ - **Your requests.** What you asked for, and asks of theirs you have answered: waiting, being worked on (a run for it is going), or
109
110
  answered (a staff member had the last word).
110
111
  - **Closed today.** What the staff closed today, with the line they closed it with, and
111
112
  **Reopen** beside each. Staff close finished issues themselves, so this is where you check.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.46",
3
+ "version": "0.1.0-alpha.47",
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": {
@@ -1,6 +1,9 @@
1
1
  /* Home: what needs you, who is working, your requests, closed today. */
2
2
 
3
3
  .home{max-width:820px}
4
+ /* Secondary text on Home is read, not scanned for numbers: the readable face and colour, not
5
+ the faint monospace the rest of the portal uses for metadata. */
6
+ .home .meta{font:12.5px/1.5 var(--sans);color:var(--ink-dim)}
4
7
  .home h2{font:600 13px var(--sans);color:var(--ink-dim);text-transform:none;letter-spacing:0;
5
8
  margin:26px 0 10px;display:flex;align-items:center;gap:8px}
6
9
  .home h2 .pill{font-size:10.5px}
@@ -59,13 +62,18 @@
59
62
 
60
63
  .homerow{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;padding:8px 2px;
61
64
  border-bottom:1px solid var(--line);font-size:13px}
62
- .homerow .homeopen{font-weight:500;font-size:13px}
65
+ .homerow .homeopen{font-weight:500;font-size:13.5px}
66
+ .homelist{display:grid;grid-template-columns:auto 1fr;align-items:start;gap:4px 12px}
67
+ .homelist > .chip{margin-top:2px}
68
+ .homelist .meta{margin-top:3px}
63
69
  .homeclick{cursor:pointer;border-radius:8px;padding-left:8px;padding-right:8px;margin:0 -8px}
64
70
  .homeclick:hover{background:var(--hover)}
65
71
  .homeclick:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
66
72
  .homeclosed{display:grid;grid-template-columns:1fr auto;align-items:center;gap:4px 14px}
67
- .homeclosed .meta{margin-top:3px;line-height:1.5;font-family:var(--sans);font-size:12px}
73
+ .homeclosed .meta{margin-top:3px}
68
74
  .homerest{margin-top:26px}
75
+ .homecard.gone,.homerow.gone{opacity:0;transform:translateX(12px);
76
+ transition:opacity .2s var(--ease),transform .2s var(--ease)}
69
77
 
70
78
  /* Activity you have not seen, as Trackers marks it: a blue edge and a bold title. Opening the
71
79
  thread marks it read, here and on GitHub. */
@@ -121,17 +121,22 @@ export function placeOf(item, owner, people, markers) {
121
121
  (item.assignees ?? []).some((a) => people.has(lower(a))) ||
122
122
  labels.some((l) => markers.has(l));
123
123
  const byPerson = people.has(lower(item.author));
124
+ /* You had the last word on an ask: approved it, answered it, said go with the default. It is
125
+ waiting on them now, so it is one of your requests until they reply or close it. If they
126
+ reply, the last word is theirs and it is back on you. */
127
+ const youAnswered = Boolean(item.lastComment) && people.has(lower(item.lastComment.author));
124
128
 
125
129
  if (!owner) {
126
- return onPerson && !byPerson
127
- ? { place: "needs", kind: kindOf(labels) }
128
- : { place: "elsewhere", why: item.role === "product" ? "contributor" : "other" };
130
+ if (!onPerson || byPerson) {
131
+ return { place: "elsewhere", why: item.role === "product" ? "contributor" : "other" };
132
+ }
133
+ return youAnswered ? { place: "requests" } : { place: "needs", kind: kindOf(labels) };
129
134
  }
130
135
  if (item.number === owner.statusIssue || labels.includes("keep-open")) {
131
136
  return { place: "staff", why: "status" };
132
137
  }
133
138
  if (byPerson) return { place: "requests" };
134
- if (onPerson) return { place: "needs", kind: kindOf(labels) };
139
+ if (onPerson) return youAnswered ? { place: "requests" } : { place: "needs", kind: kindOf(labels) };
135
140
  return {
136
141
  place: "staff",
137
142
  why: labels.some((l) => l.startsWith("from-")) ? "peer" : "own",
@@ -246,6 +246,12 @@ async function act(note, actions, payload, ok) {
246
246
  try {
247
247
  await post(payload);
248
248
  note.textContent = ok;
249
+ // Done is done: the card goes now, rather than when GitHub next answers.
250
+ const card = actions.closest?.(".homecard, .homerow");
251
+ if (card) {
252
+ card.classList.add("gone");
253
+ setTimeout(() => card.remove(), 220);
254
+ }
249
255
  refreshSoon();
250
256
  } catch (err) {
251
257
  note.textContent = String(err.message || err);
@@ -338,6 +344,16 @@ function outcome(r) {
338
344
  return r.conclusion === "success" ? "finished" : r.conclusion === "failure" ? "failed" : (r.conclusion ?? "ended");
339
345
  }
340
346
 
347
+ /**
348
+ * A timer that never keeps a process alive. In the browser this is setTimeout; under the
349
+ * tests' Node shim the polling loop would otherwise hold a test file open forever.
350
+ */
351
+ function later(fn, ms) {
352
+ const t = setTimeout(fn, ms);
353
+ t?.unref?.();
354
+ return t;
355
+ }
356
+
341
357
  function poll() {
342
358
  clearTimeout(timer);
343
359
  getLive()
@@ -351,7 +367,7 @@ function poll() {
351
367
  .finally(() => {
352
368
  if (S.view !== "home") return;
353
369
  const busy = (S.live?.staff ?? []).some((l) => l.running?.length);
354
- timer = setTimeout(poll, busy ? FAST : SLOW);
370
+ timer = later(poll, busy ? FAST : SLOW);
355
371
  });
356
372
  }
357
373
 
@@ -370,18 +386,25 @@ const WHY = {
370
386
 
371
387
  /** Something on a staff member's tracker, or elsewhere, with activity you have not seen. */
372
388
  function unreadRow(e) {
373
- return clickable(e.item, "homerow", [
389
+ return clickable(e.item, "homerow homelist", [
374
390
  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) }),
391
+ el("div", {}, [
392
+ openLink(e.item),
393
+ el("div", { className: "meta", textContent: whereOf(e) + " · " + ago(e.item.updatedAt) }),
394
+ ]),
377
395
  ]);
378
396
  }
379
397
 
380
398
  function requestRow(e) {
381
- return clickable(e.item, "homerow", [
382
- el("span", { className: "chip " + (e.status === "answered" ? "cool" : e.status === "working" ? "warm" : ""), textContent: STATUS[e.status] }),
383
- openLink(e.item),
384
- el("span", { className: "meta", textContent: whereOf(e) + " · " + ago(e.item.updatedAt) }),
399
+ return clickable(e.item, "homerow homelist", [
400
+ el("span", {
401
+ className: "chip " + (e.status === "answered" ? "cool" : e.status === "working" ? "warm" : ""),
402
+ textContent: STATUS[e.status],
403
+ }),
404
+ el("div", {}, [
405
+ openLink(e.item),
406
+ el("div", { className: "meta", textContent: whereOf(e) + " · " + ago(e.item.updatedAt) }),
407
+ ]),
385
408
  ]);
386
409
  }
387
410
 
@@ -491,5 +514,5 @@ function refreshSoon() {
491
514
  ensureInbox(true)
492
515
  .then(() => S.view === "home" && !document.querySelector("dialog[open]") && repaintInPlace())
493
516
  .catch(() => {});
494
- setTimeout(poll, 3_000);
517
+ later(poll, 3_000);
495
518
  }