@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
|
@@ -29,32 +29,36 @@ 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 { useAttentionHold } from "../data/useAttentionHold";
|
|
33
32
|
import { useNow } from "../data/useNow";
|
|
34
33
|
import { usePreferences } from "../data/usePreferences";
|
|
35
34
|
import { useScheduled } from "../data/useScheduled";
|
|
36
35
|
import { useStamps } from "../data/useStamps";
|
|
37
36
|
import { resolveDrop, type DraggedThread, type DropAction, type DropTarget } from "../model/drag";
|
|
38
|
-
import { buildForest } from "../model/
|
|
37
|
+
import { buildForest } from "../model/trees";
|
|
39
38
|
import { moveGroup, ORDER_PREFERENCE } from "../model/groups";
|
|
40
39
|
import { MARK_ALL_CONFIRM_ABOVE, type RowMenuAction } from "../model/menu";
|
|
41
40
|
import { assignProviderMarks, providerMark } from "../model/provider-mark";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
41
|
+
import { markAllReadPlan, markReadPlanFor, toggleChip, toggleGroup, toggleOlder, toggleSettled, type ToggleOutcome } from "../model/toggles";
|
|
42
|
+
import { pullRequestFact, pullRequestLookupIds, type SettleInputs } from "../model/settled";
|
|
43
|
+
import { usePullRequestAnswers } from "../data/usePullRequestAnswers";
|
|
44
|
+
import { PullRequestProbes } from "./PullRequestProbes";
|
|
45
|
+
import { buildListView, countNeedYou, needYouActive, type GroupView, type ListView } from "../model/view";
|
|
45
46
|
import { shareView } from "../model/share";
|
|
46
47
|
import { ListLiveContext, type ListLive, type ModelInfo, type RowController } from "./controller";
|
|
47
48
|
import { ConfirmDialog, CustomizeDialog, DetailsDialog, MoveDialog, NewSectionDialog, type CustomizeItem } from "./Dialogs";
|
|
48
49
|
import { useNotes } from "../data/useNotes";
|
|
50
|
+
import { useDefaultBranches } from "../data/useDefaultBranches";
|
|
51
|
+
import { useSystemFacts } from "../data/useSystemFacts";
|
|
49
52
|
import { moveTargets } from "../model/move";
|
|
50
53
|
import { modelDisplayName } from "../model/details";
|
|
51
54
|
import { groupIdForRoot } from "../model/groups";
|
|
52
55
|
import { CounterStrip } from "./glyphs";
|
|
53
56
|
import { cancelPendingCards } from "./row-card";
|
|
54
|
-
import { GroupSection,
|
|
57
|
+
import { GroupSection, type DropStates, type GroupController } from "./GroupSection";
|
|
55
58
|
import type { ProviderDisplay } from "./ProviderBadge";
|
|
56
59
|
import { ThreadDetails } from "./ThreadDetails";
|
|
57
|
-
import {
|
|
60
|
+
import { ListHeader } from "./ListHeader";
|
|
61
|
+
import type { ThreadTree } from "../model/trees";
|
|
58
62
|
|
|
59
63
|
const PLUGIN_ID = "thread-glance";
|
|
60
64
|
|
|
@@ -109,7 +113,9 @@ function ThreadListBody({
|
|
|
109
113
|
}: PluginThreadListProps & { attempt: number; onRetry(): void }) {
|
|
110
114
|
const { prefs, hydrated, update } = usePreferences();
|
|
111
115
|
const [client, updateClient] = useClientPreferences();
|
|
112
|
-
|
|
116
|
+
// The need-you filter is per window and starts off on every load.
|
|
117
|
+
const [needYouOn, setNeedYouOn] = useState(false);
|
|
118
|
+
const sidebar = useSidebarThreads({ experimental_lifecycles: prefs.showArchived ? ["active", "archived"] : ["active"] });
|
|
113
119
|
const actions = useThreadActions();
|
|
114
120
|
const sdk = useSdk();
|
|
115
121
|
const { providers } = useProviders();
|
|
@@ -134,8 +140,6 @@ function ThreadListBody({
|
|
|
134
140
|
const [confirm, setConfirm] = useState<Confirm | null>(null);
|
|
135
141
|
const [dropStates, setDropStates] = useState<DropStates>(() => new Map());
|
|
136
142
|
const [dropGroupId, setDropGroupId] = useState<string | null>(null);
|
|
137
|
-
const [defaultBranches, setDefaultBranches] = useState<ReadonlyMap<string, string | null>>(() => new Map());
|
|
138
|
-
const branchRequests = useRef(new Set<string>());
|
|
139
143
|
const models = useRef(new Map<string, Promise<ModelInfo | null>>());
|
|
140
144
|
|
|
141
145
|
const ready = sidebar.status === "ready";
|
|
@@ -160,7 +164,32 @@ function ThreadListBody({
|
|
|
160
164
|
[ready, threads, activeThreadId, stamps.finishedAt, stamps.seenAt, draftIds, scheduled, now, notes, prefs.childAttention],
|
|
161
165
|
);
|
|
162
166
|
const { targets, prune } = useAutoExpand(hydrated ? forest : null, activeThreadId);
|
|
163
|
-
|
|
167
|
+
// Projects with a thread on a branch: the rest need no default branch.
|
|
168
|
+
const branchedProjectIds = useMemo(
|
|
169
|
+
() => [...new Set(threads.flatMap((thread) => (thread.environment?.branchName ? [thread.projectId] : [])))],
|
|
170
|
+
[threads],
|
|
171
|
+
);
|
|
172
|
+
const defaultBranches = useDefaultBranches(branchedProjectIds);
|
|
173
|
+
const defaultBranchOf = useCallback(
|
|
174
|
+
(thread: PluginSidebarThread) => (defaultBranches.has(thread.projectId) ? (defaultBranches.get(thread.projectId) ?? null) : undefined),
|
|
175
|
+
[defaultBranches],
|
|
176
|
+
);
|
|
177
|
+
const [pullRequests, onPullRequest] = usePullRequestAnswers();
|
|
178
|
+
const system = useSystemFacts();
|
|
179
|
+
const pullRequestLookups = useMemo(
|
|
180
|
+
() => (forest === null ? [] : pullRequestLookupIds(forest.infos.values(), defaultBranchOf)),
|
|
181
|
+
[forest, defaultBranchOf],
|
|
182
|
+
);
|
|
183
|
+
const settle: SettleInputs = useMemo(
|
|
184
|
+
() => ({
|
|
185
|
+
now,
|
|
186
|
+
settleAfter: prefs.settleAfter,
|
|
187
|
+
startedAt: stamps.startedAt,
|
|
188
|
+
finishedAt: stamps.finishedAt,
|
|
189
|
+
pullRequestOf: (thread) => pullRequestFact(thread, defaultBranchOf(thread), pullRequests),
|
|
190
|
+
}),
|
|
191
|
+
[now, prefs.settleAfter, stamps.startedAt, stamps.finishedAt, defaultBranchOf, pullRequests],
|
|
192
|
+
);
|
|
164
193
|
// Rows and groups that did not change keep their objects, so their
|
|
165
194
|
// memoized components skip the render.
|
|
166
195
|
const previousView = useRef<ListView | null>(null);
|
|
@@ -175,14 +204,37 @@ function ThreadListBody({
|
|
|
175
204
|
sections: sidebar.sections,
|
|
176
205
|
prefs,
|
|
177
206
|
activeThreadId,
|
|
178
|
-
held,
|
|
179
207
|
targets,
|
|
208
|
+
settle,
|
|
209
|
+
defaultProviderId: system.defaultProviderId,
|
|
210
|
+
primaryHostId: system.primaryHostId,
|
|
211
|
+
comfortable: client.density === "comfortable",
|
|
212
|
+
defaultBranchOf,
|
|
213
|
+
needYouOnly: needYouActive(needYouOn, countNeedYou(forest)),
|
|
180
214
|
})),
|
|
181
|
-
[
|
|
215
|
+
[
|
|
216
|
+
forest,
|
|
217
|
+
threads,
|
|
218
|
+
sidebar.projects,
|
|
219
|
+
sidebar.sections,
|
|
220
|
+
prefs,
|
|
221
|
+
activeThreadId,
|
|
222
|
+
targets,
|
|
223
|
+
settle,
|
|
224
|
+
system.defaultProviderId,
|
|
225
|
+
system.primaryHostId,
|
|
226
|
+
client.density,
|
|
227
|
+
defaultBranchOf,
|
|
228
|
+
needYouOn,
|
|
229
|
+
],
|
|
182
230
|
);
|
|
183
231
|
useLayoutEffect(() => {
|
|
184
232
|
previousView.current = view;
|
|
185
233
|
}, [view]);
|
|
234
|
+
// Nothing left that needs you turns the filter off, so it does not narrow
|
|
235
|
+
// the list again unasked when something next does. Set during render, as
|
|
236
|
+
// React adjusts state from a changed input.
|
|
237
|
+
if (needYouOn && forest !== null && !needYouActive(needYouOn, countNeedYou(forest))) setNeedYouOn(false);
|
|
186
238
|
|
|
187
239
|
// Viewing a child stamps seenAt, on arrival and on leaving, so a child
|
|
188
240
|
// that finishes while you watch doesn't turn unread behind you.
|
|
@@ -198,22 +250,6 @@ function ThreadListBody({
|
|
|
198
250
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
199
251
|
}, [activeThreadId]);
|
|
200
252
|
|
|
201
|
-
// Each project's default branch, fetched once per session.
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
if (!prefs.showPullRequests || forest === null) return;
|
|
204
|
-
for (const family of forest.families) {
|
|
205
|
-
const thread = family.root.thread;
|
|
206
|
-
const projectId = thread.projectId;
|
|
207
|
-
if (thread.environment?.branchName == null || thread.host === null) continue;
|
|
208
|
-
if (branchRequests.current.has(projectId)) continue;
|
|
209
|
-
branchRequests.current.add(projectId);
|
|
210
|
-
sdk.projects.branches({ projectId, hostId: thread.host.id, limit: "1" }).then(
|
|
211
|
-
(result) => setDefaultBranches((current) => new Map(current).set(projectId, result.defaultBranch)),
|
|
212
|
-
() => setDefaultBranches((current) => new Map(current).set(projectId, null)),
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}, [prefs.showPullRequests, forest, sdk]);
|
|
216
|
-
|
|
217
253
|
const marks = useMemo(() => assignProviderMarks(providers), [providers]);
|
|
218
254
|
// One display per harness, built once: rows compare it by identity.
|
|
219
255
|
const providerDisplay = useMemo(() => {
|
|
@@ -258,10 +294,13 @@ function ThreadListBody({
|
|
|
258
294
|
case "copy-id":
|
|
259
295
|
void copyText(thread.id, "Thread ID copied");
|
|
260
296
|
return;
|
|
261
|
-
case "mark-read":
|
|
262
|
-
|
|
263
|
-
|
|
297
|
+
case "mark-read": {
|
|
298
|
+
const forest = latest.current.forest;
|
|
299
|
+
const plan = forest === null ? { read: [thread.id], seen: [] } : markReadPlanFor(thread.id, forest, context);
|
|
300
|
+
if (plan.seen.length > 0) markSeen(plan.seen);
|
|
301
|
+
for (const id of plan.read) actions.setRead(id, true).catch(fail("Couldn't mark read"));
|
|
264
302
|
return;
|
|
303
|
+
}
|
|
265
304
|
case "mark-unread":
|
|
266
305
|
clearSeen([thread.id]);
|
|
267
306
|
actions.setRead(thread.id, false).catch(fail("Couldn't mark unread"));
|
|
@@ -342,7 +381,6 @@ function ThreadListBody({
|
|
|
342
381
|
[sdk],
|
|
343
382
|
);
|
|
344
383
|
|
|
345
|
-
const multiHost = view?.multiHost ?? false;
|
|
346
384
|
const built = forest !== null && view !== null;
|
|
347
385
|
const rowController: RowController | null = useMemo(() => {
|
|
348
386
|
if (!built) return null;
|
|
@@ -350,11 +388,7 @@ function ThreadListBody({
|
|
|
350
388
|
compact: isCompactViewport,
|
|
351
389
|
comfortable: client.density === "comfortable",
|
|
352
390
|
setEditingId,
|
|
353
|
-
showPullRequests: prefs.showPullRequests,
|
|
354
391
|
harnessIcon: prefs.harnessIcon,
|
|
355
|
-
defaultBranchOf: (thread) =>
|
|
356
|
-
defaultBranches.has(thread.projectId) ? defaultBranches.get(thread.projectId) ?? null : undefined,
|
|
357
|
-
multiHost,
|
|
358
392
|
provider: providerDisplay,
|
|
359
393
|
sections: sidebar.sections,
|
|
360
394
|
mode: prefs.organizationMode,
|
|
@@ -364,11 +398,10 @@ function ThreadListBody({
|
|
|
364
398
|
applyToggle(toggleChip(row, prefs, forest!));
|
|
365
399
|
},
|
|
366
400
|
onToggleOlder: (row) => {
|
|
367
|
-
const { prefs, forest
|
|
368
|
-
|
|
369
|
-
applyToggle(toggleOlder(row, prefs, group, forest!));
|
|
401
|
+
const { prefs, forest } = latest.current;
|
|
402
|
+
applyToggle(toggleOlder(row, prefs, forest!));
|
|
370
403
|
},
|
|
371
|
-
|
|
404
|
+
onToggleSettled: (row) => applyToggle(toggleSettled(row, latest.current.prefs)),
|
|
372
405
|
onToggleEnvironment: (environmentId) => {
|
|
373
406
|
const { collapsedEnvironments } = latest.current.prefs;
|
|
374
407
|
update({
|
|
@@ -398,11 +431,8 @@ function ThreadListBody({
|
|
|
398
431
|
built,
|
|
399
432
|
isCompactViewport,
|
|
400
433
|
client.density,
|
|
401
|
-
prefs.showPullRequests,
|
|
402
434
|
prefs.harnessIcon,
|
|
403
435
|
prefs.organizationMode,
|
|
404
|
-
defaultBranches,
|
|
405
|
-
multiHost,
|
|
406
436
|
providerDisplay,
|
|
407
437
|
sidebar.sections,
|
|
408
438
|
onNavigate,
|
|
@@ -414,54 +444,58 @@ function ThreadListBody({
|
|
|
414
444
|
]);
|
|
415
445
|
|
|
416
446
|
const live: ListLive = useMemo(
|
|
417
|
-
() => ({ now, stamps, notes,
|
|
447
|
+
() => ({ now, stamps, notes, treeOf: (threadId) => forest?.treeOf.get(threadId) }),
|
|
418
448
|
[now, stamps, notes, forest],
|
|
419
449
|
);
|
|
420
450
|
|
|
421
451
|
const activeGroupId = useMemo(() => {
|
|
422
|
-
const
|
|
423
|
-
return
|
|
452
|
+
const tree = activeThreadId === null || forest === null ? undefined : forest.treeOf.get(activeThreadId);
|
|
453
|
+
return tree === undefined
|
|
424
454
|
? null
|
|
425
|
-
: groupIdForRoot(
|
|
455
|
+
: groupIdForRoot(tree.root.thread, { mode: prefs.organizationMode, projects: sidebar.projects });
|
|
426
456
|
}, [activeThreadId, forest, prefs.organizationMode, sidebar.projects]);
|
|
427
|
-
const showArchived = prefs.
|
|
457
|
+
const showArchived = prefs.showArchived;
|
|
428
458
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const {
|
|
433
|
-
|
|
434
|
-
const families = group.rootIds.flatMap((id) => forest!.familyOf.get(id) ?? []);
|
|
435
|
-
const plan = markAllReadPlan(families, {
|
|
436
|
-
activeThreadId,
|
|
437
|
-
finishedAt: stamps.finishedAt,
|
|
438
|
-
seenAt: stamps.seenAt,
|
|
439
|
-
});
|
|
459
|
+
/** Marks every unread thread in the trees read, asking first above MARK_ALL_CONFIRM_ABOVE. `where` names them. */
|
|
460
|
+
const markTreesRead = useCallback(
|
|
461
|
+
(trees: readonly ThreadTree[], where: string) => {
|
|
462
|
+
const { activeThreadId, stamps } = latest.current;
|
|
463
|
+
const plan = markAllReadPlan(trees, { activeThreadId, finishedAt: stamps.finishedAt, seenAt: stamps.seenAt });
|
|
440
464
|
const run = () => {
|
|
441
465
|
if (plan.seen.length > 0) markSeen(plan.seen);
|
|
442
466
|
for (const id of plan.read) actions.setRead(id, true).catch(() => undefined);
|
|
443
467
|
};
|
|
444
468
|
if (plan.read.length === 0) {
|
|
445
|
-
toast(`Nothing unread in ${
|
|
469
|
+
toast(`Nothing unread in ${where}`);
|
|
446
470
|
} else if (plan.read.length > MARK_ALL_CONFIRM_ABOVE) {
|
|
447
471
|
setConfirm({
|
|
448
472
|
title: `Mark ${plan.read.length} threads read?`,
|
|
449
|
-
description: `Every unread thread in ${
|
|
473
|
+
description: `Every unread thread in ${where}, child threads included, will be marked read.`,
|
|
450
474
|
confirmLabel: "Mark all read",
|
|
451
475
|
run,
|
|
452
476
|
});
|
|
453
477
|
} else {
|
|
454
478
|
run();
|
|
455
479
|
}
|
|
456
|
-
}
|
|
480
|
+
},
|
|
481
|
+
[actions, markSeen],
|
|
482
|
+
);
|
|
483
|
+
const onMarkListRead = useCallback(() => markTreesRead(latest.current.forest?.trees ?? [], "the list"), [markTreesRead]);
|
|
484
|
+
const onToggleNeedYou = useCallback(() => setNeedYouOn((on) => !on), []);
|
|
485
|
+
|
|
486
|
+
const groupController: GroupController | null = useMemo(() => {
|
|
487
|
+
if (!built) return null;
|
|
488
|
+
// Folded roots count too: every tree bucketed in the group.
|
|
489
|
+
const markAll = (group: GroupView) =>
|
|
490
|
+
markTreesRead(
|
|
491
|
+
group.rootIds.flatMap((id) => latest.current.forest!.treeOf.get(id) ?? []),
|
|
492
|
+
group.descriptor.label,
|
|
493
|
+
);
|
|
457
494
|
return {
|
|
458
495
|
compact: isCompactViewport,
|
|
459
496
|
activeGroupId,
|
|
460
497
|
showArchived,
|
|
461
|
-
onToggleArchived: () =>
|
|
462
|
-
update({
|
|
463
|
-
threadLifecycles: latest.current.prefs.threadLifecycles.includes("archived") ? ["active"] : ["active", "archived"],
|
|
464
|
-
}),
|
|
498
|
+
onToggleArchived: () => update({ showArchived: !latest.current.prefs.showArchived }),
|
|
465
499
|
canCreateSections: prefs.organizationMode === "chronological",
|
|
466
500
|
onToggleCollapse: (group) => {
|
|
467
501
|
const { prefs, forest } = latest.current;
|
|
@@ -502,7 +536,7 @@ function ThreadListBody({
|
|
|
502
536
|
onMarkAllRead: markAll,
|
|
503
537
|
onNewSection: () => setNewSectionOpen(true),
|
|
504
538
|
};
|
|
505
|
-
}, [built, isCompactViewport, activeGroupId, showArchived, prefs.organizationMode, applyToggle, actions, onNavigate, update, sdk,
|
|
539
|
+
}, [built, isCompactViewport, activeGroupId, showArchived, prefs.organizationMode, applyToggle, actions, onNavigate, update, sdk, markTreesRead]);
|
|
506
540
|
|
|
507
541
|
const sensors = useSensors(
|
|
508
542
|
useSensor(MouseSensor, MOUSE_SENSOR),
|
|
@@ -524,8 +558,7 @@ function ThreadListBody({
|
|
|
524
558
|
}, []);
|
|
525
559
|
|
|
526
560
|
const dropContext = useMemo(() => {
|
|
527
|
-
|
|
528
|
-
const groupOfThread = new Map<string, string>(Object.entries(view?.attention?.homeGroupIds ?? {}));
|
|
561
|
+
const groupOfThread = new Map<string, string>();
|
|
529
562
|
for (const group of [...(view?.groups ?? []), ...(view?.more ?? [])]) {
|
|
530
563
|
for (const row of group.rows) if (row.type === "thread") groupOfThread.set(row.info.thread.id, group.descriptor.id);
|
|
531
564
|
}
|
|
@@ -679,24 +712,23 @@ function ThreadListBody({
|
|
|
679
712
|
return (
|
|
680
713
|
<ListLiveContext.Provider value={live}>
|
|
681
714
|
<div className="flex w-full min-w-0 flex-col px-1.5 pb-2">
|
|
682
|
-
<
|
|
715
|
+
<ListHeader
|
|
716
|
+
mode={prefs.organizationMode}
|
|
717
|
+
needYouCount={view.needYouCount}
|
|
718
|
+
needYouOnly={needYouOn}
|
|
719
|
+
onToggleNeedYou={onToggleNeedYou}
|
|
720
|
+
onMarkAllRead={onMarkListRead}
|
|
721
|
+
prefs={prefs}
|
|
722
|
+
client={client}
|
|
723
|
+
onPrefs={update}
|
|
724
|
+
onClient={updateClient}
|
|
725
|
+
/>
|
|
726
|
+
<PullRequestProbes threadIds={pullRequestLookups} onAnswer={onPullRequest} />
|
|
683
727
|
{threads.length === 0 ? (
|
|
684
728
|
// bb's own pinned New thread button covers the empty list.
|
|
685
729
|
<p className="px-3 py-4 text-sm text-muted-foreground">No threads yet.</p>
|
|
686
730
|
) : (
|
|
687
731
|
<DndContext sensors={sensors} collisionDetection={collision} onDragMove={onDragMove} onDragEnd={onDragEnd} onDragCancel={onDragCancel}>
|
|
688
|
-
{view.attention !== null ? (
|
|
689
|
-
<AttentionSection
|
|
690
|
-
view={view.attention}
|
|
691
|
-
rowController={rowController}
|
|
692
|
-
environmentProviders={environmentProviders}
|
|
693
|
-
dropStates={dropStates}
|
|
694
|
-
activeThreadId={activeThreadId}
|
|
695
|
-
editingId={editingId}
|
|
696
|
-
now={now}
|
|
697
|
-
stamps={stamps}
|
|
698
|
-
/>
|
|
699
|
-
) : null}
|
|
700
732
|
{view.groups.map((group) => (
|
|
701
733
|
<GroupSection
|
|
702
734
|
key={group.descriptor.id}
|
|
@@ -824,7 +856,7 @@ function ThreadListBody({
|
|
|
824
856
|
<ThreadDetails
|
|
825
857
|
info={details}
|
|
826
858
|
controller={rowController}
|
|
827
|
-
showPullRequest
|
|
859
|
+
showPullRequest
|
|
828
860
|
actions={{
|
|
829
861
|
open: () => {
|
|
830
862
|
setDetailsId(null);
|
|
@@ -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 {
|
|
22
|
+
import { pluginStatusWins } from "../model/state";
|
|
23
23
|
import { 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 { ChildDot, GlyphIcon, NoteLine, PluginStatusGlyph } from "./glyphs";
|
|
28
28
|
import { ProviderBadge } from "./ProviderBadge";
|
|
29
29
|
import { PullRequestBadge } from "./PullRequestBadge";
|
|
30
30
|
import { RenameEditor } from "./RenameEditor";
|
|
@@ -58,12 +58,11 @@ function swallowNextClick(): void {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* A quiet title, and its chip: the foreground mixed toward the
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* the light theme.
|
|
61
|
+
* A quiet title, and its chip: the foreground mixed toward the sidebar in
|
|
62
|
+
* oklch, which keeps 4.5:1 in both of bb's themes. Opacity blends in sRGB and
|
|
63
|
+
* lands lower in the light theme.
|
|
65
64
|
*/
|
|
66
|
-
const QUIET_TEXT = "text-[color:color-mix(in_oklch,var(--foreground)
|
|
65
|
+
const QUIET_TEXT = "text-[color:color-mix(in_oklch,var(--foreground)_68%,var(--sidebar))]";
|
|
67
66
|
|
|
68
67
|
export const ROW_ICON_BUTTON =
|
|
69
68
|
"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";
|
|
@@ -175,7 +174,7 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
175
174
|
);
|
|
176
175
|
const menuItems = rowMenuItems({
|
|
177
176
|
thread,
|
|
178
|
-
unread: info.unread,
|
|
177
|
+
unread: row.depth === 0 ? row.treeUnread : info.unread,
|
|
179
178
|
splitAvailable: split.isAvailable,
|
|
180
179
|
isRoot: thread.parentThreadId === null,
|
|
181
180
|
hasSections: controller.mode === "chronological" || controller.sections.length > 0,
|
|
@@ -219,10 +218,7 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
219
218
|
controller.setEditingId(thread.id);
|
|
220
219
|
};
|
|
221
220
|
|
|
222
|
-
const
|
|
223
|
-
const branch = thread.environment?.branchName ?? null;
|
|
224
|
-
const showPullRequest =
|
|
225
|
-
controller.showPullRequests && row.depth === 0 && branch !== null && defaultBranch !== undefined && branch !== defaultBranch;
|
|
221
|
+
const showPullRequest = row.pullRequest !== null;
|
|
226
222
|
|
|
227
223
|
const stateSlot = miniMap ? (
|
|
228
224
|
<SplitMiniMap panes={miniMap} label={`${thread.displayTitle} — open in split; ${info.state.label}`} working={info.flags.has("working")} />
|
|
@@ -300,7 +296,7 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
300
296
|
const chip = row.chip;
|
|
301
297
|
const indent = rowIndent(row.depth);
|
|
302
298
|
const note = row.note;
|
|
303
|
-
const twoLines =
|
|
299
|
+
const twoLines = note !== null || row.branchLine !== null;
|
|
304
300
|
const dimmed = row.dimmed && !editing;
|
|
305
301
|
const menuShowing = menuOpen || contextOpen;
|
|
306
302
|
// Desktop: the actions cross-fade over the harness and age, as bb's
|
|
@@ -366,7 +362,10 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
366
362
|
}}
|
|
367
363
|
className="absolute inset-0 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring"
|
|
368
364
|
/>
|
|
369
|
-
<span className="pointer-events-none relative flex size-4 shrink-0 items-center justify-center">
|
|
365
|
+
<span className="pointer-events-none relative flex size-4 shrink-0 items-center justify-center">
|
|
366
|
+
{stateSlot}
|
|
367
|
+
{row.childDot !== null ? <ChildDot flag={row.childDot} /> : null}
|
|
368
|
+
</span>
|
|
370
369
|
{row.nested ? (
|
|
371
370
|
// Tight against the title, and over the row's gap, so it adds 8px.
|
|
372
371
|
<span
|
|
@@ -404,27 +403,31 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
404
403
|
}}
|
|
405
404
|
/>
|
|
406
405
|
) : (
|
|
407
|
-
<span
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
406
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
407
|
+
<span
|
|
408
|
+
title={thread.displayTitle}
|
|
409
|
+
className={cn(
|
|
410
|
+
"min-w-0 truncate",
|
|
411
|
+
row.bold ? "font-semibold" : "font-normal",
|
|
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`,
|
|
416
|
+
)}
|
|
417
|
+
>
|
|
418
|
+
{/* Plain text: mention pills lost the truncation fight. */}
|
|
419
|
+
{thread.displayTitle}
|
|
420
|
+
</span>
|
|
421
|
+
{/* On a two-line row the badge stays on the title's line, not centred beside both. */}
|
|
422
|
+
{row.pullRequest === "title" && twoLines ? <PullRequestBadge threadId={thread.id} /> : null}
|
|
420
423
|
</span>
|
|
421
424
|
)}
|
|
422
425
|
{!editing && note !== null ? (
|
|
423
426
|
<span className="min-w-0 truncate text-xs leading-4 text-muted-foreground" title={noteText(note)}>
|
|
424
427
|
<NoteLine note={note} />
|
|
425
428
|
</span>
|
|
426
|
-
) :
|
|
427
|
-
<
|
|
429
|
+
) : row.branchLine !== null && !editing ? (
|
|
430
|
+
<BranchLine row={row} branch={row.branchLine} />
|
|
428
431
|
) : null}
|
|
429
432
|
</span>
|
|
430
433
|
{row.hiddenBadge ? (
|
|
@@ -432,7 +435,7 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
432
435
|
<Icon name={ICONS.hidden} aria-hidden className="size-3.5" />
|
|
433
436
|
</span>
|
|
434
437
|
) : null}
|
|
435
|
-
{
|
|
438
|
+
{row.pullRequest === "title" && !twoLines && !editing ? (
|
|
436
439
|
<span className="pointer-events-none relative">
|
|
437
440
|
<PullRequestBadge threadId={thread.id} />
|
|
438
441
|
</span>
|
|
@@ -441,8 +444,8 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
441
444
|
<button
|
|
442
445
|
type="button"
|
|
443
446
|
aria-expanded={chip.expanded}
|
|
444
|
-
aria-label={chipLabel(thread.displayTitle, chip.count, chip.
|
|
445
|
-
title={chipLabel(thread.displayTitle, chip.count, chip.
|
|
447
|
+
aria-label={chipLabel(thread.displayTitle, chip.count, chip.expanded)}
|
|
448
|
+
title={chipLabel(thread.displayTitle, chip.count, chip.expanded)}
|
|
446
449
|
onClick={(event) => {
|
|
447
450
|
event.preventDefault();
|
|
448
451
|
event.stopPropagation();
|
|
@@ -450,20 +453,12 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
450
453
|
}}
|
|
451
454
|
onPointerDown={(event) => event.stopPropagation()}
|
|
452
455
|
onKeyDown={(event) => event.stopPropagation()}
|
|
453
|
-
data-tone={chipTone(chip.flag)}
|
|
454
456
|
className={cn(
|
|
455
|
-
"pointer-events-auto relative z-10 inline-flex h-5 shrink-0 items-center gap-0.5 rounded-md
|
|
456
|
-
CHIP_TONE_CLASS[chipTone(chip.flag)],
|
|
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",
|
|
457
458
|
dimmed && QUIET_TEXT,
|
|
458
459
|
)}
|
|
459
460
|
>
|
|
460
461
|
{chip.count}
|
|
461
|
-
{chip.flag !== null ? <FlagGlyph flag={chip.flag} className="size-3" /> : null}
|
|
462
|
-
{controller.harnessIcon !== "hidden"
|
|
463
|
-
? chip.providerIds.map((providerId) => (
|
|
464
|
-
<ProviderBadge key={providerId} display={controller.provider(providerId)} className="size-3 [&_*]:size-3" />
|
|
465
|
-
))
|
|
466
|
-
: null}
|
|
467
462
|
<Icon
|
|
468
463
|
name={ICONS.expand}
|
|
469
464
|
aria-hidden
|
|
@@ -471,7 +466,7 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
471
466
|
/>
|
|
472
467
|
</button>
|
|
473
468
|
) : null}
|
|
474
|
-
{!editing ? (
|
|
469
|
+
{!editing && row.harness ? (
|
|
475
470
|
<span
|
|
476
471
|
className={cn(
|
|
477
472
|
"pointer-events-none relative inline-flex shrink-0 transition-opacity",
|
|
@@ -490,14 +485,16 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
490
485
|
</kbd>
|
|
491
486
|
) : (
|
|
492
487
|
<>
|
|
493
|
-
{row.
|
|
488
|
+
{row.machine !== null ? (
|
|
494
489
|
<span
|
|
495
|
-
title={`
|
|
496
|
-
|
|
490
|
+
title={`On ${row.machine}`}
|
|
491
|
+
aria-label={`On ${row.machine}`}
|
|
492
|
+
className={cn("pointer-events-none max-w-20 truncate text-[11px] text-muted-foreground transition-opacity", fadeClass)}
|
|
497
493
|
>
|
|
498
|
-
{row.
|
|
494
|
+
{row.machine}
|
|
499
495
|
</span>
|
|
500
|
-
) :
|
|
496
|
+
) : null}
|
|
497
|
+
{time !== null ? (
|
|
501
498
|
<span
|
|
502
499
|
title={time.label}
|
|
503
500
|
aria-label={time.label}
|
|
@@ -521,6 +518,22 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
521
518
|
: "absolute right-0 opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100",
|
|
522
519
|
)}
|
|
523
520
|
>
|
|
521
|
+
{!compact && row.treeUnread ? (
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
aria-label="Mark read"
|
|
525
|
+
title="Mark read"
|
|
526
|
+
className={ROW_ICON_BUTTON}
|
|
527
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
528
|
+
onClick={(event) => {
|
|
529
|
+
event.preventDefault();
|
|
530
|
+
event.stopPropagation();
|
|
531
|
+
controller.onMenuAction("mark-read", thread);
|
|
532
|
+
}}
|
|
533
|
+
>
|
|
534
|
+
<Icon name={ICONS.markRead} aria-hidden className="size-4" />
|
|
535
|
+
</button>
|
|
536
|
+
) : null}
|
|
524
537
|
{!compact ? (
|
|
525
538
|
<button
|
|
526
539
|
type="button"
|
|
@@ -601,38 +614,17 @@ export const ThreadRowView = memo(function ThreadRowView({
|
|
|
601
614
|
);
|
|
602
615
|
});
|
|
603
616
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
multiHost: boolean;
|
|
611
|
-
defaultBranch: string | null | undefined;
|
|
612
|
-
}) {
|
|
613
|
-
const thread = row.info.thread;
|
|
614
|
-
const environment = thread.environment;
|
|
615
|
-
const parts: React.ReactNode[] = [];
|
|
616
|
-
// The branch earns the line only when it isn't the project's default:
|
|
617
|
-
// "main" on every row said nothing.
|
|
618
|
-
const branch = environment?.branchName ?? null;
|
|
619
|
-
if (branch !== null && branch !== defaultBranch) {
|
|
620
|
-
parts.push(
|
|
621
|
-
<span key="branch" className="inline-flex min-w-0 items-center gap-0.5">
|
|
617
|
+
/** The Comfortable second line: the branch, then its pull request badge. */
|
|
618
|
+
function BranchLine({ row, branch }: { row: ThreadRow; branch: string }) {
|
|
619
|
+
const environment = row.info.thread.environment;
|
|
620
|
+
return (
|
|
621
|
+
<span className="flex min-w-0 items-center gap-1.5 text-xs leading-4 text-muted-foreground">
|
|
622
|
+
<span className="inline-flex min-w-0 items-center gap-0.5">
|
|
622
623
|
<Icon name={environment?.isWorktree ? ICONS.worktree : ICONS.branch} aria-hidden className="size-3 shrink-0" />
|
|
623
624
|
<span className="truncate">{branch}</span>
|
|
624
625
|
{environment?.isWorktree ? <span className="sr-only"> (worktree)</span> : null}
|
|
625
|
-
</span
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
parts.push(
|
|
630
|
-
<span key="host" className="inline-flex shrink-0 items-center gap-0.5">
|
|
631
|
-
<Icon name={ICONS.machine} aria-hidden className="size-3" />
|
|
632
|
-
{thread.host.name}
|
|
633
|
-
</span>,
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
if (parts.length === 0) return null;
|
|
637
|
-
return <span className="flex min-w-0 items-center gap-2 text-xs leading-4 text-muted-foreground">{parts}</span>;
|
|
626
|
+
</span>
|
|
627
|
+
{row.pullRequest === "second-line" ? <PullRequestBadge threadId={row.info.thread.id} /> : null}
|
|
628
|
+
</span>
|
|
629
|
+
);
|
|
638
630
|
}
|