@gperezmz/bb-plugin-thread-glance 0.2.0 → 0.2.2
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 +2 -2
- package/dist/app.css +1 -1
- package/dist/app.js +29 -29
- package/dist/app.meta.json +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +2 -2
- package/dist/server.meta.json +1 -1
- package/features/thread-list/components/FoldRows.tsx +12 -11
- package/features/thread-list/components/GroupSection.tsx +131 -102
- package/features/thread-list/components/ThreadDetails.tsx +2 -3
- package/features/thread-list/components/ThreadList.tsx +10 -9
- package/features/thread-list/components/ThreadRowView.tsx +22 -18
- package/features/thread-list/components/Toolbar.tsx +1 -1
- package/features/thread-list/components/controller.ts +2 -0
- package/features/thread-list/components/glyphs.tsx +19 -1
- package/features/thread-list/data/{useNeedsYouHold.ts → useAttentionHold.ts} +6 -6
- package/features/thread-list/model/{needs-you.ts → attention.ts} +42 -21
- package/features/thread-list/model/counters.ts +3 -3
- package/features/thread-list/model/details.ts +1 -1
- package/features/thread-list/model/expansion.ts +10 -10
- package/features/thread-list/model/families.ts +29 -23
- package/features/thread-list/model/layout.ts +0 -43
- package/features/thread-list/model/notes.ts +37 -4
- package/features/thread-list/model/settings.ts +1 -1
- package/features/thread-list/model/share.ts +3 -3
- package/features/thread-list/model/state.ts +1 -1
- package/features/thread-list/model/toggles.ts +7 -5
- package/features/thread-list/model/view.ts +69 -47
- package/features/thread-list/testing/fixtures.ts +3 -3
- package/package.json +1 -1
- package/server/cli.ts +1 -1
- package/shared/preferences.ts +2 -2
- package/skills/thread-glance/SKILL.md +4 -4
- package/features/thread-list/components/Rails.tsx +0 -27
package/dist/server.meta.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The fold rows (`N older` on a group, `N more child threads` on a family),
|
|
2
|
-
// the "+N more" line in Needs
|
|
2
|
+
// the "+N more" line in Needs attention, 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,7 +11,6 @@ import {
|
|
|
11
11
|
} from "@/components/ui/dropdown-menu";
|
|
12
12
|
import { cn } from "@/lib/utils";
|
|
13
13
|
import { ICONS } from "../icons";
|
|
14
|
-
import { RowRails } from "./Rails";
|
|
15
14
|
import { olderRowText } from "../model/labels";
|
|
16
15
|
import { rowIndent } from "../model/layout";
|
|
17
16
|
import type { EnvironmentRow, LeftOutRow, OlderRow } from "../model/view";
|
|
@@ -37,7 +36,6 @@ export const OlderRowView = memo(function OlderRowView({ row, controller }: { ro
|
|
|
37
36
|
)}
|
|
38
37
|
style={{ paddingLeft: rowIndent(row.depth) + (inFamily ? 0 : 2) }}
|
|
39
38
|
>
|
|
40
|
-
<RowRails rails={row.rails} />
|
|
41
39
|
{inFamily ? (
|
|
42
40
|
<span aria-hidden className="inline-flex size-4 shrink-0 items-center justify-center">
|
|
43
41
|
<Icon name={ICONS.more} className="size-3.5" />
|
|
@@ -50,18 +48,22 @@ export const OlderRowView = memo(function OlderRowView({ row, controller }: { ro
|
|
|
50
48
|
);
|
|
51
49
|
});
|
|
52
50
|
|
|
53
|
-
/** The "+N more" line under a family in Needs
|
|
54
|
-
export const LeftOutRowView = memo(function LeftOutRowView({ row }: { row: LeftOutRow }) {
|
|
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 }) {
|
|
55
53
|
const noun = row.count === 1 ? "child thread" : "child threads";
|
|
56
54
|
return (
|
|
57
|
-
<
|
|
58
|
-
|
|
55
|
+
<button
|
|
56
|
+
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"
|
|
59
62
|
style={{ paddingLeft: rowIndent(row.depth) }}
|
|
60
63
|
>
|
|
61
|
-
<RowRails rails={row.rails} />
|
|
62
64
|
<span aria-hidden className="size-4 shrink-0" />
|
|
63
|
-
|
|
64
|
-
</
|
|
65
|
+
+{row.count} more
|
|
66
|
+
</button>
|
|
65
67
|
);
|
|
66
68
|
});
|
|
67
69
|
|
|
@@ -84,7 +86,6 @@ export const EnvironmentRowView = memo(function EnvironmentRowView({
|
|
|
84
86
|
className="group/row relative flex h-7 w-full items-center gap-1.5 rounded-md pr-1 text-sm text-muted-foreground hover:bg-sidebar-accent"
|
|
85
87
|
style={{ paddingLeft: rowIndent(row.depth) }}
|
|
86
88
|
>
|
|
87
|
-
<RowRails rails={row.rails} />
|
|
88
89
|
<button
|
|
89
90
|
type="button"
|
|
90
91
|
aria-expanded={!row.collapsed}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A top-level group: its header with counters, and its rows, windowed
|
|
2
|
-
// in chunks; and the Needs
|
|
2
|
+
// in chunks; and the Needs attention section above every group.
|
|
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,7 +14,7 @@ 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 { ATTENTION_GROUP_ID, type GroupView, type AttentionView, type Row } from "../model/view";
|
|
18
18
|
import { chunk, windowedNavValue } from "../model/windowing";
|
|
19
19
|
import type { RowController } from "./controller";
|
|
20
20
|
import { EnvironmentRowView, LeftOutRowView, OlderRowView } from "./FoldRows";
|
|
@@ -63,15 +63,31 @@ const GroupHeader = memo(function GroupHeader({
|
|
|
63
63
|
dropActive: boolean;
|
|
64
64
|
}) {
|
|
65
65
|
const [renaming, setRenaming] = useState(false);
|
|
66
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
66
67
|
const label = group.descriptor.label;
|
|
67
68
|
const draggable = useDraggable({
|
|
68
69
|
id: `group:${group.descriptor.id}`,
|
|
69
70
|
data: { kind: "group", groupId: group.descriptor.id },
|
|
70
71
|
disabled: controller.compact || inOverflow || renaming,
|
|
71
72
|
});
|
|
72
|
-
const
|
|
73
|
+
const compact = controller.compact;
|
|
74
|
+
// Desktop: the counter sits flush right, and "+" and "…" fade in over its
|
|
75
|
+
// place on hover or focus, as a row's actions fade over its age. Nothing is
|
|
76
|
+
// kept for them otherwise. Phones keep them in line, always shown.
|
|
77
|
+
const counterFade = compact
|
|
73
78
|
? ""
|
|
74
|
-
:
|
|
79
|
+
: menuOpen
|
|
80
|
+
? "opacity-0"
|
|
81
|
+
: "group-hover/header:opacity-0 group-focus-within/header:opacity-0";
|
|
82
|
+
const actionsFade = compact
|
|
83
|
+
? ""
|
|
84
|
+
: cn(
|
|
85
|
+
"absolute top-1/2 right-0 -translate-y-1/2 pl-1",
|
|
86
|
+
dropActive ? "bg-sidebar-accent" : "bg-sidebar",
|
|
87
|
+
menuOpen
|
|
88
|
+
? "opacity-100"
|
|
89
|
+
: "pointer-events-none opacity-0 group-hover/header:pointer-events-auto group-hover/header:opacity-100 group-focus-within/header:pointer-events-auto group-focus-within/header:opacity-100",
|
|
90
|
+
);
|
|
75
91
|
return (
|
|
76
92
|
<div
|
|
77
93
|
ref={draggable.setNodeRef}
|
|
@@ -117,98 +133,96 @@ const GroupHeader = memo(function GroupHeader({
|
|
|
117
133
|
</button>
|
|
118
134
|
)}
|
|
119
135
|
{!renaming ? (
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<Icon name={ICONS.newThread} aria-hidden className="size-4" />
|
|
139
|
-
</button>
|
|
140
|
-
) : !renaming && !controller.compact ? (
|
|
141
|
-
// Keeps every header's counters on one edge.
|
|
142
|
-
<span aria-hidden className="size-6 shrink-0" />
|
|
143
|
-
) : null}
|
|
144
|
-
{!renaming ? (
|
|
145
|
-
<DropdownMenu>
|
|
146
|
-
<DropdownMenuTrigger asChild>
|
|
147
|
-
<button
|
|
148
|
-
type="button"
|
|
149
|
-
aria-label={`${label} actions`}
|
|
150
|
-
className={cn(ROW_ICON_BUTTON, actionsClass)}
|
|
151
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
152
|
-
>
|
|
153
|
-
<Icon name={ICONS.more} aria-hidden className="size-4" />
|
|
154
|
-
</button>
|
|
155
|
-
</DropdownMenuTrigger>
|
|
156
|
-
<DropdownMenuContent align="end" className="min-w-48">
|
|
157
|
-
<DropdownMenuItem onSelect={() => controller.onMarkAllRead(group)}>
|
|
158
|
-
<Icon name={ICONS.markRead} aria-hidden className="size-4" />
|
|
159
|
-
Mark all read
|
|
160
|
-
</DropdownMenuItem>
|
|
161
|
-
{group.descriptor.kind !== "pinned" && group.descriptor.newThreadProjectId !== null ? (
|
|
162
|
-
<DropdownMenuItem onSelect={() => controller.onNewThread(group)}>
|
|
136
|
+
<span className="relative ml-auto flex shrink-0 items-center">
|
|
137
|
+
<CounterStrip
|
|
138
|
+
counters={visibleCounters(group.counters, { collapsed: group.collapsed, more: false })}
|
|
139
|
+
className={cn("transition-opacity", counterFade)}
|
|
140
|
+
/>
|
|
141
|
+
<span className={cn("flex items-center transition-opacity", actionsFade)}>
|
|
142
|
+
{group.descriptor.kind !== "pinned" &&
|
|
143
|
+
(group.descriptor.newThreadProjectId !== null || group.descriptor.kind === "machine") &&
|
|
144
|
+
(!controller.compact || controller.activeGroupId === group.descriptor.id) ? (
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
aria-label={`New thread in ${label}`}
|
|
148
|
+
title={`New thread in ${label}`}
|
|
149
|
+
data-sidebar-hover-actions-mobile={controller.compact ? "always" : undefined}
|
|
150
|
+
className={ROW_ICON_BUTTON}
|
|
151
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
152
|
+
onClick={() => controller.onNewThread(group)}
|
|
153
|
+
>
|
|
163
154
|
<Icon name={ICONS.newThread} aria-hidden className="size-4" />
|
|
164
|
-
|
|
165
|
-
</DropdownMenuItem>
|
|
166
|
-
) : null}
|
|
167
|
-
{controller.canCreateSections ? (
|
|
168
|
-
<DropdownMenuItem onSelect={() => controller.onNewSection()}>
|
|
169
|
-
<Icon name={ICONS.newSection} aria-hidden className="size-4" />
|
|
170
|
-
New section
|
|
171
|
-
</DropdownMenuItem>
|
|
172
|
-
) : null}
|
|
173
|
-
{canRename(group) ? (
|
|
174
|
-
<DropdownMenuItem onSelect={() => setRenaming(true)}>
|
|
175
|
-
<Icon name={ICONS.rename} aria-hidden className="size-4" />
|
|
176
|
-
Rename
|
|
177
|
-
</DropdownMenuItem>
|
|
155
|
+
</button>
|
|
178
156
|
) : null}
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
157
|
+
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
|
158
|
+
<DropdownMenuTrigger asChild>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
aria-label={`${label} actions`}
|
|
162
|
+
className={ROW_ICON_BUTTON}
|
|
163
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
164
|
+
>
|
|
165
|
+
<Icon name={ICONS.more} aria-hidden className="size-4" />
|
|
166
|
+
</button>
|
|
167
|
+
</DropdownMenuTrigger>
|
|
168
|
+
<DropdownMenuContent align="end" className="min-w-48">
|
|
169
|
+
<DropdownMenuItem onSelect={() => controller.onMarkAllRead(group)}>
|
|
170
|
+
<Icon name={ICONS.markRead} aria-hidden className="size-4" />
|
|
171
|
+
Mark all read
|
|
185
172
|
</DropdownMenuItem>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
173
|
+
{group.descriptor.kind !== "pinned" && group.descriptor.newThreadProjectId !== null ? (
|
|
174
|
+
<DropdownMenuItem onSelect={() => controller.onNewThread(group)}>
|
|
175
|
+
<Icon name={ICONS.newThread} aria-hidden className="size-4" />
|
|
176
|
+
New thread
|
|
177
|
+
</DropdownMenuItem>
|
|
178
|
+
) : null}
|
|
179
|
+
{controller.canCreateSections ? (
|
|
180
|
+
<DropdownMenuItem onSelect={() => controller.onNewSection()}>
|
|
181
|
+
<Icon name={ICONS.newSection} aria-hidden className="size-4" />
|
|
182
|
+
New section
|
|
183
|
+
</DropdownMenuItem>
|
|
184
|
+
) : null}
|
|
185
|
+
{canRename(group) ? (
|
|
186
|
+
<DropdownMenuItem onSelect={() => setRenaming(true)}>
|
|
187
|
+
<Icon name={ICONS.rename} aria-hidden className="size-4" />
|
|
188
|
+
Rename
|
|
189
|
+
</DropdownMenuItem>
|
|
190
|
+
) : null}
|
|
191
|
+
<DropdownMenuSeparator />
|
|
192
|
+
{group.descriptor.kind !== "pinned" ? (
|
|
193
|
+
group.hidden ? (
|
|
194
|
+
<DropdownMenuItem onSelect={() => controller.onShow(group)}>
|
|
195
|
+
<Icon name={ICONS.show} aria-hidden className="size-4" />
|
|
196
|
+
Show in list
|
|
197
|
+
</DropdownMenuItem>
|
|
198
|
+
) : (
|
|
199
|
+
<DropdownMenuItem onSelect={() => controller.onHide(group)}>
|
|
200
|
+
<Icon name={ICONS.hidden} aria-hidden className="size-4" />
|
|
201
|
+
Hide from list
|
|
202
|
+
</DropdownMenuItem>
|
|
203
|
+
)
|
|
204
|
+
) : null}
|
|
205
|
+
<DropdownMenuItem onSelect={() => controller.onToggleArchived()}>
|
|
206
|
+
<Icon name={controller.showArchived ? ICONS.check : ICONS.archive} aria-hidden className="size-4" />
|
|
207
|
+
Show archived threads
|
|
190
208
|
</DropdownMenuItem>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
</DropdownMenuItem>
|
|
209
|
-
) : null}
|
|
210
|
-
</DropdownMenuContent>
|
|
211
|
-
</DropdownMenu>
|
|
209
|
+
<DropdownMenuItem onSelect={() => controller.onCustomize()}>
|
|
210
|
+
<Icon name={ICONS.customize} aria-hidden className="size-4" />
|
|
211
|
+
Customize list
|
|
212
|
+
</DropdownMenuItem>
|
|
213
|
+
{group.descriptor.kind === "section" ? (
|
|
214
|
+
<DropdownMenuItem
|
|
215
|
+
className="text-destructive focus:text-destructive"
|
|
216
|
+
onSelect={() => controller.onRemove(group)}
|
|
217
|
+
>
|
|
218
|
+
<Icon name={ICONS.remove} aria-hidden className="size-4" />
|
|
219
|
+
Remove section
|
|
220
|
+
</DropdownMenuItem>
|
|
221
|
+
) : null}
|
|
222
|
+
</DropdownMenuContent>
|
|
223
|
+
</DropdownMenu>
|
|
224
|
+
</span>
|
|
225
|
+
</span>
|
|
212
226
|
) : null}
|
|
213
227
|
</div>
|
|
214
228
|
);
|
|
@@ -333,7 +347,7 @@ function Rows({
|
|
|
333
347
|
) : row.type === "older" ? (
|
|
334
348
|
<OlderRowView key={row.key} row={row} controller={rowController} />
|
|
335
349
|
) : row.type === "left-out" ? (
|
|
336
|
-
<LeftOutRowView key={row.key} row={row} />
|
|
350
|
+
<LeftOutRowView key={row.key} row={row} controller={rowController} />
|
|
337
351
|
) : (
|
|
338
352
|
<EnvironmentRowView
|
|
339
353
|
key={row.key}
|
|
@@ -382,7 +396,7 @@ export const GroupSection = memo(function GroupSection({
|
|
|
382
396
|
dropActive={dropTargetGroupId === group.descriptor.id}
|
|
383
397
|
/>
|
|
384
398
|
{group.collapsed ? null : group.rows.length === 0 ? (
|
|
385
|
-
// A group whose every family sits in Needs
|
|
399
|
+
// A group whose every family sits in Needs attention draws only its header.
|
|
386
400
|
group.rootIds.length === 0 ? <p className="py-1 pl-8 text-xs text-muted-foreground">No threads</p> : null
|
|
387
401
|
) : (
|
|
388
402
|
<Rows
|
|
@@ -397,23 +411,38 @@ export const GroupSection = memo(function GroupSection({
|
|
|
397
411
|
);
|
|
398
412
|
});
|
|
399
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
|
+
|
|
400
425
|
/**
|
|
401
|
-
* The Needs
|
|
402
|
-
* has no collapse, menu or drag, and
|
|
426
|
+
* The Needs attention section: a header with its family count, then its rows,
|
|
427
|
+
* on one band of bb's attention colour. It has no collapse, menu or drag, and
|
|
428
|
+
* no drop target of its own.
|
|
403
429
|
*/
|
|
404
|
-
export const
|
|
430
|
+
export const AttentionSection = memo(function AttentionSection({ view, ...rest }: SectionProps & { view: AttentionView }) {
|
|
405
431
|
return (
|
|
406
|
-
<section aria-label="Needs
|
|
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)}>
|
|
407
433
|
<h2
|
|
408
434
|
style={{ top: TOOLBAR_HEIGHT }}
|
|
409
|
-
className="sticky z-20 flex h-7 items-center gap-1 bg-
|
|
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"
|
|
410
436
|
>
|
|
411
|
-
<span className="min-w-0 flex-1 truncate">Needs
|
|
412
|
-
<span
|
|
437
|
+
<span className="min-w-0 flex-1 truncate">Needs attention</span>
|
|
438
|
+
<span
|
|
439
|
+
className={cn("rounded-full px-1.5 text-[11px] leading-4 tabular-nums", ATTENTION_COUNT)}
|
|
440
|
+
aria-label={`${view.familyCount} ${view.familyCount === 1 ? "family" : "families"}`}
|
|
441
|
+
>
|
|
413
442
|
{view.familyCount}
|
|
414
443
|
</span>
|
|
415
444
|
</h2>
|
|
416
|
-
<Rows {...rest} rows={view.rows} groupId={
|
|
445
|
+
<Rows {...rest} rows={view.rows} groupId={ATTENTION_GROUP_ID} inPinned={false} forceMount={false} />
|
|
417
446
|
</section>
|
|
418
447
|
);
|
|
419
448
|
});
|
|
@@ -8,7 +8,7 @@ import { lastReply } from "../model/notes";
|
|
|
8
8
|
import { finishedAtFor, stateSince } from "../model/time";
|
|
9
9
|
import { ICONS } from "../icons";
|
|
10
10
|
import { ListLiveContext, type ModelInfo, type RowController } from "./controller";
|
|
11
|
-
import { GlyphIcon } from "./glyphs";
|
|
11
|
+
import { GlyphIcon, NoteLine } from "./glyphs";
|
|
12
12
|
import { ProviderBadge } from "./ProviderBadge";
|
|
13
13
|
import { pullRequestLabel, pullRequestTone } from "./PullRequestBadge";
|
|
14
14
|
|
|
@@ -87,8 +87,7 @@ export function ThreadDetails({
|
|
|
87
87
|
</div>
|
|
88
88
|
{info.note !== null ? (
|
|
89
89
|
<p className="break-words leading-snug">
|
|
90
|
-
<
|
|
91
|
-
{info.note.text}
|
|
90
|
+
<NoteLine note={info.note} />
|
|
92
91
|
</p>
|
|
93
92
|
) : null}
|
|
94
93
|
<dl className="flex flex-col gap-1">
|
|
@@ -29,7 +29,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|
|
29
29
|
import { cn } from "@/lib/utils";
|
|
30
30
|
import { useAutoExpand } from "../data/useAutoExpand";
|
|
31
31
|
import { useClientPreferences } from "../data/useClientPreferences";
|
|
32
|
-
import {
|
|
32
|
+
import { useAttentionHold } from "../data/useAttentionHold";
|
|
33
33
|
import { useNow } from "../data/useNow";
|
|
34
34
|
import { usePreferences } from "../data/usePreferences";
|
|
35
35
|
import { useScheduled } from "../data/useScheduled";
|
|
@@ -40,7 +40,7 @@ import { moveGroup, ORDER_PREFERENCE } from "../model/groups";
|
|
|
40
40
|
import { MARK_ALL_CONFIRM_ABOVE, type RowMenuAction } from "../model/menu";
|
|
41
41
|
import { assignProviderMarks, providerMark } from "../model/provider-mark";
|
|
42
42
|
import { isDoneUnseen } from "../model/state";
|
|
43
|
-
import { markAllReadPlan, toggleChip, toggleGroup, toggleOlder, type ToggleOutcome } from "../model/toggles";
|
|
43
|
+
import { markAllReadPlan, openChildren, toggleChip, toggleGroup, toggleOlder, type ToggleOutcome } from "../model/toggles";
|
|
44
44
|
import { buildListView, type GroupView, type ListView } from "../model/view";
|
|
45
45
|
import { shareView } from "../model/share";
|
|
46
46
|
import { ListLiveContext, type ListLive, type ModelInfo, type RowController } from "./controller";
|
|
@@ -51,7 +51,7 @@ import { modelDisplayName } from "../model/details";
|
|
|
51
51
|
import { groupIdForRoot } from "../model/groups";
|
|
52
52
|
import { CounterStrip } from "./glyphs";
|
|
53
53
|
import { cancelPendingCards } from "./row-card";
|
|
54
|
-
import { GroupSection,
|
|
54
|
+
import { GroupSection, AttentionSection, type DropStates, type GroupController } from "./GroupSection";
|
|
55
55
|
import type { ProviderDisplay } from "./ProviderBadge";
|
|
56
56
|
import { ThreadDetails } from "./ThreadDetails";
|
|
57
57
|
import { Toolbar } from "./Toolbar";
|
|
@@ -160,7 +160,7 @@ function ThreadListBody({
|
|
|
160
160
|
[ready, threads, activeThreadId, stamps.finishedAt, stamps.seenAt, draftIds, scheduled, now, notes, prefs.childAttention],
|
|
161
161
|
);
|
|
162
162
|
const { targets, prune } = useAutoExpand(hydrated ? forest : null, activeThreadId);
|
|
163
|
-
const heldRootId =
|
|
163
|
+
const heldRootId = useAttentionHold(forest, activeThreadId);
|
|
164
164
|
// Rows and groups that did not change keep their objects, so their
|
|
165
165
|
// memoized components skip the render.
|
|
166
166
|
const previousView = useRef<ListView | null>(null);
|
|
@@ -368,6 +368,7 @@ function ThreadListBody({
|
|
|
368
368
|
const group = view!.groups.find((candidate) => candidate.descriptor.id === row.scopeId) ?? null;
|
|
369
369
|
applyToggle(toggleOlder(row, prefs, group, forest!));
|
|
370
370
|
},
|
|
371
|
+
onOpenChildren: (rootId) => applyToggle(openChildren(rootId, latest.current.prefs)),
|
|
371
372
|
onToggleEnvironment: (environmentId) => {
|
|
372
373
|
const { collapsedEnvironments } = latest.current.prefs;
|
|
373
374
|
update({
|
|
@@ -523,8 +524,8 @@ function ThreadListBody({
|
|
|
523
524
|
}, []);
|
|
524
525
|
|
|
525
526
|
const dropContext = useMemo(() => {
|
|
526
|
-
// A row in Needs
|
|
527
|
-
const groupOfThread = new Map<string, string>(Object.entries(view?.
|
|
527
|
+
// A row in Needs attention drops as it would in its home group.
|
|
528
|
+
const groupOfThread = new Map<string, string>(Object.entries(view?.attention?.homeGroupIds ?? {}));
|
|
528
529
|
for (const group of [...(view?.groups ?? []), ...(view?.more ?? [])]) {
|
|
529
530
|
for (const row of group.rows) if (row.type === "thread") groupOfThread.set(row.info.thread.id, group.descriptor.id);
|
|
530
531
|
}
|
|
@@ -684,9 +685,9 @@ function ThreadListBody({
|
|
|
684
685
|
<p className="px-3 py-4 text-sm text-muted-foreground">No threads yet.</p>
|
|
685
686
|
) : (
|
|
686
687
|
<DndContext sensors={sensors} collisionDetection={collision} onDragMove={onDragMove} onDragEnd={onDragEnd} onDragCancel={onDragCancel}>
|
|
687
|
-
{view.
|
|
688
|
-
<
|
|
689
|
-
view={view.
|
|
688
|
+
{view.attention !== null ? (
|
|
689
|
+
<AttentionSection
|
|
690
|
+
view={view.attention}
|
|
690
691
|
rowController={rowController}
|
|
691
692
|
environmentProviders={environmentProviders}
|
|
692
693
|
dropStates={dropStates}
|
|
@@ -16,18 +16,18 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/h
|
|
|
16
16
|
import { cn } from "@/lib/utils";
|
|
17
17
|
import { ICONS } from "../icons";
|
|
18
18
|
import { chipLabel, rowAriaLabel } from "../model/labels";
|
|
19
|
-
import { rowIndent
|
|
19
|
+
import { rowIndent } from "../model/layout";
|
|
20
20
|
import { rowMenuItems } from "../model/menu";
|
|
21
|
+
import { noteText } from "../model/notes";
|
|
21
22
|
import { chipTone, pluginStatusWins } from "../model/state";
|
|
22
23
|
import { trailingTime } from "../model/time";
|
|
23
24
|
import type { ThreadRow } from "../model/view";
|
|
24
25
|
import type { DraggedThread } from "../model/drag";
|
|
25
26
|
import type { RowController } from "./controller";
|
|
26
|
-
import { CHIP_TONE_CLASS, FlagGlyph, GlyphIcon, PluginStatusGlyph } from "./glyphs";
|
|
27
|
+
import { CHIP_TONE_CLASS, FlagGlyph, GlyphIcon, NoteLine, PluginStatusGlyph } from "./glyphs";
|
|
27
28
|
import { ProviderBadge } from "./ProviderBadge";
|
|
28
29
|
import { PullRequestBadge } from "./PullRequestBadge";
|
|
29
30
|
import { RenameEditor } from "./RenameEditor";
|
|
30
|
-
import { RowRails } from "./Rails";
|
|
31
31
|
import { RowContextMenuContent, RowDropdownMenuContent, type ContextMenuInput } from "./RowMenu";
|
|
32
32
|
import { SplitMiniMap, type MiniMapPane } from "./SplitMiniMap";
|
|
33
33
|
import { ThreadDetails } from "./ThreadDetails";
|
|
@@ -57,6 +57,14 @@ function swallowNextClick(): void {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* A quiet title, and its chip: the foreground mixed toward the surface under
|
|
62
|
+
* the row (the sidebar, or the Needs attention band) in oklch, which keeps
|
|
63
|
+
* 4.5:1 in both of bb's themes. Opacity blends in sRGB and lands lower in
|
|
64
|
+
* the light theme.
|
|
65
|
+
*/
|
|
66
|
+
const QUIET_TEXT = "text-[color:color-mix(in_oklch,var(--foreground)_var(--tg-quiet,68%),var(--tg-surface,var(--sidebar)))]";
|
|
67
|
+
|
|
60
68
|
export const ROW_ICON_BUTTON =
|
|
61
69
|
"pointer-events-auto relative z-10 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-state-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring data-[state=open]:bg-state-active";
|
|
62
70
|
|
|
@@ -291,10 +299,9 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
291
299
|
|
|
292
300
|
const chip = row.chip;
|
|
293
301
|
const indent = rowIndent(row.depth);
|
|
294
|
-
const
|
|
295
|
-
const twoLines = controller.comfortable ||
|
|
296
|
-
|
|
297
|
-
const dimmed = info.quiet && !isActive && !editing && chip === null;
|
|
302
|
+
const note = row.note;
|
|
303
|
+
const twoLines = controller.comfortable || note !== null;
|
|
304
|
+
const dimmed = row.dimmed && !editing;
|
|
298
305
|
const menuShowing = menuOpen || contextOpen;
|
|
299
306
|
// Desktop: the actions cross-fade over the harness and age, as bb's
|
|
300
307
|
// trailing slot does. Compact: "…" always shows beside them.
|
|
@@ -359,7 +366,6 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
359
366
|
}}
|
|
360
367
|
className="absolute inset-0 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring"
|
|
361
368
|
/>
|
|
362
|
-
<RowRails rails={row.rails} />
|
|
363
369
|
<span className="pointer-events-none relative flex size-4 shrink-0 items-center justify-center">{stateSlot}</span>
|
|
364
370
|
{row.nested ? (
|
|
365
371
|
// Tight against the title, and over the row's gap, so it adds 8px.
|
|
@@ -403,22 +409,19 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
403
409
|
className={cn(
|
|
404
410
|
"min-w-0 truncate",
|
|
405
411
|
info.unread ? "font-semibold" : "font-normal",
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
// Foreground thinned toward the sidebar: at least 4.5:1 in both themes, and below a muted root.
|
|
411
|
-
title.muted && "text-[color:color-mix(in_oklch,var(--foreground)_72%,var(--sidebar))] group-hover/row:text-foreground",
|
|
412
|
+
// Children sit a step below their parent.
|
|
413
|
+
row.depth > 0 && "text-xs",
|
|
414
|
+
// Quiet threads step back so live ones lead; hover brings them back.
|
|
415
|
+
dimmed && `${QUIET_TEXT} group-hover/row:text-foreground`,
|
|
412
416
|
)}
|
|
413
417
|
>
|
|
414
418
|
{/* Plain text: mention pills lost the truncation fight. */}
|
|
415
419
|
{thread.displayTitle}
|
|
416
420
|
</span>
|
|
417
421
|
)}
|
|
418
|
-
{!editing &&
|
|
419
|
-
<span className="min-w-0 truncate text-xs leading-4 text-muted-foreground" title={
|
|
420
|
-
<
|
|
421
|
-
{info.note.text}
|
|
422
|
+
{!editing && note !== null ? (
|
|
423
|
+
<span className="min-w-0 truncate text-xs leading-4 text-muted-foreground" title={noteText(note)}>
|
|
424
|
+
<NoteLine note={note} />
|
|
422
425
|
</span>
|
|
423
426
|
) : controller.comfortable && !editing ? (
|
|
424
427
|
<SecondLine row={row} multiHost={controller.multiHost} defaultBranch={controller.defaultBranchOf(thread)} />
|
|
@@ -451,6 +454,7 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
451
454
|
className={cn(
|
|
452
455
|
"pointer-events-auto relative z-10 inline-flex h-5 shrink-0 items-center gap-0.5 rounded-md border px-1 text-[11px] leading-none tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
453
456
|
CHIP_TONE_CLASS[chipTone(chip.flag)],
|
|
457
|
+
dimmed && QUIET_TEXT,
|
|
454
458
|
)}
|
|
455
459
|
>
|
|
456
460
|
{chip.count}
|
|
@@ -199,7 +199,7 @@ export function SettingsPanel({
|
|
|
199
199
|
/>
|
|
200
200
|
</Line>
|
|
201
201
|
<Toggle
|
|
202
|
-
label="Needs
|
|
202
|
+
label="Needs attention counts every child"
|
|
203
203
|
description="Every unread or failed child thread; otherwise only those blocked on you."
|
|
204
204
|
checked={countsEveryChild(prefs.childAttention)}
|
|
205
205
|
onChange={(value) => onPrefs({ childAttention: childAttentionFor(value) })}
|
|
@@ -33,6 +33,8 @@ export interface RowController {
|
|
|
33
33
|
onNavigate(): void;
|
|
34
34
|
onToggleChip(row: ThreadRow): void;
|
|
35
35
|
onToggleOlder(row: OlderRow): void;
|
|
36
|
+
/** Opens a family's children from its "+N more" line in Needs attention. */
|
|
37
|
+
onOpenChildren(rootId: string): void;
|
|
36
38
|
onToggleEnvironment(environmentId: string): void;
|
|
37
39
|
onMenuAction(action: RowMenuAction, thread: PluginSidebarThread, sectionId?: string | null): void;
|
|
38
40
|
onRename(threadId: string, title: string): Promise<void>;
|
|
@@ -4,6 +4,7 @@ import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
|
|
|
4
4
|
import type { PluginSidebarThreadRowStatus } from "@get-bb/plugin-sdk/app";
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
6
|
import type { Counters } from "../model/counters";
|
|
7
|
+
import type { RowNote } from "../model/notes";
|
|
7
8
|
import { FLAG_GLYPHS, type ChipTone, type Flag, type Glyph, type Tone } from "../model/state";
|
|
8
9
|
|
|
9
10
|
export const TONE_CLASS: Record<Tone, string> = {
|
|
@@ -92,6 +93,23 @@ export function PluginStatusGlyph({ status }: { status: PluginSidebarThreadRowSt
|
|
|
92
93
|
);
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/** A note's prefix takes the tone of the row's glyph for the same reason. */
|
|
97
|
+
const NOTE_TONE_CLASS: Record<RowNote["tone"], string> = {
|
|
98
|
+
attention: "text-attention",
|
|
99
|
+
destructive: "text-destructive",
|
|
100
|
+
muted: "text-muted-foreground",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** "Failed: timeout", its prefix in the note's tone, or the prefix alone when there is no text. */
|
|
104
|
+
export function NoteLine({ note }: { note: RowNote }) {
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
<span className={NOTE_TONE_CLASS[note.tone]}>{note.text === "" ? note.prefix : `${note.prefix}:`}</span>
|
|
108
|
+
{note.text === "" ? null : ` ${note.text}`}
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
95
113
|
export function FlagGlyph({ flag, className }: { flag: Flag; className?: string }) {
|
|
96
114
|
return <GlyphIcon glyph={FLAG_GLYPHS[flag]} className={className} />;
|
|
97
115
|
}
|
|
@@ -101,7 +119,7 @@ const COUNTER_ITEMS: readonly {
|
|
|
101
119
|
flag: Flag;
|
|
102
120
|
label: (count: number) => string;
|
|
103
121
|
}[] = [
|
|
104
|
-
{ key: "waitsOnYou", flag: "waits-on-you", label: (n) => `${n}
|
|
122
|
+
{ key: "waitsOnYou", flag: "waits-on-you", label: (n) => `${n} waiting on you` },
|
|
105
123
|
{ key: "failed", flag: "unread-failed", label: (n) => `${n} failed` },
|
|
106
124
|
{ key: "offline", flag: "offline", label: (n) => `${n} waiting for an offline machine` },
|
|
107
125
|
{ key: "working", flag: "working", label: (n) => `${n} working` },
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
// Which family Needs
|
|
1
|
+
// Which family Needs attention holds, and which family is open, while one of its threads is open. Diffed
|
|
2
2
|
// render to render, as auto-expansion is; nothing here is persisted.
|
|
3
3
|
import { useRef } from "react";
|
|
4
4
|
import type { Forest } from "../model/families";
|
|
5
|
-
import {
|
|
5
|
+
import { holdAttention, NO_HOLD, type AttentionHold } from "../model/attention";
|
|
6
6
|
|
|
7
|
-
export function
|
|
8
|
-
const held = useRef<
|
|
7
|
+
export function useAttentionHold(forest: Forest | null, activeThreadId: string | null): string | null {
|
|
8
|
+
const held = useRef<AttentionHold>(NO_HOLD);
|
|
9
9
|
const lastForest = useRef<Forest | null>(null);
|
|
10
10
|
const lastActive = useRef<string | null | undefined>(undefined);
|
|
11
11
|
// Derived during render, so the family never leaves for a frame. Idempotent:
|
|
12
12
|
// a repeated render with the same inputs keeps what it stored.
|
|
13
13
|
if (forest !== null && (forest !== lastForest.current || activeThreadId !== lastActive.current)) {
|
|
14
|
-
held.current =
|
|
14
|
+
held.current = holdAttention(held.current, activeThreadId === null ? undefined : forest.familyOf.get(activeThreadId));
|
|
15
15
|
lastForest.current = forest;
|
|
16
16
|
lastActive.current = activeThreadId;
|
|
17
17
|
}
|
|
18
|
-
return held.current;
|
|
18
|
+
return held.current.heldRootId;
|
|
19
19
|
}
|