@gperezmz/bb-plugin-thread-glance 0.3.0 → 0.4.1

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.
@@ -3,7 +3,7 @@
3
3
  "sdkVersion": "0.5.9",
4
4
  "artifactFormatVersion": 1,
5
5
  "pluginId": "thread-glance",
6
- "pluginVersion": "0.3.0",
6
+ "pluginVersion": "0.4.1",
7
7
  "builtWith": {
8
8
  "bbVersion": "0.43.4",
9
9
  "pluginSdkVersion": "0.5.9"
@@ -39,9 +39,7 @@ import { moveGroup, ORDER_PREFERENCE } from "../model/groups";
39
39
  import { MARK_ALL_CONFIRM_ABOVE, type RowMenuAction } from "../model/menu";
40
40
  import { assignProviderMarks, providerMark } from "../model/provider-mark";
41
41
  import { markAllReadPlan, markReadPlanFor, toggleChip, toggleGroup, toggleOlder, toggleSettled, type ToggleOutcome } from "../model/toggles";
42
- import { pullRequestFact, pullRequestLookupIds, type SettleInputs } from "../model/settled";
43
- import { usePullRequestAnswers } from "../data/usePullRequestAnswers";
44
- import { PullRequestProbes } from "./PullRequestProbes";
42
+ import type { SettleInputs } from "../model/settled";
45
43
  import { buildListView, countNeedYou, needYouActive, type GroupView, type ListView } from "../model/view";
46
44
  import { shareView } from "../model/share";
47
45
  import { ListLiveContext, type ListLive, type ModelInfo, type RowController } from "./controller";
@@ -174,21 +172,15 @@ function ThreadListBody({
174
172
  (thread: PluginSidebarThread) => (defaultBranches.has(thread.projectId) ? (defaultBranches.get(thread.projectId) ?? null) : undefined),
175
173
  [defaultBranches],
176
174
  );
177
- const [pullRequests, onPullRequest] = usePullRequestAnswers();
178
175
  const system = useSystemFacts();
179
- const pullRequestLookups = useMemo(
180
- () => (forest === null ? [] : pullRequestLookupIds(forest.infos.values(), defaultBranchOf)),
181
- [forest, defaultBranchOf],
182
- );
183
176
  const settle: SettleInputs = useMemo(
184
177
  () => ({
185
178
  now,
186
179
  settleAfter: prefs.settleAfter,
187
180
  startedAt: stamps.startedAt,
188
181
  finishedAt: stamps.finishedAt,
189
- pullRequestOf: (thread) => pullRequestFact(thread, defaultBranchOf(thread), pullRequests),
190
182
  }),
191
- [now, prefs.settleAfter, stamps.startedAt, stamps.finishedAt, defaultBranchOf, pullRequests],
183
+ [now, prefs.settleAfter, stamps.startedAt, stamps.finishedAt],
192
184
  );
193
185
  // Rows and groups that did not change keep their objects, so their
194
186
  // memoized components skip the render.
@@ -723,7 +715,6 @@ function ThreadListBody({
723
715
  onPrefs={update}
724
716
  onClient={updateClient}
725
717
  />
726
- <PullRequestProbes threadIds={pullRequestLookups} onAnswer={onPullRequest} />
727
718
  {threads.length === 0 ? (
728
719
  // bb's own pinned New thread button covers the empty list.
729
720
  <p className="px-3 py-4 text-sm text-muted-foreground">No threads yet.</p>
@@ -19,12 +19,12 @@ import { chipLabel, rowAriaLabel } from "../model/labels";
19
19
  import { rowIndent } from "../model/layout";
20
20
  import { rowMenuItems } from "../model/menu";
21
21
  import { noteText } from "../model/notes";
22
- import { pluginStatusWins } from "../model/state";
23
- import { trailingTime } from "../model/time";
22
+ import { chipTone, pluginStatusWins } from "../model/state";
23
+ import { TRAILING_SLOT_SIZERS, trailingTime } from "../model/time";
24
24
  import type { ThreadRow } from "../model/view";
25
25
  import type { DraggedThread } from "../model/drag";
26
26
  import type { RowController } from "./controller";
27
- import { ChildDot, GlyphIcon, NoteLine, PluginStatusGlyph } from "./glyphs";
27
+ import { ChipStateGlyph, GlyphIcon, NoteLine, PluginStatusGlyph, TONE_CLASS } from "./glyphs";
28
28
  import { ProviderBadge } from "./ProviderBadge";
29
29
  import { PullRequestBadge } from "./PullRequestBadge";
30
30
  import { RenameEditor } from "./RenameEditor";
@@ -299,8 +299,12 @@ export const ThreadRowView = memo(function ThreadRowView({
299
299
  const twoLines = note !== null || row.branchLine !== null;
300
300
  const dimmed = row.dimmed && !editing;
301
301
  const menuShowing = menuOpen || contextOpen;
302
- // Desktop: the actions cross-fade over the harness and age, as bb's
303
- // trailing slot does. Compact: "…" always shows beside them.
302
+ // A pressed shortcut's pill takes the machine's place, as it takes the time's.
303
+ const showMachine = row.machine !== null && shortcut === null;
304
+ const badgesShown = row.harness || showMachine;
305
+ const actionsShown = !compact && shortcut === null;
306
+ // Desktop: on hover the harness and machine fade out for Mark read and
307
+ // Archive, and the time for "…". Compact: nothing fades.
304
308
  const fadeClass = compact
305
309
  ? ""
306
310
  : menuShowing
@@ -364,7 +368,6 @@ export const ThreadRowView = memo(function ThreadRowView({
364
368
  />
365
369
  <span className="pointer-events-none relative flex size-4 shrink-0 items-center justify-center">
366
370
  {stateSlot}
367
- {row.childDot !== null ? <ChildDot flag={row.childDot} /> : null}
368
371
  </span>
369
372
  {row.nested ? (
370
373
  // Tight against the title, and over the row's gap, so it adds 8px.
@@ -440,141 +443,169 @@ export const ThreadRowView = memo(function ThreadRowView({
440
443
  <PullRequestBadge threadId={thread.id} />
441
444
  </span>
442
445
  ) : null}
443
- {chip !== null && !editing ? (
444
- <button
445
- type="button"
446
- aria-expanded={chip.expanded}
447
- aria-label={chipLabel(thread.displayTitle, chip.count, chip.expanded)}
448
- title={chipLabel(thread.displayTitle, chip.count, chip.expanded)}
449
- onClick={(event) => {
450
- event.preventDefault();
451
- event.stopPropagation();
452
- controller.onToggleChip(row);
453
- }}
454
- onPointerDown={(event) => event.stopPropagation()}
455
- onKeyDown={(event) => event.stopPropagation()}
456
- className={cn(
457
- "pointer-events-auto relative z-10 inline-flex h-5 shrink-0 items-center gap-0.5 rounded-md px-0.5 text-[11px] leading-none tabular-nums text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring",
458
- dimmed && QUIET_TEXT,
459
- )}
460
- >
461
- {chip.count}
462
- <Icon
463
- name={ICONS.expand}
464
- aria-hidden
465
- className={cn("size-3 transition-transform duration-150", chip.expanded && "rotate-90")}
466
- />
467
- </button>
468
- ) : null}
469
- {!editing && row.harness ? (
470
- <span
471
- className={cn(
472
- "pointer-events-none relative inline-flex shrink-0 transition-opacity",
473
- // The hover actions take this slot too, as bb's trailing slot does.
474
- shortcut === null && fadeClass,
475
- )}
476
- >
477
- <ProviderBadge display={provider} mode={controller.harnessIcon} />
478
- </span>
479
- ) : null}
480
- {!editing ? (
481
- <span className="relative flex h-6 min-w-8 shrink-0 items-center justify-end gap-1">
482
- {shortcut !== null ? (
483
- <kbd aria-hidden className="pointer-events-none rounded border border-border px-1 font-sans text-[10px] leading-4 text-muted-foreground">
484
- {shortcut.label}
485
- </kbd>
486
- ) : (
487
- <>
488
- {row.machine !== null ? (
446
+ {!editing && (badgesShown || actionsShown) ? (
447
+ // One cell: the harness and machine at rest, Mark read and Archive
448
+ // on hover. The actions take no width at rest, so no title is
449
+ // shorter for them; on hover the title gives up only what they
450
+ // need beyond the badges they replace.
451
+ <span className="relative -ml-1.5 grid shrink-0 items-center">
452
+ {badgesShown ? (
453
+ <span className={cn("flex items-center gap-1.5 pl-1.5 [grid-area:1/1] transition-opacity", shortcut === null && fadeClass)}>
454
+ {row.harness ? (
455
+ <span className="pointer-events-none relative inline-flex shrink-0">
456
+ <ProviderBadge display={provider} mode={controller.harnessIcon} />
457
+ </span>
458
+ ) : null}
459
+ {showMachine ? (
489
460
  <span
490
461
  title={`On ${row.machine}`}
491
462
  aria-label={`On ${row.machine}`}
492
- className={cn("pointer-events-none max-w-20 truncate text-[11px] text-muted-foreground transition-opacity", fadeClass)}
463
+ className="pointer-events-none max-w-20 truncate text-[11px] text-muted-foreground"
493
464
  >
494
465
  {row.machine}
495
466
  </span>
496
467
  ) : null}
497
- {time !== null ? (
498
- <span
499
- title={time.label}
500
- aria-label={time.label}
501
- className={cn(
502
- "pointer-events-none text-[11px] tabular-nums transition-opacity",
503
- // A running timer reads as work, an age as history.
504
- time.kind === "timer" ? "font-medium text-[var(--timeline-accent)]" : "text-muted-foreground",
505
- fadeClass,
506
- )}
468
+ </span>
469
+ ) : null}
470
+ {actionsShown ? (
471
+ <span
472
+ className={cn(
473
+ "items-center justify-self-end gap-0.5 pl-1.5 [grid-area:1/1]",
474
+ menuShowing ? "flex" : "hidden group-hover/row:flex group-focus-within/row:flex",
475
+ )}
476
+ >
477
+ {row.treeUnread ? (
478
+ <button
479
+ type="button"
480
+ aria-label="Mark read"
481
+ title="Mark read"
482
+ className={ROW_ICON_BUTTON}
483
+ onPointerDown={(event) => event.stopPropagation()}
484
+ onClick={(event) => {
485
+ event.preventDefault();
486
+ event.stopPropagation();
487
+ controller.onMenuAction("mark-read", thread);
488
+ }}
507
489
  >
508
- {time.text}
509
- </span>
490
+ <Icon name={ICONS.markRead} aria-hidden className="size-4" />
491
+ </button>
510
492
  ) : null}
511
- <span
512
- className={cn(
513
- "flex items-center gap-0.5 transition-opacity",
514
- compact
515
- ? "relative"
516
- : menuShowing
517
- ? "absolute right-0 opacity-100"
518
- : "absolute right-0 opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100",
519
- )}
493
+ <button
494
+ type="button"
495
+ aria-label={archived ? "Unarchive thread" : "Archive thread"}
496
+ title={archived ? "Unarchive" : "Archive"}
497
+ className={ROW_ICON_BUTTON}
498
+ onPointerDown={(event) => event.stopPropagation()}
499
+ onClick={(event) => {
500
+ event.preventDefault();
501
+ event.stopPropagation();
502
+ controller.onMenuAction(archived ? "unarchive" : "archive", thread);
503
+ }}
520
504
  >
521
- {!compact && row.treeUnread ? (
522
- <button
523
- type="button"
524
- aria-label="Mark read"
525
- title="Mark read"
526
- className={ROW_ICON_BUTTON}
527
- onPointerDown={(event) => event.stopPropagation()}
528
- onClick={(event) => {
529
- event.preventDefault();
530
- event.stopPropagation();
531
- controller.onMenuAction("mark-read", thread);
532
- }}
533
- >
534
- <Icon name={ICONS.markRead} aria-hidden className="size-4" />
535
- </button>
536
- ) : null}
537
- {!compact ? (
538
- <button
539
- type="button"
540
- aria-label={archived ? "Unarchive thread" : "Archive thread"}
541
- title={archived ? "Unarchive" : "Archive"}
542
- className={ROW_ICON_BUTTON}
543
- onPointerDown={(event) => event.stopPropagation()}
544
- onClick={(event) => {
545
- event.preventDefault();
546
- event.stopPropagation();
547
- controller.onMenuAction(archived ? "unarchive" : "archive", thread);
548
- }}
505
+ <Icon name={archived ? ICONS.unarchive : ICONS.archive} aria-hidden className="size-4" />
506
+ </button>
507
+ </span>
508
+ ) : null}
509
+ </span>
510
+ ) : null}
511
+ {!editing ? (
512
+ // The chip sits 4px before the trailing slot, whose width never
513
+ // changes, so hover never moves it.
514
+ <span className="relative flex shrink-0 items-center gap-1">
515
+ {chip !== null ? (
516
+ <button
517
+ type="button"
518
+ aria-expanded={chip.expanded}
519
+ aria-label={chipLabel(thread.displayTitle, chip)}
520
+ title={chipLabel(thread.displayTitle, chip)}
521
+ onClick={(event) => {
522
+ event.preventDefault();
523
+ event.stopPropagation();
524
+ controller.onToggleChip(row);
525
+ }}
526
+ onPointerDown={(event) => event.stopPropagation()}
527
+ onKeyDown={(event) => event.stopPropagation()}
528
+ className={cn(
529
+ "pointer-events-auto relative z-10 inline-flex h-5 shrink-0 items-center gap-0.5 rounded-md px-0.5 text-[11px] leading-none tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
530
+ chip.flag === null
531
+ ? cn("text-muted-foreground hover:text-foreground", dimmed && QUIET_TEXT)
532
+ : TONE_CLASS[chipTone(chip.flag)],
533
+ )}
534
+ >
535
+ {chip.flag !== null ? <ChipStateGlyph flag={chip.flag} /> : null}
536
+ {chip.count > 0 ? chip.count : null}
537
+ <Icon
538
+ name={ICONS.expand}
539
+ aria-hidden
540
+ className={cn("size-3 transition-transform duration-150", chip.expanded && "rotate-90")}
541
+ />
542
+ </button>
543
+ ) : null}
544
+ {/* The trailing slot: one grid cell, as wide as "…" or the widest
545
+ time, whichever is wider, holding the time at rest and "…" on hover. */}
546
+ <span data-trailing-slot="" className="relative grid h-6 shrink-0 items-center">
547
+ <span aria-hidden className="invisible w-6 [grid-area:1/1]" />
548
+ {TRAILING_SLOT_SIZERS.map((text) => (
549
+ <span key={text} aria-hidden className="invisible text-[11px] font-medium tabular-nums [grid-area:1/1]">
550
+ {text}
551
+ </span>
552
+ ))}
553
+ {shortcut !== null ? (
554
+ <kbd aria-hidden className="pointer-events-none justify-self-end rounded border border-border px-1 font-sans text-[10px] leading-4 text-muted-foreground [grid-area:1/1]">
555
+ {shortcut.label}
556
+ </kbd>
557
+ ) : (
558
+ <>
559
+ {time !== null ? (
560
+ <span
561
+ title={time.label}
562
+ aria-label={time.label}
563
+ className={cn(
564
+ "pointer-events-none justify-self-end text-[11px] tabular-nums transition-opacity [grid-area:1/1]",
565
+ // A running timer reads as work, an age as history.
566
+ time.kind === "timer" ? "font-medium text-[var(--timeline-accent)]" : "text-muted-foreground",
567
+ fadeClass,
568
+ )}
549
569
  >
550
- <Icon name={archived ? ICONS.unarchive : ICONS.archive} aria-hidden className="size-4" />
551
- </button>
570
+ {time.text}
571
+ </span>
552
572
  ) : null}
553
- <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
554
- <DropdownMenuTrigger asChild>
555
- <button
556
- type="button"
557
- aria-label="Thread actions"
558
- // Phones open the menu by long-press, as bb's lists do;
559
- // the button stays for keyboards and screen readers.
560
- className={compact ? "sr-only" : ROW_ICON_BUTTON}
561
- onPointerDown={(event) => event.stopPropagation()}
562
- onClick={(event) => event.stopPropagation()}
563
- >
564
- <Icon name={ICONS.more} aria-hidden className="size-4" />
565
- </button>
566
- </DropdownMenuTrigger>
567
- <RowDropdownMenuContent
568
- items={menuItems}
569
- sections={controller.sections}
570
- currentSectionId={thread.sectionId}
571
- onAction={onMenuAction}
572
- onCloseAutoFocus={onMenuCloseAutoFocus}
573
- />
574
- </DropdownMenu>
575
- </span>
576
- </>
577
- )}
573
+ <span
574
+ className={cn(
575
+ "flex items-center justify-self-end transition-opacity [grid-area:1/1]",
576
+ compact
577
+ ? "relative"
578
+ : menuShowing
579
+ ? "opacity-100"
580
+ : "opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100",
581
+ )}
582
+ >
583
+ <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
584
+ <DropdownMenuTrigger asChild>
585
+ <button
586
+ type="button"
587
+ aria-label="Thread actions"
588
+ // Phones open the menu by long-press, as bb's lists do;
589
+ // the button stays for keyboards and screen readers.
590
+ className={compact ? "sr-only" : ROW_ICON_BUTTON}
591
+ onPointerDown={(event) => event.stopPropagation()}
592
+ onClick={(event) => event.stopPropagation()}
593
+ >
594
+ <Icon name={ICONS.more} aria-hidden className="size-4" />
595
+ </button>
596
+ </DropdownMenuTrigger>
597
+ <RowDropdownMenuContent
598
+ items={menuItems}
599
+ sections={controller.sections}
600
+ currentSectionId={thread.sectionId}
601
+ onAction={onMenuAction}
602
+ onCloseAutoFocus={onMenuCloseAutoFocus}
603
+ />
604
+ </DropdownMenu>
605
+ </span>
606
+ </>
607
+ )}
608
+ </span>
578
609
  </span>
579
610
  ) : null}
580
611
  </div>
@@ -1,11 +1,12 @@
1
- // State glyphs, the child dot and header counters. They draw what the model
2
- // decided; tone and animation come from host token classes only.
1
+ // State glyphs, the children chip's state glyph and header counters. They
2
+ // draw what the model decided; tone and animation come from host token
3
+ // classes only.
3
4
  import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
4
5
  import type { PluginSidebarThreadRowStatus } from "@get-bb/plugin-sdk/app";
5
6
  import { cn } from "@/lib/utils";
6
7
  import type { Counters } from "../model/counters";
7
8
  import type { RowNote } from "../model/notes";
8
- import { dotTone, FLAG_GLYPHS, type DotTone, type Flag, type Glyph, type Tone } from "../model/state";
9
+ import { FLAG_GLYPHS, type Flag, type Glyph, type Tone } from "../model/state";
9
10
 
10
11
  export const TONE_CLASS: Record<Tone, string> = {
11
12
  attention: "text-attention",
@@ -18,32 +19,6 @@ export const TONE_CLASS: Record<Tone, string> = {
18
19
  none: "",
19
20
  };
20
21
 
21
- // The child dot's colours: the tone of the state it stands for, and the
22
- // unread dot's accent for unread.
23
- const DOT_TONE_CLASS: Record<DotTone, string> = {
24
- attention: "bg-attention",
25
- destructive: "bg-destructive",
26
- working: "bg-[var(--timeline-accent)]",
27
- unread: "bg-[var(--timeline-accent)]",
28
- };
29
-
30
- /**
31
- * The child dot on a parent's status glyph, at its lower right corner. A
32
- * ring of the sidebar's colour keeps it apart from the glyph under it.
33
- */
34
- export function ChildDot({ flag }: { flag: Flag }) {
35
- return (
36
- <span
37
- aria-hidden
38
- data-child-dot={flag}
39
- className={cn(
40
- "pointer-events-none absolute -right-0.5 -bottom-0.5 size-[7px] rounded-full ring-[1.5px] ring-sidebar",
41
- DOT_TONE_CLASS[dotTone(flag)],
42
- )}
43
- />
44
- );
45
- }
46
-
47
22
  const SHINE = "animate-shine-icon motion-reduce:animate-none";
48
23
  const SPIN = "animate-spin motion-reduce:animate-none";
49
24
 
@@ -139,6 +114,12 @@ export function FlagGlyph({ flag, className }: { flag: Flag; className?: string
139
114
  return <GlyphIcon glyph={FLAG_GLYPHS[flag]} className={className} />;
140
115
  }
141
116
 
117
+ /** The state glyph that leads a children chip, smaller than the Status column's; the chip's label names it. */
118
+ export function ChipStateGlyph({ flag }: { flag: Flag }) {
119
+ if (FLAG_GLYPHS[flag].icon === "dot") return <UnreadDot className="size-[5px]" />;
120
+ return <FlagGlyph flag={flag} className="size-3" />;
121
+ }
122
+
142
123
  const COUNTER_ITEMS: readonly {
143
124
  key: keyof Counters;
144
125
  flag: Flag;
@@ -1,6 +1,6 @@
1
1
  // Accessible names: everything a tooltip says is in the aria-label.
2
- import type { OlderRow, SettledRow, ThreadRow } from "./view";
3
- import { FLAG_GLYPHS, type Flag } from "./state";
2
+ import type { Chip, OlderRow, SettledRow, ThreadRow } from "./view";
3
+ import type { Flag } from "./state";
4
4
  import { formatDateTime } from "./details";
5
5
  import type { OrganizationMode } from "@/shared/preferences";
6
6
 
@@ -20,7 +20,6 @@ export function rowAriaLabel(
20
20
  ): string {
21
21
  const parts = [stateText(row, options.pluginLabel), options.providerName];
22
22
  if (row.parentTitle !== null && row.depth > 0) parts.push(`child of ${row.parentTitle}`);
23
- if (row.childDot !== null) parts.push(`child threads: ${FLAG_GLYPHS[row.childDot].label}`);
24
23
  if (row.crossGroupLabel !== null) parts.push(row.crossGroupLabel.toLowerCase());
25
24
  if (row.hiddenBadge) parts.push("hidden thread");
26
25
  if (row.info.unread && row.info.state.kind !== "unread") parts.push("unread");
@@ -28,9 +27,24 @@ export function rowAriaLabel(
28
27
  return `Open ${row.info.thread.displayTitle} — ${parts.join("; ")}`;
29
28
  }
30
29
 
31
- export function chipLabel(title: string, count: number, expanded: boolean): string {
32
- const noun = count === 1 ? "child thread" : "child threads";
33
- return `${expanded ? "Collapse" : "Show"} ${count} ${noun} of ${title}`;
30
+ /** What a children chip's state says, uncounted: the state can come from any depth, the count only from direct children. */
31
+ const CHIP_STATE_TEXT: Readonly<Record<Flag, string>> = {
32
+ "waits-on-you": "waiting on you below",
33
+ "unread-failed": "failed below",
34
+ "queue-failed": "queued message failed below",
35
+ offline: "machine offline below",
36
+ working: "working below",
37
+ unread: "unread below",
38
+ };
39
+
40
+ /**
41
+ * "Show 2 child threads of Release, working below"; with no visible child,
42
+ * "Show hidden child threads of Release, waiting on you below".
43
+ */
44
+ export function chipLabel(title: string, chip: Chip): string {
45
+ const children = chip.count === 0 ? "hidden child threads" : `${chip.count} ${chip.count === 1 ? "child thread" : "child threads"}`;
46
+ const state = chip.flag === null ? "" : `, ${CHIP_STATE_TEXT[chip.flag]}`;
47
+ return `${chip.expanded ? "Collapse" : "Show"} ${children} of ${title}${state}`;
34
48
  }
35
49
 
36
50
  /** The text and accessible name of an open tree's fold row. */
@@ -15,46 +15,12 @@ export const SETTLE_AFTER_MS: Readonly<Record<Exclude<SettleAfter, "never">, num
15
15
  "1w": 7 * 24 * HOUR,
16
16
  };
17
17
 
18
- /** A pull request's state, as bb's lookup reports it. */
19
- export type PullRequestState = "open" | "draft" | "merged" | "closed";
20
-
21
- /**
22
- * What settling knows of a thread's pull request: its state, null when the
23
- * thread has none, or `unknown` while a lookup it needs has not answered.
24
- */
25
- export type PullRequestFact = PullRequestState | null | "unknown";
26
-
27
- /**
28
- * Whether a thread's branch can carry a pull request worth looking up: it is
29
- * on a branch other than its project's default. A default branch the lookup
30
- * could not find (null) is no reason to skip one; one still being looked up
31
- * (undefined) is, until it answers.
32
- */
33
- export function needsPullRequestLookup(branch: string | null, defaultBranch: string | null | undefined): boolean {
34
- return branch !== null && defaultBranch !== undefined && branch !== defaultBranch;
35
- }
36
-
37
- /** A thread's pull request fact, from its project's default branch and the lookups that answered. */
38
- export function pullRequestFact(
39
- thread: Pick<PluginSidebarThread, "id" | "environment">,
40
- defaultBranch: string | null | undefined,
41
- answers: ReadonlyMap<string, PullRequestState | null>,
42
- ): PullRequestFact {
43
- const branch = thread.environment?.branchName ?? null;
44
- if (branch === null) return null;
45
- if (defaultBranch === undefined) return "unknown";
46
- if (!needsPullRequestLookup(branch, defaultBranch)) return null;
47
- const answer = answers.get(thread.id);
48
- return answer === undefined ? "unknown" : answer;
49
- }
50
-
51
18
  export interface SettleInputs {
52
19
  now: number;
53
20
  settleAfter: SettleAfter;
54
21
  /** Server stamps: when each thread last started and finished a turn. */
55
22
  startedAt: Readonly<Record<string, number>>;
56
23
  finishedAt: Readonly<Record<string, number>>;
57
- pullRequestOf(thread: PluginSidebarThread): PullRequestFact;
58
24
  }
59
25
 
60
26
  /**
@@ -77,46 +43,18 @@ export function lastActivityAt(
77
43
 
78
44
  /**
79
45
  * A settled thread: quiet as if no thread were open, not needing attention,
80
- * not pinned, with no open pull request, and either its pull request merged
81
- * or closed or its own last activity is older than the Settle after period.
82
- * Hidden and archived threads take the same test.
46
+ * not pinned, and its own last activity is older than the Settle after
47
+ * period. Hidden and archived threads take the same test.
83
48
  */
84
49
  export function isSettledThread(info: ThreadInfo, inputs: SettleInputs): boolean {
85
50
  const thread = info.thread;
86
- if (!mightSettle(info)) return false;
87
- const pullRequest = inputs.pullRequestOf(thread);
88
- if (pullRequest === "unknown" || pullRequest === "open" || pullRequest === "draft") return false;
89
- if (pullRequest === "merged" || pullRequest === "closed") return true;
51
+ if (thread.pinnedAt !== null || thread.isPinned) return false;
52
+ if (!isQuietThread(info.state, info.unread, false) || info.attentionFlags.size > 0) return false;
90
53
  if (inputs.settleAfter === "never") return false;
91
54
  return inputs.now - lastActivityAt(thread, inputs) >= SETTLE_AFTER_MS[inputs.settleAfter];
92
55
  }
93
56
 
94
- /** A row's thread that settles once its pull request and activity allow: quiet as if none were open, needing no attention, not pinned. */
95
- function mightSettle(info: ThreadInfo): boolean {
96
- const thread = info.thread;
97
- if (thread.pinnedAt !== null || thread.isPinned) return false;
98
- return isQuietThread(info.state, info.unread, false) && info.attentionFlags.size === 0;
99
- }
100
-
101
57
  /** A tree settles as one unit: when every thread in it is settled. */
102
58
  export function isSettledTree(tree: Pick<ThreadTree, "root" | "descendants">, inputs: SettleInputs): boolean {
103
59
  return isSettledThread(tree.root, inputs) && tree.descendants.every((info) => isSettledThread(info, inputs));
104
60
  }
105
-
106
- /**
107
- * The threads whose pull request settling has to look up: every thread that
108
- * would settle but for its pull request, on a branch other than its
109
- * project's default.
110
- */
111
- export function pullRequestLookupIds(
112
- infos: Iterable<ThreadInfo>,
113
- defaultBranchOf: (thread: PluginSidebarThread) => string | null | undefined,
114
- ): string[] {
115
- const ids: string[] = [];
116
- for (const info of infos) {
117
- const thread = info.thread;
118
- if (!mightSettle(info)) continue;
119
- if (needsPullRequestLookup(thread.environment?.branchName ?? null, defaultBranchOf(thread))) ids.push(thread.id);
120
- }
121
- return ids;
122
- }
@@ -271,17 +271,17 @@ export function hiddenThreadFlags(flags: ReadonlySet<Flag>): Set<Flag> {
271
271
  return kept;
272
272
  }
273
273
 
274
- /** Flags a hidden thread adds to its parent's child dot: what asks for you or failed, and offline. */
275
- const HIDDEN_DOT_FLAGS: ReadonlySet<Flag> = new Set<Flag>(["waits-on-you", "unread-failed", "queue-failed", "offline"]);
274
+ /** Flags a hidden thread adds to the children chip above it: what asks for you or failed, and offline. */
275
+ const HIDDEN_CHIP_FLAGS: ReadonlySet<Flag> = new Set<Flag>(["waits-on-you", "unread-failed", "queue-failed", "offline"]);
276
276
 
277
277
  /**
278
- * The flags a thread adds to the child dot of the threads above it. A
278
+ * The flags a thread adds to the children chip of the threads above it. A
279
279
  * hidden thread adds only waits-on-you, failed, queued message failed and
280
280
  * offline; which trees need attention reads `hiddenThreadFlags` instead,
281
281
  * so this leaves it unchanged.
282
282
  */
283
- export function dotFlagsOf(flags: ReadonlySet<Flag>, isHidden: boolean): Set<Flag> {
284
- return isHidden ? new Set([...flags].filter((flag) => HIDDEN_DOT_FLAGS.has(flag))) : new Set(flags);
283
+ export function chipFlagsOf(flags: ReadonlySet<Flag>, isHidden: boolean): Set<Flag> {
284
+ return isHidden ? new Set([...flags].filter((flag) => HIDDEN_CHIP_FLAGS.has(flag))) : new Set(flags);
285
285
  }
286
286
 
287
287
  /** The most urgent flag, or null. */
@@ -289,22 +289,10 @@ export function mostUrgent(flags: ReadonlySet<Flag>): Flag | null {
289
289
  return FLAG_ORDER.find((flag) => flags.has(flag)) ?? null;
290
290
  }
291
291
 
292
- /** The colour of a child dot, by the most urgent state among the descendants. */
293
- export type DotTone = "attention" | "destructive" | "working" | "unread";
294
-
295
- export function dotTone(flag: Flag): DotTone {
296
- switch (flag) {
297
- case "waits-on-you":
298
- case "offline":
299
- return "attention";
300
- case "unread-failed":
301
- case "queue-failed":
302
- return "destructive";
303
- case "working":
304
- return "working";
305
- case "unread":
306
- return "unread";
307
- }
292
+ /** The tone a children chip takes for a state: its glyph's, and the unread dot's accent for unread. */
293
+ export function chipTone(flag: Flag): Tone {
294
+ const tone = FLAG_GLYPHS[flag].tone;
295
+ return tone === "none" ? "working" : tone;
308
296
  }
309
297
 
310
298
  export const FLAG_GLYPHS: Readonly<Record<Flag, Glyph & { label: string }>> = {