@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.
- package/dist/app.css +1 -1
- package/dist/app.js +29 -29
- package/dist/app.meta.json +1 -1
- package/dist/server.js.map +1 -1
- package/dist/server.meta.json +1 -1
- package/features/thread-list/components/ThreadList.tsx +2 -11
- package/features/thread-list/components/ThreadRowView.tsx +160 -129
- package/features/thread-list/components/glyphs.tsx +10 -29
- package/features/thread-list/model/labels.ts +20 -6
- package/features/thread-list/model/settled.ts +4 -66
- package/features/thread-list/model/state.ts +9 -21
- package/features/thread-list/model/time.ts +7 -0
- package/features/thread-list/model/trees.ts +11 -11
- package/features/thread-list/model/view.ts +14 -12
- package/features/thread-list/testing/browser.css +13 -0
- package/features/thread-list/testing/fixtures.ts +1 -7
- package/package.json +5 -1
- package/skills/thread-glance/SKILL.md +1 -1
- package/features/thread-list/components/PullRequestProbes.tsx +0 -27
- package/features/thread-list/data/usePullRequestAnswers.ts +0 -36
package/dist/server.meta.json
CHANGED
|
@@ -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 {
|
|
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
|
|
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 {
|
|
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
|
-
//
|
|
303
|
-
|
|
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
|
-
{
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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=
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
{
|
|
509
|
-
</
|
|
490
|
+
<Icon name={ICONS.markRead} aria-hidden className="size-4" />
|
|
491
|
+
</button>
|
|
510
492
|
) : null}
|
|
511
|
-
<
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
{
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
551
|
-
</
|
|
570
|
+
{time.text}
|
|
571
|
+
</span>
|
|
552
572
|
) : null}
|
|
553
|
-
<
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
2
|
-
// decided; tone and animation come from host token
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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,
|
|
81
|
-
*
|
|
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 (
|
|
87
|
-
|
|
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
|
|
275
|
-
const
|
|
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
|
|
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
|
|
284
|
-
return isHidden ? new Set([...flags].filter((flag) =>
|
|
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
|
|
293
|
-
export
|
|
294
|
-
|
|
295
|
-
|
|
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 }>> = {
|