@nanocollective/roster 0.1.0-alpha.28 → 0.1.0-alpha.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.28",
3
+ "version": "0.1.0-alpha.29",
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": {
@@ -35,6 +35,7 @@
35
35
  <span class="ic" data-icon="inbox"></span> Inbox
36
36
  <span class="n" id="inboxcount"></span>
37
37
  </button>
38
+ <div class="staffviews" id="inboxsub" hidden></div>
38
39
  <!-- "Pending work", not "Pull requests": what is behind it is finished work waiting on you,
39
40
  and the word for how it is delivered is not the word for what it is. -->
40
41
  <button class="nav" id="prsnav" data-view="prs" title="Finished work waiting on you to merge">
@@ -14,7 +14,7 @@ import {
14
14
  syncNotice,
15
15
  } from "./refresh.js";
16
16
  import { onRender } from "./router.js";
17
- import { ORG_WIDE, S, VIEWS, applyHash, readHash, writeHash } from "./state.js";
17
+ import { ORG_WIDE, S, VIEWS, applyHash, humanLabel, readHash, writeHash } from "./state.js";
18
18
  import { viewBrain } from "./views/brain.js";
19
19
  import { viewChanged } from "./views/changed.js";
20
20
  import { viewDocs } from "./views/docs.js";
@@ -254,6 +254,7 @@ function initStaffFold() {
254
254
  }
255
255
 
