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

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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/app.tsx +2 -1
  3. package/dist/app.css +1 -1
  4. package/dist/app.js +31 -31
  5. package/dist/app.meta.json +1 -1
  6. package/dist/server.js +10 -10
  7. package/dist/server.js.map +3 -3
  8. package/dist/server.meta.json +1 -1
  9. package/features/thread-list/components/FoldRows.tsx +21 -30
  10. package/features/thread-list/components/GroupSection.tsx +8 -50
  11. package/features/thread-list/components/ListHeader.tsx +79 -0
  12. package/features/thread-list/components/ProviderBadge.tsx +0 -1
  13. package/features/thread-list/components/PullRequestProbes.tsx +27 -0
  14. package/features/thread-list/components/{Toolbar.tsx → SettingsPanel.tsx} +26 -57
  15. package/features/thread-list/components/ThreadDetails.tsx +3 -3
  16. package/features/thread-list/components/ThreadList.tsx +116 -84
  17. package/features/thread-list/components/ThreadRowView.tsx +70 -78
  18. package/features/thread-list/components/controller.ts +5 -10
  19. package/features/thread-list/components/glyphs.tsx +39 -14
  20. package/features/thread-list/data/same-window.ts +34 -0
  21. package/features/thread-list/data/useAutoExpand.ts +1 -1
  22. package/features/thread-list/data/useClientPreferences.ts +15 -7
  23. package/features/thread-list/data/useDefaultBranches.ts +35 -0
  24. package/features/thread-list/data/usePreferences.ts +16 -7
  25. package/features/thread-list/data/usePullRequestAnswers.ts +36 -0
  26. package/features/thread-list/data/useSystemFacts.ts +52 -0
  27. package/features/thread-list/icons.ts +1 -1
  28. package/features/thread-list/model/attention.ts +2 -66
  29. package/features/thread-list/model/branches.ts +27 -0
  30. package/features/thread-list/model/counters.ts +5 -5
  31. package/features/thread-list/model/details.ts +6 -6
  32. package/features/thread-list/model/expansion.ts +5 -5
  33. package/features/thread-list/model/groups.ts +2 -2
  34. package/features/thread-list/model/labels.ts +28 -17
  35. package/features/thread-list/model/layout.ts +5 -2
  36. package/features/thread-list/model/menu.ts +1 -0
  37. package/features/thread-list/model/move.ts +1 -1
  38. package/features/thread-list/model/notes.ts +2 -28
  39. package/features/thread-list/model/settings.ts +2 -18
  40. package/features/thread-list/model/settled.ts +122 -0
  41. package/features/thread-list/model/share.ts +1 -7
  42. package/features/thread-list/model/sort.ts +5 -16
  43. package/features/thread-list/model/state.ts +25 -9
  44. package/features/thread-list/model/toggles.ts +35 -24
  45. package/features/thread-list/model/{families.ts → trees.ts} +40 -28
  46. package/features/thread-list/model/view.ts +200 -274
  47. package/features/thread-list/testing/fixtures.ts +41 -12
  48. package/package.json +1 -1
  49. package/server/cli.ts +1 -1
  50. package/server/import.ts +36 -6
  51. package/server/preference-store.ts +2 -1
  52. package/server.ts +1 -1
  53. package/shared/preferences.ts +38 -27
  54. package/skills/thread-glance/SKILL.md +11 -12
  55. package/features/thread-list/data/useAttentionHold.ts +0 -19
@@ -3,7 +3,7 @@
3
3
  "sdkVersion": "0.5.9",
4
4
  "artifactFormatVersion": 1,
5
5
  "pluginId": "thread-glance",
