@nanocollective/roster 0.1.0-alpha.39 → 0.1.0-alpha.40

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/dist/cli.js CHANGED
@@ -5552,7 +5552,6 @@ var TIMELINE_PR = `
5552
5552
  }
5553
5553
  `;
5554
5554
  var CLOSED_FIRST = 30;
5555
- var CLOSED_TIMELINE = 40;
5556
5555
  var CLOSED_DAYS = 45;
5557
5556
  var ISSUE_TYPES = `[ISSUE_COMMENT, CROSS_REFERENCED_EVENT, REFERENCED_EVENT, CLOSED_EVENT,
5558
5557
  REOPENED_EVENT, LABELED_EVENT, UNLABELED_EVENT, ASSIGNED_EVENT, UNASSIGNED_EVENT,
@@ -5561,58 +5560,46 @@ var PR_TYPES = `[ISSUE_COMMENT, CROSS_REFERENCED_EVENT, REFERENCED_EVENT, CLOSED
5561
5560
  REOPENED_EVENT, MERGED_EVENT, LABELED_EVENT, UNLABELED_EVENT, ASSIGNED_EVENT,
5562
5561
  UNASSIGNED_EVENT, RENAMED_TITLE_EVENT, PULL_REQUEST_REVIEW, READY_FOR_REVIEW_EVENT,
5563
5562
  REVIEW_REQUESTED_EVENT]`;
5563
+ var LIGHT = `number title url state createdAt updatedAt
5564
+ author { login }
5565
+ labels(first:12) { nodes { name } }
5566
+ assignees(first:8) { nodes { login } }
5567
+ comments { totalCount }`;
5568
+ var LIGHT_PR = `${LIGHT} isDraft mergeable
5569
+ commits(last:1) { nodes { commit { statusCheckRollup { state } } } }`;
5564
5570
  var QUERY = `
