@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.
- package/README.md +1 -1
- package/app.tsx +2 -1
- package/dist/app.css +1 -1
- package/dist/app.js +31 -31
- package/dist/app.meta.json +1 -1
- package/dist/server.js +10 -10
- package/dist/server.js.map +3 -3
- package/dist/server.meta.json +1 -1
- package/features/thread-list/components/FoldRows.tsx +21 -30
- package/features/thread-list/components/GroupSection.tsx +8 -50
- package/features/thread-list/components/ListHeader.tsx +79 -0
- package/features/thread-list/components/ProviderBadge.tsx +0 -1
- package/features/thread-list/components/PullRequestProbes.tsx +27 -0
- package/features/thread-list/components/{Toolbar.tsx → SettingsPanel.tsx} +26 -57
- package/features/thread-list/components/ThreadDetails.tsx +3 -3
- package/features/thread-list/components/ThreadList.tsx +116 -84
- package/features/thread-list/components/ThreadRowView.tsx +70 -78
- package/features/thread-list/components/controller.ts +5 -10
- package/features/thread-list/components/glyphs.tsx +39 -14
- package/features/thread-list/data/same-window.ts +34 -0
- package/features/thread-list/data/useAutoExpand.ts +1 -1
- package/features/thread-list/data/useClientPreferences.ts +15 -7
- package/features/thread-list/data/useDefaultBranches.ts +35 -0
- package/features/thread-list/data/usePreferences.ts +16 -7
- package/features/thread-list/data/usePullRequestAnswers.ts +36 -0
- package/features/thread-list/data/useSystemFacts.ts +52 -0
- package/features/thread-list/icons.ts +1 -1
- package/features/thread-list/model/attention.ts +2 -66
- package/features/thread-list/model/branches.ts +27 -0
- package/features/thread-list/model/counters.ts +5 -5
- package/features/thread-list/model/details.ts +6 -6
- package/features/thread-list/model/expansion.ts +5 -5
- package/features/thread-list/model/groups.ts +2 -2
- package/features/thread-list/model/labels.ts +28 -17
- package/features/thread-list/model/layout.ts +5 -2
- package/features/thread-list/model/menu.ts +1 -0
- package/features/thread-list/model/move.ts +1 -1
- package/features/thread-list/model/notes.ts +2 -28
- package/features/thread-list/model/settings.ts +2 -18
- package/features/thread-list/model/settled.ts +122 -0
- package/features/thread-list/model/share.ts +1 -7
- package/features/thread-list/model/sort.ts +5 -16
- package/features/thread-list/model/state.ts +25 -9
- package/features/thread-list/model/toggles.ts +35 -24
- package/features/thread-list/model/{families.ts → trees.ts} +40 -28
- package/features/thread-list/model/view.ts +200 -274
- package/features/thread-list/testing/fixtures.ts +41 -12
- package/package.json +1 -1
- package/server/cli.ts +1 -1
- package/server/import.ts +36 -6
- package/server/preference-store.ts +2 -1
- package/server.ts +1 -1
- package/shared/preferences.ts +38 -27
- package/skills/thread-glance/SKILL.md +11 -12
- package/features/thread-list/data/useAttentionHold.ts +0 -19
package/dist/server.meta.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The fold rows (
|
|
2
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
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=
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
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
|
|
52
|
-
export const
|
|
53
|
-
const
|
|
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={
|
|
58
|
-
aria-label={
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
65
|
-
|
|
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
|
|
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 {
|
|
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,
|
|
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 === "
|
|
350
|
-
<
|
|
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.
|
|
399
|
-
|
|
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
|
+
});
|
|
@@ -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
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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/
|
|
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.
|
|
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
|
|
96
|
+
<ProviderBadge display={provider} mode={controller.harnessIcon} />
|
|
97
97
|
{provider.name}
|
|
98
98
|
</span>
|
|
99
99
|
</Line>
|