6
- "pluginVersion": "0.2.3",
6
+ "pluginVersion": "0.3.0",
7
7
  "builtWith": {
8
8
  "bbVersion": "0.43.4",
9
9
  "pluginSdkVersion": "0.5.9"
@@ -1,5 +1,5 @@
1
- // The fold rows (`N older` on a group, `N more child threads` on a family),
2
- // the "+N more" line in Needs attention, and the environment folder row.
1
+ // The fold rows (a group's settled fold, `N more child threads` in a tree)
2
+ // and the environment folder row.
3
3
  import { memo, useState } from "react";
4
4
  import { experimental_Icon as Icon, experimental_ProviderIcon as ProviderIcon } from "@get-bb/plugin-sdk/app";
5
5
  import type { PluginEnvironmentProvider } from "@get-bb/plugin-sdk/app";
@@ -11,9 +11,9 @@ import {
11
11
  } from "@/components/ui/dropdown-menu";
12
12
  import { cn } from "@/lib/utils";
13
13
  import { ICONS } from "../icons";
14
- import { olderRowText } from "../model/labels";
14
+ import { olderRowText, settledRowText } from "../model/labels";
15
15
  import { rowIndent } from "../model/layout";
16
- import type { EnvironmentRow, LeftOutRow, OlderRow } from "../model/view";
16
+ import type { EnvironmentRow, OlderRow, SettledRow } from "../model/view";
17
17
  import type { RowController } from "./controller";
18
18
  import { FlagGlyph } from "./glyphs";
19
19
  import { RenameEditor } from "./RenameEditor";
@@ -21,48 +21,39 @@ import { ROW_ICON_BUTTON } from "./ThreadRowView";
21
21
 
22
22
  export const OlderRowView = memo(function OlderRowView({ row, controller }: { row: OlderRow; controller: RowController }) {
23
23
  const { label, ariaLabel } = olderRowText(row);
24
- // A family's fold sits where its children do, with a dots glyph in the
25
- // status slot; the group's fold keeps the chevron.
26
- const inFamily = row.scope !== "group";
24
+ // It sits where the children do, with a dots glyph in the Status column.
27
25
  return (
28
26
  <button
29
27
  type="button"
30
28
  aria-expanded={row.expanded}
31
29
  aria-label={ariaLabel}
32
30
  onClick={() => controller.onToggleOlder(row)}
33
- className={cn(
34
- "relative flex w-full items-center rounded-md pr-2 text-left text-xs text-muted-foreground outline-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring",
35
- inFamily ? "h-7 gap-1.5 max-md:pointer-coarse:h-9" : "h-6 gap-1",
36
- )}
37
- style={{ paddingLeft: rowIndent(row.depth) + (inFamily ? 0 : 2) }}
31
+ className="relative flex h-7 w-full items-center gap-1.5 rounded-md pr-2 text-left text-xs text-muted-foreground outline-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring max-md:pointer-coarse:h-9"
32
+ style={{ paddingLeft: rowIndent(row.depth) }}
38
33
  >
39
- {inFamily ? (
40
- <span aria-hidden className="inline-flex size-4 shrink-0 items-center justify-center">
41
- <Icon name={ICONS.more} className="size-3.5" />
42
- </span>
43
- ) : (
44
- <Icon name={ICONS.expand} aria-hidden className={cn("size-3 transition-transform", row.expanded && "-rotate-90")} />
45
- )}
34
+ <span aria-hidden className="inline-flex size-4 shrink-0 items-center justify-center">
35
+ <Icon name={ICONS.more} className="size-3.5" />
36
+ </span>
46
37
  {label}
47
38
  </button>
48
39
  );
49
40
  });
50
41
 
51
- /** The "+N more" line under a closed family in Needs attention: it opens the family, as the chip does. */
52
- export const LeftOutRowView = memo(function LeftOutRowView({ row, controller }: { row: LeftOutRow; controller: RowController }) {
53
- const noun = row.count === 1 ? "child thread" : "child threads";
42
+ /** The settled fold: a faint label, a hairline to the row's end, and a chevron. */
43
+ export const SettledRowView = memo(function SettledRowView({ row, controller }: { row: SettledRow; controller: RowController }) {
44
+ const { label, ariaLabel } = settledRowText(row);
54
45
  return (
55
46
  <button
56
47
  type="button"
57
- aria-expanded={false}
58
- aria-label={`Show ${row.count} more ${noun}`}
59
- title={`Show ${row.count} more ${noun}`}
60
- onClick={() => controller.onOpenChildren(row.scopeId)}
61
- className="relative flex h-6 w-full items-center gap-1.5 rounded-md pr-2 text-left text-xs text-muted-foreground outline-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring"
62
- style={{ paddingLeft: rowIndent(row.depth) }}
48
+ aria-expanded={row.expanded}
49
+ aria-label={ariaLabel}
50
+ onClick={() => controller.onToggleSettled(row)}
51
+ className="group/settled relative flex h-6 w-full items-center gap-2 rounded-md pr-1 text-left text-[11px] text-muted-foreground/70 outline-none hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring max-md:pointer-coarse:h-9"
52
+ style={{ paddingLeft: rowIndent(0) }}
63
53
  >
64
- <span aria-hidden className="size-4 shrink-0" />
65
- +{row.count} more
54
+ <span className="shrink-0 tabular-nums">{label}</span>
55
+ <span aria-hidden className="h-px flex-1 bg-border/60" />
56
+ <Icon name={ICONS.expand} aria-hidden className={cn("size-3 shrink-0 transition-transform duration-150", row.expanded && "rotate-90")} />
66
57
  </button>
67
58
  );
68
59
  });
@@ -1,5 +1,5 @@
1
1
  // A top-level group: its header with counters, and its rows, windowed
2
- // in chunks; and the Needs attention section above every group.
2
+ // in chunks.
3
3
  import { memo, useEffect, useRef, useState } from "react";
4
4
  import { useDraggable, useDroppable } from "@dnd-kit/core";
5
5
  import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
@@ -14,10 +14,10 @@ import {
14
14
  } from "@/components/ui/dropdown-menu";
15
15
  import { cn } from "@/lib/utils";
16
16
  import { ICONS } from "../icons";
17
- import { ATTENTION_GROUP_ID, type GroupView, type AttentionView, type Row } from "../model/view";
17
+ import type { GroupView, Row } from "../model/view";
18
18
  import { chunk, windowedNavValue } from "../model/windowing";
19
19
  import type { RowController } from "./controller";
20
- import { EnvironmentRowView, LeftOutRowView, OlderRowView } from "./FoldRows";
20
+ import { EnvironmentRowView, OlderRowView, SettledRowView } from "./FoldRows";
21
21
  import { CounterStrip } from "./glyphs";
22
22
  import { visibleCounters } from "../model/counters";
23
23
  import { RenameEditor } from "./RenameEditor";
@@ -43,8 +43,6 @@ export interface GroupController {
43
43
 
44
44
  export type DropStates = ReadonlyMap<string, "valid" | "blocked" | "unchanged" | "before" | "after">;
45
45
 
46
- /** The sticky toolbar's height; headers stick below it. */
47
- export const TOOLBAR_HEIGHT = 28;
48
46
  const ROW_HEIGHT = { compact: 28, comfortable: 44 };
49
47
 
50
48
  function canRename(group: GroupView): boolean {
@@ -96,9 +94,8 @@ const GroupHeader = memo(function GroupHeader({
96
94
  role={undefined}
97
95
  tabIndex={undefined}
98
96
  data-sidebar="group-label"
99
- style={{ top: inOverflow ? 0 : TOOLBAR_HEIGHT }}
100
97
  className={cn(
101
- "group/header sticky z-20 flex h-7 items-center gap-1 rounded-md bg-sidebar pl-2 pr-1 text-xs text-muted-foreground max-md:pointer-coarse:h-9",
98
+ "group/header sticky top-0 z-20 flex h-7 items-center gap-1 rounded-md bg-sidebar pl-2 pr-1 text-xs text-muted-foreground max-md:pointer-coarse:h-9",
102
99
  dropActive && "bg-sidebar-accent",
103
100
  draggable.isDragging && "opacity-50",
104
101
  !controller.compact && !inOverflow && "select-none",
@@ -346,8 +343,8 @@ function Rows({
346
343
  />
347
344
  ) : row.type === "older" ? (
348
345
  <OlderRowView key={row.key} row={row} controller={rowController} />
349
- ) : row.type === "left-out" ? (
350
- <LeftOutRowView key={row.key} row={row} controller={rowController} />
346
+ ) : row.type === "settled" ? (
347
+ <SettledRowView key={row.key} row={row} controller={rowController} />
351
348
  ) : (
352
349
  <EnvironmentRowView
353
350
  key={row.key}
@@ -395,9 +392,8 @@ export const GroupSection = memo(function GroupSection({
395
392
  inOverflow={inOverflow}
396
393
  dropActive={dropTargetGroupId === group.descriptor.id}
397
394
  />
398
- {group.collapsed ? null : group.rows.length === 0 ? (
399
- // A group whose every family sits in Needs attention draws only its header.
400
- group.rootIds.length === 0 ? <p className="py-1 pl-8 text-xs text-muted-foreground">No threads</p> : null
395
+ {group.rows.length === 0 ? (
396
+ group.collapsed || group.rootIds.length > 0 ? null : <p className="py-1 pl-8 text-xs text-muted-foreground">No threads</p>
401
397
  ) : (
402
398
  <Rows
403
399
  {...rest}
@@ -410,41 +406,3 @@ export const GroupSection = memo(function GroupSection({
410
406
  </section>
411
407
  );
412
408
  });
413
-
414
- // bb's attention colour, thinned over the sidebar: the one colour the section
415
- // adds. Opaque, so the sticky header hides the rows scrolling under it. Mixed
416
- // in oklab: the sidebar's grey has hue 0 in oklch, which would pull the tint
417
- // toward pink. Set as the surface quiet titles mix toward; the band is
418
- // lighter than the sidebar in the dark theme, so they keep 70% of the
419
- // foreground there to stay above 4.5:1 (68% measured 4.44:1).
420
- const ATTENTION_BAND =
421
- "[--tg-surface:color-mix(in_oklab,var(--attention)_8%,var(--sidebar))] [--tg-quiet:70%] bg-[var(--tg-surface)]";
422
- const ATTENTION_COUNT =
423
- "bg-[color-mix(in_oklab,var(--attention)_20%,transparent)] text-[color:color-mix(in_oklab,var(--attention),var(--foreground)_55%)]";
424
-
425
- /**
426
- * The Needs attention section: a header with its family count (none when every
427
- * family in it is attended), then its rows, on one band of bb's attention
428
- * colour. It has no collapse, menu or drag, and no drop target of its own.
429
- */
430
- export const AttentionSection = memo(function AttentionSection({ view, ...rest }: SectionProps & { view: AttentionView }) {
431
- return (
432
- <section aria-label="Needs attention" className={cn("relative mb-1 flex w-full min-w-0 flex-col rounded-md pb-0.5", ATTENTION_BAND)}>
433
- <h2
434
- style={{ top: TOOLBAR_HEIGHT }}
435
- className="sticky z-20 flex h-7 items-center gap-1 rounded-t-md bg-[var(--tg-surface)] pl-2 pr-1 text-xs font-medium text-muted-foreground max-md:pointer-coarse:h-9"
436
- >
437
- <span className="min-w-0 flex-1 truncate">Needs attention</span>
438
- {view.familyCount > 0 ? (
439
- <span
440
- className={cn("rounded-full px-1.5 text-[11px] leading-4 tabular-nums", ATTENTION_COUNT)}
441
- aria-label={`${view.familyCount} ${view.familyCount === 1 ? "family" : "families"}`}
442
- >
443
- {view.familyCount}
444
- </span>
445
- ) : null}
446
- </h2>
447
- <Rows {...rest} rows={view.rows} groupId={ATTENTION_GROUP_ID} inPinned={false} forceMount={false} />
448
- </section>
449
- );
450
- });
@@ -0,0 +1,79 @@
1
+ // The list header: the grouping's name, the need-you filter, Mark all read
2
+ // and the settings button, above every group.
3
+ import { memo, useState } from "react";
4
+ import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
5
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
6
+ import { cn } from "@/lib/utils";
7
+ import type { ClientPreferences, OrganizationMode, Preferences } from "@/shared/preferences";
8
+ import { ICONS } from "../icons";
9
+ import { groupingName } from "../model/labels";
10
+ import { SettingsPanel } from "./SettingsPanel";
11
+ import { ROW_ICON_BUTTON } from "./ThreadRowView";
12
+
13
+ // bb's attention colour, thinned, as the need-you filter's own: the list's
14
+ // one place to see what waits on you.
15
+ const NEED_YOU =
16
+ "bg-[color-mix(in_oklab,var(--attention)_14%,transparent)] text-[color:color-mix(in_oklab,var(--attention),var(--foreground)_55%)] hover:bg-[color-mix(in_oklab,var(--attention)_24%,transparent)]";
17
+ const NEED_YOU_ON = "bg-[color-mix(in_oklab,var(--attention)_30%,transparent)] text-foreground";
18
+
19
+ export interface ListHeaderProps {
20
+ mode: OrganizationMode;
21
+ /** Thread trees that need attention; the filter is not drawn at 0. */
22
+ needYouCount: number;
23
+ needYouOnly: boolean;
24
+ onToggleNeedYou(): void;
25
+ onMarkAllRead(): void;
26
+ prefs: Preferences;
27
+ client: ClientPreferences;
28
+ onPrefs(patch: Partial<Preferences>): void;
29
+ onClient(patch: Partial<ClientPreferences>): void;
30
+ }
31
+
32
+ export const ListHeader = memo(function ListHeader({
33
+ mode,
34
+ needYouCount,
35
+ needYouOnly,
36
+ onToggleNeedYou,
37
+ onMarkAllRead,
38
+ prefs,
39
+ client,
40
+ onPrefs,
41
+ onClient,
42
+ }: ListHeaderProps) {
43
+ const [settingsOpen, setSettingsOpen] = useState(false);
44
+ return (
45
+ <div
46
+ data-sidebar="list-header"
47
+ className="flex h-7 items-center gap-1 pl-2 pr-1 text-xs text-muted-foreground max-md:pointer-coarse:h-9"
48
+ >
49
+ <h2 className="min-w-0 flex-1 truncate font-medium select-none">{groupingName(mode)}</h2>
50
+ {needYouCount > 0 ? (
51
+ <button
52
+ type="button"
53
+ aria-pressed={needYouOnly}
54
+ title={needYouOnly ? "Show every thread" : "Show only what needs you"}
55
+ onClick={onToggleNeedYou}
56
+ className={cn(
57
+ "inline-flex h-5 shrink-0 items-center rounded-full px-1.5 text-[11px] leading-none tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
58
+ needYouOnly ? NEED_YOU_ON : NEED_YOU,
59
+ )}
60
+ >
61
+ {needYouCount} need you
62
+ </button>
63
+ ) : null}
64
+ <button type="button" aria-label="Mark all read" title="Mark all read" className={ROW_ICON_BUTTON} onClick={onMarkAllRead}>
65
+ <Icon name={ICONS.markRead} aria-hidden className="size-4" />
66
+ </button>
67
+ <Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
68
+ <PopoverTrigger asChild>
69
+ <button type="button" aria-label="Thread Glance settings" title="Settings" className={ROW_ICON_BUTTON}>
70
+ <Icon name={ICONS.settings} aria-hidden className="size-4" />
71
+ </button>
72
+ </PopoverTrigger>
73
+ <PopoverContent side="bottom" align="end" className="w-80 max-w-[calc(100vw-1rem)] p-1">
74
+ <SettingsPanel prefs={prefs} client={client} onPrefs={onPrefs} onClient={onClient} />
75
+ </PopoverContent>
76
+ </Popover>
77
+ </div>
78
+ );
79
+ });
@@ -26,7 +26,6 @@ export function ProviderBadge({
26
26
  mode?: HarnessIcon;
27
27
  className?: string;
28
28
  }) {
29
- if (mode === "hidden") return null;
30
29
  const muted = mode === "muted";
31
30
  if (display.provider !== null) {
32
31
  // Without its tint the logo draws as a currentColor mask.
@@ -0,0 +1,27 @@
1
+ // Looks up the pull request of every thread settling has to know about and
2
+ // reports each answer. Draws nothing: the host shares one lookup per
3
+ // environment with the badges that ask for the same threads.
4
+ import { memo, useEffect } from "react";
5
+ import { experimental_useSidebarThreadPullRequest as usePullRequest } from "@get-bb/plugin-sdk/app";
6
+ import type { PullRequestState } from "../model/settled";
7
+
8
+ type Answer = (threadId: string, state: PullRequestState | null) => void;
9
+
10
+ const Probe = memo(function Probe({ threadId, onAnswer }: { threadId: string; onAnswer: Answer }) {
11
+ const { isLoading, pullRequest } = usePullRequest(threadId);
12
+ const state = isLoading ? undefined : (pullRequest?.state ?? null);
13
+ useEffect(() => {
14
+ if (state !== undefined) onAnswer(threadId, state);
15
+ }, [threadId, state, onAnswer]);
16
+ return null;
17
+ });
18
+
19
+ export function PullRequestProbes({ threadIds, onAnswer }: { threadIds: readonly string[]; onAnswer: Answer }) {
20
+ return (
21
+ <>
22
+ {threadIds.map((threadId) => (
23
+ <Probe key={threadId} threadId={threadId} onAnswer={onAnswer} />
24
+ ))}
25
+ </>
26
+ );
27
+ }
@@ -1,20 +1,19 @@
1
- // The top of the scroll area: the settings button and its popover.
1
+ // Thread Glance's settings, under the list header's settings button.
2
2
  import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
3
- import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
4
3
  import { cn } from "@/lib/utils";
5
4
  import type { ClientPreferences, Preferences } from "@/shared/preferences";
6
- import { childAttentionFor, countsEveryChild, lifecyclesFor, sortArrow, sortFieldPatch, threadsShown } from "../model/settings";
5
+ import { childAttentionFor, countsEveryChild, sortArrow, sortFieldPatch } from "../model/settings";
7
6
  import { effectiveSortField } from "../model/sort";
8
7
  import { ICONS } from "../icons";
9
- import { TOOLBAR_HEIGHT } from "./GroupSection";
10
- import { ROW_ICON_BUTTON } from "./ThreadRowView";
11
8
 
12
9
  // bb's own segmented controls (the Reasoning picker, the diff view toggle)
13
10
  // mark the chosen item with the state-active token and no shadow. The track
14
11
  // is a translucent tint, so the chosen item reads as raised in both themes.
15
- const SEGMENT_TRACK = "flex min-w-0 flex-1 gap-0.5 rounded-md bg-surface-recessed p-0.5";
12
+ // An item never shrinks below its text: where a line cannot hold its label
13
+ // and its control, the control wraps onto a line of its own.
14
+ const SEGMENT_TRACK = "flex flex-1 gap-0.5 rounded-md bg-surface-recessed p-0.5";
16
15
  const SEGMENT_ITEM =
17
- "flex min-w-0 flex-1 items-center justify-center whitespace-nowrap rounded-sm px-1 py-0.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring";
16
+ "flex flex-1 items-center justify-center whitespace-nowrap rounded-sm px-1 py-0.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring";
18
17
 
19
18
  function segmentClass(selected: boolean): string {
20
19
  return cn(
@@ -25,12 +24,12 @@ function segmentClass(selected: boolean): string {
25
24
  );
26
25
  }
27
26
 
28
- /** A label, then its control, on one line. */
27
+ /** A label, then its control: on one line where the panel is wide enough, else the control below. */
29
28
  function Line({ label, children }: { label: string; children: React.ReactNode }) {
30
29
  return (
31
- <div className="flex items-center gap-2 px-2 py-1">
32
- <span className="w-[5.5rem] shrink-0 text-sm">{label}</span>
33
- {children}
30
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-2 py-1">
31
+ <span className="min-w-[5.5rem] shrink-0 whitespace-nowrap text-sm">{label}</span>
32
+ <div className="flex flex-1 gap-2">{children}</div>
34
33
  </div>
35
34
  );
36
35
  }
@@ -104,6 +103,7 @@ function Heading({ children }: { children: React.ReactNode }) {
104
103
  return <h3 className="px-2 pb-0.5 pt-3 text-xs font-medium text-muted-foreground first:pt-0">{children}</h3>;
105
104
  }
106
105
 
106
+ /** Thread Glance's settings, as the list header's settings button opens them. */
107
107
  export function SettingsPanel({
108
108
  prefs,
109
109
  client,
@@ -152,14 +152,26 @@ export function SettingsPanel({
152
152
  {arrow.glyph}
153
153
  </button>
154
154
  </Line>
155
- <Toggle label="Working threads first" checked={prefs.workingFirst} onChange={(value) => onPrefs({ workingFirst: value })} />
156
155
  <Toggle
157
156
  label="Worktrees as folders"
158
157
  description="Threads sharing a worktree fold into one row"
159
158
  checked={prefs.environmentGrouping}
160
159
  onChange={(value) => onPrefs({ environmentGrouping: value })}
161
160
  />
162
- <Toggle label="Collapse older threads" checked={prefs.foldOlder} onChange={(value) => onPrefs({ foldOlder: value })} />
161
+ <Line label="Settle after">
162
+ <Segmented
163
+ label="Settle after"
164
+ options={[
165
+ ["12h", "12h"],
166
+ ["1d", "1d"],
167
+ ["3d", "3d"],
168
+ ["1w", "1w"],
169
+ ["never", "Never"],
170
+ ]}
171
+ current={prefs.settleAfter}
172
+ onSelect={(value) => onPrefs({ settleAfter: value })}
173
+ />
174
+ </Line>
163
175
  <Heading>Rows</Heading>
164
176
  <Line label="Density">
165
177
  <Segmented
@@ -178,26 +190,12 @@ export function SettingsPanel({
178
190
  options={[
179
191
  ["muted", "Muted"],
180
192
  ["colour", "Colour"],
181
- ["hidden", "Hidden"],
182
193
  ]}
183
194
  current={prefs.harnessIcon}
184
195
  onSelect={(value) => onPrefs({ harnessIcon: value })}
185
196
  />
186
197
  </Line>
187
- <Toggle label="Pull request badge" checked={prefs.showPullRequests} onChange={(value) => onPrefs({ showPullRequests: value })} />
188
- <Heading>Show</Heading>
189
- <Line label="Threads">
190
- <Segmented
191
- label="Threads"
192
- options={[
193
- ["active", "Active"],
194
- ["archived", "Archived"],
195
- ["both", "Both"],
196
- ]}
197
- current={threadsShown(prefs.threadLifecycles)}
198
- onSelect={(value) => onPrefs({ threadLifecycles: lifecyclesFor(value) })}
199
- />
200
- </Line>
198
+ <Heading>Attention</Heading>
201
199
  <Toggle
202
200
  label="Needs attention counts every child"
203
201
  description="Every unread or failed child thread; otherwise only those blocked on you."
@@ -207,32 +205,3 @@ export function SettingsPanel({
207
205
  </div>
208
206
  );
209
207
  }
210
-
211
- /** The slim row above the list: ⚙ alone at its right. */
212
- export function Toolbar({
213
- prefs,
214
- client,
215
- onPrefs,
216
- onClient,
217
- }: {
218
- prefs: Preferences;
219
- client: ClientPreferences;
220
- onPrefs(patch: Partial<Preferences>): void;
221
- onClient(patch: Partial<ClientPreferences>): void;
222
- }) {
223
- return (
224
- // Sticky with the group headers, so the settings stay in reach.
225
- <div style={{ height: TOOLBAR_HEIGHT }} className="sticky top-0 z-30 flex items-center justify-end bg-sidebar">
226
- <Popover>
227
- <PopoverTrigger asChild>
228
- <button type="button" aria-label="Thread Glance settings" title="List settings" className={ROW_ICON_BUTTON}>
229
- <Icon name={ICONS.settings} aria-hidden className="size-4" />
230
- </button>
231
- </PopoverTrigger>
232
- <PopoverContent align="end" className="w-80 p-2">
233
- <SettingsPanel prefs={prefs} client={client} onPrefs={onPrefs} onClient={onClient} />
234
- </PopoverContent>
235
- </Popover>
236
- </div>
237
- );
238
- }
@@ -2,7 +2,7 @@
2
2
  import { useContext, useEffect, useState } from "react";
3
3
  import { experimental_Icon as Icon, experimental_useSidebarThreadPullRequest as usePullRequest } from "@get-bb/plugin-sdk/app";
4
4
  import { Button } from "@/components/ui/button";
5
- import type { ThreadInfo } from "../model/families";
5
+ import type { ThreadInfo } from "../model/trees";
6
6
  import { childSummary, descendantsOf, formatDateTime, sinceLabel } from "../model/details";
7
7
  import { lastReply } from "../model/notes";
8
8
  import { finishedAtFor, stateSince } from "../model/time";
@@ -70,7 +70,7 @@ export function ThreadDetails({
70
70
  const model = useModel(controller, info);
71
71
  const live = useContext(ListLiveContext);
72
72
  const since = sinceLabel(info.state.kind, stateSince(info.state.kind, thread.id, live.stamps, live.now));
73
- const children = childSummary(descendantsOf(live.familyOf(thread.id), thread.id));
73
+ const children = childSummary(descendantsOf(live.treeOf(thread.id), thread.id));
74
74
  const environment = thread.environment;
75
75
  const finished = finishedAtFor(thread, live.stamps.finishedAt);
76
76
  const reply = lastReply(live.notes[thread.id]);
@@ -93,7 +93,7 @@ export function ThreadDetails({
93
93
  <dl className="flex flex-col gap-1">
94
94
  <Line label="Harness">
95
95
  <span className="inline-flex items-center gap-1.5">
96
- <ProviderBadge display={provider} mode={controller.harnessIcon === "hidden" ? "muted" : controller.harnessIcon} />
96
+ <ProviderBadge display={provider} mode={controller.harnessIcon} />
97
97
  {provider.name}
98
98
  </span>
99
99
  </Line>