5565
5571
  query($owner:String!, $name:String!) {
5566
5572
  repository(owner:$owner, name:$name) {
5567
- issues(states:OPEN, first:60, orderBy:{field:UPDATED_AT, direction:DESC}) {
5568
- nodes {
5573
+ issues(states:OPEN, first:60, orderBy:{field:UPDATED_AT, direction:DESC}) { nodes { ${LIGHT} } }
5574
+ pullRequests(states:OPEN, first:60, orderBy:{field:UPDATED_AT, direction:DESC}) { nodes { ${LIGHT_PR} } }
5575
+ closedIssues: issues(states:CLOSED, first:${CLOSED_FIRST}, orderBy:{field:UPDATED_AT, direction:DESC}) {
5576
+ nodes { ${LIGHT} }
5577
+ }
5578
+ closedPullRequests: pullRequests(
5579
+ states:[CLOSED, MERGED], first:${CLOSED_FIRST}, orderBy:{field:UPDATED_AT, direction:DESC}
5580
+ ) { nodes { ${LIGHT_PR} } }
5581
+ }
5582
+ }`;
5583
+ var THREAD_QUERY = `
5584
+ query($owner:String!, $name:String!, $number:Int!) {
5585
+ repository(owner:$owner, name:$name) {
5586
+ issueOrPullRequest(number:$number) {
5587
+ ... on Issue {
5569
5588
  number title body url state createdAt updatedAt
5570
5589
  author { login }
5571
5590
  ${REACTIONS}
5572
5591
  labels(first:12) { nodes { name } }
5573
5592
  assignees(first:8) { nodes { login } }
5574
- timelineItems(last:80, itemTypes:${ISSUE_TYPES}) { nodes { ${TIMELINE_COMMON} } }
5593
+ timelineItems(last:100, itemTypes:${ISSUE_TYPES}) { nodes { ${TIMELINE_COMMON} } }
5575
5594
  }
5576
- }
5577
- pullRequests(states:OPEN, first:60, orderBy:{field:UPDATED_AT, direction:DESC}) {
5578
- nodes {
5595
+ ... on PullRequest {
5579
5596
  number title body url state createdAt updatedAt isDraft mergeable
5580
5597
  author { login }
5581
5598
  ${REACTIONS}
5582
5599
  labels(first:12) { nodes { name } }
5583
5600
  assignees(first:8) { nodes { login } }
5584
5601
  commits(last:1) { nodes { commit { statusCheckRollup { state } } } }
5585
- timelineItems(last:80, itemTypes:${PR_TYPES}) {
5586
- nodes { ${TIMELINE_COMMON} ${TIMELINE_PR} }
5587
- }
5588
- }
5589
- }
5590
- closedIssues: issues(
5591
- states:CLOSED, first:${CLOSED_FIRST}, orderBy:{field:UPDATED_AT, direction:DESC}
5592
- ) {
5593
- nodes {
5594
- number title body url state createdAt updatedAt
5595
- author { login }
5596
- ${REACTIONS}
5597
- labels(first:12) { nodes { name } }
5598
- assignees(first:8) { nodes { login } }
5599
- timelineItems(last:${CLOSED_TIMELINE}, itemTypes:${ISSUE_TYPES}) {
5600
- nodes { ${TIMELINE_COMMON} }
5601
- }
5602
- }
5603
- }
5604
- closedPullRequests: pullRequests(
5605
- states:[CLOSED, MERGED], first:${CLOSED_FIRST}, orderBy:{field:UPDATED_AT, direction:DESC}
5606
- ) {
5607
- nodes {
5608
- number title body url state createdAt updatedAt isDraft
5609
- author { login }
5610
- ${REACTIONS}
5611
- labels(first:12) { nodes { name } }
5612
- assignees(first:8) { nodes { login } }
5613
- timelineItems(last:${CLOSED_TIMELINE}, itemTypes:${PR_TYPES}) {
5614
- nodes { ${TIMELINE_COMMON} ${TIMELINE_PR} }
5615
- }
5602
+ timelineItems(last:100, itemTypes:${PR_TYPES}) { nodes { ${TIMELINE_COMMON} ${TIMELINE_PR} } }
5616
5603
  }
5617
5604
  }
5618
5605
  }
@@ -5680,6 +5667,9 @@ function shape(n, repo, role, kind) {
5680
5667
  url: n.url,
5681
5668
  state: n.state ?? "OPEN",
5682
5669
  comments: events.filter((e) => e.type === "comment").map((e) => ({ author: e.actor, createdAt: e.createdAt, body: e.body ?? "" })),
5670
+ // From the list, the count comes without the comments; from a thread, they are counted.
5671
+ commentCount: n.comments?.totalCount ?? events.filter((e) => e.type === "comment").length,
5672
+ partial: !n.timelineItems,
5683
5673
  events,
5684
5674
  reactions: reactions(n)
5685
5675
  };
@@ -5794,13 +5784,31 @@ function event(e) {
5794
5784
  }
5795
5785
  async function fetchThread(repo, number, kind) {
5796
5786
  const [owner, name] = repo.split("/");
5797
- const data = await query(owner, name);
5798
- const lists = kind === "pr" ? [data?.pullRequests?.nodes, data?.closedPullRequests?.nodes] : [data?.issues?.nodes, data?.closedIssues?.nodes];
5799
- for (const nodes of lists) {
5800
- const found = (nodes ?? []).find((n) => n.number === number);
5801
- if (found) return shape(found, repo, "", kind);
5802
- }
5803
- throw new Error(`${repo}#${number} was not found in ${repo}`);
5787
+ const { stdout } = await run4(
5788
+ "gh",
5789
+ [
5790
+ "api",
5791
+ "graphql",
5792
+ "-f",
5793
+ `query=${THREAD_QUERY}`,
5794
+ "-F",
5795
+ `owner=${owner}`,
5796
+ "-F",
5797
+ `name=${name}`,
5798
+ "-F",
5799
+ `number=${number}`
5800
+ ],
5801
+ { maxBuffer: 64 * 1024 * 1024 }
5802
+ );
5803
+ const n = JSON.parse(stdout)?.data?.repository?.issueOrPullRequest;
5804
+ if (!n) throw new Error(`${repo}#${number} was not found in ${repo}`);
5805
+ const item = shape(n, repo, "", kind);
5806
+ if (kind === "pr") {
5807
+ item.draft = n.isDraft;
5808
+ item.checks = rollup(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state);
5809
+ item.mergeable = n.mergeable;
5810
+ }
5811
+ return item;
5804
5812
  }
5805
5813
  function rollup(state) {
5806
5814
  if (!state) return "none";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.39",
3
+ "version": "0.1.0-alpha.40",
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": {
@@ -92,6 +92,10 @@ export function belongsTo(item, s) {
92
92
  return false;
93
93
  }
94
94
 
95
+ /* Threads read so far, by repo#number, kept for the session. Each is reused while its
96
+ updatedAt matches the list's, so a thread is fetched again only when it has changed. */
97
+ const THREADS = new Map();
98
+
95
99
  export const viewInbox = (m) => inboxScreen(m, {});
96
100
 
97
101
  /**
@@ -337,9 +341,9 @@ function inboxScreen(m, opts) {
337
341
  : "") +
338
342
  // How much conversation is on a thread, which is most of what tells a live one from
339
343
  // something that was filed and never answered.
340
- (i.comments.length
341
- ? '<span class="cc" title="' + i.comments.length + ' comments">' +
342
- iconHTML("review") + i.comments.length + "</span>"
344
+ ((i.commentCount ?? i.comments.length)
345
+ ? '<span class="cc" title="' + (i.commentCount ?? i.comments.length) + ' comments">' +
346
+ iconHTML("review") + (i.commentCount ?? i.comments.length) + "</span>"
343
347
  : "") +
344
348
  chips +
345
349
  '<span class="when">' + ago(i.updatedAt) + "</span>" +
@@ -378,6 +382,36 @@ function inboxScreen(m, opts) {
378
382
  post({ thread, ref: item.repo + "#" + item.number }, "/api/notifications/read").catch(() => {});
379
383
  }
380
384
 
385
+ /* The list carries no bodies or timelines (carrying them cost most of an hour's GitHub
386
+ allowance per load), so a thread is read when it is opened, and kept until GitHub says
387
+ it has changed. Reopening one costs nothing. */
388
+ if (item.partial) {
389
+ const key = item.repo + "#" + item.number;
390
+ const kept = THREADS.get(key);
391
+ const keep = { role: item.role, unread: item.unread };
392
+ if (kept && kept.updatedAt === item.updatedAt) {
393
+ Object.assign(item, kept, keep, { partial: false });
394
+ } else {
395
+ viewer.dataset.want = key;
396
+ viewer.replaceChildren(...skeleton("head", 1), ...skeleton("line", 8));
397
+ getThread(item.repo, item.number, item.kind)
398
+ .then((fresh) => {
399
+ if (!fresh || fresh.error) throw new Error(fresh?.error ?? "could not read it");
400
+ THREADS.set(key, fresh);
401
+ Object.assign(item, fresh, keep, { partial: false });
402
+ })
403
+ .catch((e) => {
404
+ item.partial = false;
405
+ item.loadError = String(e.message || e);
406
+ })
407
+ .finally(() => {
408
+ // Somebody may have opened another thread while this one was loading.
409
+ if (viewer.dataset.want === key) openThread(ref);
410
+ });
411
+ return;
412
+ }
413
+ }
414
+
381
415
  const head = el("div", { className: "thead" });
382
416
  head.innerHTML =
383
417
  '<div class="meta">' + esc(item.repo) + " · " + (item.kind === "pr" ? "PR " : "") + "#" + item.number +
@@ -406,6 +440,11 @@ function inboxScreen(m, opts) {
406
440
  for a wrapper that would only ever hold one thing. */
407
441
  const pane = item.kind === "pr" ? el("div") : viewer;
408
442
  if (item.kind === "pr") viewer.append(prTabs(item, pane), pane);
443
+ if (item.loadError) {
444
+ pane.append(el("p", { className: "err", textContent: "Couldn't load the conversation: " + item.loadError }));
445
+ delete item.loadError;
446
+ item.partial = true; // so opening it again tries again
447
+ }
409
448
  conversation(pane, item);
410
449
  viewer.scrollTop = 0;
411
450
  }
@@ -762,7 +801,12 @@ function inboxScreen(m, opts) {
762
801
  const at = S.inbox.items.findIndex(
763
802
  (i) => i.repo === item.repo && i.number === item.number,
764
803
  );
765
- if (at >= 0) S.inbox.items[at] = { ...S.inbox.items[at], ...fresh };
804
+ if (at >= 0) {
805
+ const old = S.inbox.items[at];
806
+ THREADS.set(item.repo + "#" + item.number, fresh);
807
+ // A single thread does not know which part of the org it is in, or whether it is unread.
808
+ S.inbox.items[at] = { ...old, ...fresh, role: old.role, unread: old.unread, partial: false };
809
+ }
766
810
  }
767
811
  } catch {
768
812
  /* the comment posted; a stale pane is not worth an error */