256
256
  function paintSidebar() {
257
+ paintInboxLinks();
257
258
  const host = $("#stafflist");
258
259
  host.replaceChildren();
259
260
  paintedRoster = S.data.staff.map((s) => s.handle).join(" ");
@@ -315,7 +316,43 @@ function paintSidebar() {
315
316
  }
316
317
 
317
318
  /** Which rows are lit, and which staff member is unfolded. */
319
+ /* The inbox's filters, as links under Inbox: everything, what is on you, decisions, then one
320
+ per staff member. They replaced two dropdowns on the Inbox screen itself. */
321
+ function paintInboxLinks() {
322
+ const sub = $("#inboxsub");
323
+ if (!sub) return;
324
+ sub.replaceChildren();
325
+ const links = [
326
+ ["", "", "All"],
327
+ ["mine", "", "On " + humanLabel()],
328
+ ["decision", "", "Decisions"],
329
+ ...S.data.staff.map((s) => ["", s.handle, s.name]),
330
+ ];
331
+ for (const [filter, who, label] of links) {
332
+ const b = el("button", { className: "nav subnav inboxlink", textContent: label });
333
+ b.dataset.filter = filter;
334
+ b.dataset.who = who;
335
+ b.onclick = () => {
336
+ S.view = "inbox";
337
+ S.inboxFilter = filter;
338
+ S.inboxStaff = who;
339
+ S.inboxOpen = null;
340
+ render();
341
+ };
342
+ sub.append(b);
343
+ }
344
+ }
345
+
318
346
  function markSidebar() {
347
+ const sub = $("#inboxsub");
348
+ if (sub) sub.hidden = S.view !== "inbox";
349
+ for (const b of document.querySelectorAll(".inboxlink")) {
350
+ const on =
351
+ S.view === "inbox" &&
352
+ b.dataset.filter === (S.inboxFilter || "") &&
353
+ b.dataset.who === (S.inboxStaff || "");
354
+ b.setAttribute("aria-current", String(on));
355
+ }
319
356
  for (const b of document.querySelectorAll(".nav")) {
320
357
  if (!b.dataset.view) continue;
321
358
  const mine = !b.dataset.staff || b.dataset.staff === S.staffHandle;
@@ -122,37 +122,25 @@ function inboxScreen(m, opts) {
122
122
  placeholder: "Filter by title, label, repo…",
123
123
  value: S.query,
124
124
  });
125
- const scope = el("select");
126
- scope.append(
127
- el("option", { value: "", textContent: "Everything" }),
128
- el("option", { value: "mine", textContent: "On " + humanLabel() }),
129
- // A decision is not a PR, so this one is only ever worth offering on the inbox.
130
- ...(opts.prs ? [] : [el("option", { value: "decision", textContent: "Decisions" })]),
131
- );
132
- scope.value = opts.prs && S.inboxFilter !== "mine" ? "" : S.inboxFilter;
133
-
134
- /* Open only by default. An inbox is what is waiting on somebody, and burying that under
135
- five months of finished work would be answering a different question. */
136
- const state = el("select", { title: "Which items to list" });
137
- state.append(
138
- el("option", { value: "", textContent: "Open" }),
139
- el("option", { value: "closed", textContent: "Recently closed" }),
140
- el("option", { value: "all", textContent: "Open and closed" }),
141
- );
142
- state.value = S.inboxState;
143
-
144
- const whose = el("select", {
145
- title: "Whose work: their brain repo, anything their bot wrote, and anything addressed to them",
146
- });
147
- whose.append(
148
- el("option", { value: "", textContent: "Everyone" }),
149
- ...S.data.staff.map((s) => el("option", { value: s.handle, textContent: s.name })),
150
- );
151
- whose.value = S.inboxStaff;
125
+ /* Open or closed, as a toggle. Who and what (a staff member, what is on you, decisions)
126
+ moved to the sidebar under Inbox, where a row of dropdowns used to be. */
127
+ if (S.inboxState === "all") S.inboxState = "";
128
+ const toggle = el("div", { className: "modes inboxstate" });
129
+ for (const [value, label] of [["", "Open"], ["closed", "Closed"]]) {
130
+ const b = el("button", { className: "mode", textContent: label });
131
+ b.classList.toggle("on", (S.inboxState || "") === value);
132
+ b.onclick = () => {
133
+ S.inboxState = value;
134
+ for (const x of toggle.children) x.classList.toggle("on", x === b);
135
+ writeHash(false);
136
+ paint();
137
+ };
138
+ toggle.append(b);
139
+ }
152
140
 
153
141
  const refresh = el("button", { className: "iconbtn", title: "Refresh from GitHub" });
154
142
  refresh.innerHTML = '<span class="sync">' + iconHTML("refresh") + "</span>";
155
- const controls = [search, whose, state, scope, refresh];
143
+ const controls = [search, toggle, refresh];
156
144
  // A pull request comes from a branch, so there is nothing here that could open one.
157
145
  if (!opts.prs) {
158
146
  const newBtn = el("button", { className: "ghbtn", textContent: "New issue" });
@@ -171,9 +159,6 @@ function inboxScreen(m, opts) {
171
159
  m.append(split);
172
160
 
173
161
  search.oninput = () => { S.query = search.value; writeHash(false); paint(); };
174
- scope.onchange = () => { S.inboxFilter = scope.value; writeHash(false); paint(); };
175
- whose.onchange = () => { S.inboxStaff = whose.value; writeHash(false); paint(); };
176
- state.onchange = () => { S.inboxState = state.value; writeHash(false); paint(); };
177
162
  refresh.onclick = () => { refresh.classList.add("spin"); load(true); };
178
163
 
179
164
  if (S.inbox) { stampCounts(); paint(); restore(); } else load(false);
@@ -245,7 +230,9 @@ function inboxScreen(m, opts) {
245
230
  // "On you" is on any of the humans this org answers to, not only the first one.
246
231
  const onAHuman = (i) => (i.assignees ?? []).some(isHuman);
247
232
 
248
- const whoseStaff = S.inboxStaff ? S.data.staff.find((s) => s.handle === S.inboxStaff) : null;
233
+ // The sidebar's filters are the inbox's. Pending work has no control for them, so it ignores them.
234
+ const staffPick = opts.prs ? "" : S.inboxStaff;
235
+ const whoseStaff = staffPick ? S.data.staff.find((s) => s.handle === staffPick) : null;
249
236
  /* The two screens partition the org rather than overlap on it. Pending work is every pull
250
237
  request; the inbox is everything else. An inbox that also listed the PRs said the same
251
238
  thing twice and made the badge beside it a number you could not act on. */
@@ -261,8 +248,9 @@ function inboxScreen(m, opts) {
261
248
  (i) => !q || (i.title + " " + i.repo + " " + i.labels.join(" ") + " #" + i.number)
262
249
  .toLowerCase().includes(q),
263
250
  );
264
- if (S.inboxFilter === "mine") items = items.filter(onAHuman);
265
- else if (S.inboxFilter === "decision") items = items.filter((i) => i.labels.includes("decision"));
251
+ const filter = opts.prs ? "" : S.inboxFilter;
252
+ if (filter === "mine") items = items.filter(onAHuman);
253
+ else if (filter === "decision") items = items.filter((i) => i.labels.includes("decision"));
266
254
  /* `s=pr` was the inbox's "Open work (PRs)" scope, and Pending work is now that screen. An
267
255
  old bookmark carrying it is ignored rather than honoured into an empty list. */
268
256