@gperezmz/bb-plugin-thread-glance 0.2.1 → 0.2.3
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 +135 -104
- package/features/thread-list/components/ThreadDetails.tsx +2 -3
- package/features/thread-list/components/ThreadList.tsx +12 -11
- package/features/thread-list/components/ThreadRowView.tsx +23 -19
- 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/useAttentionHold.ts +19 -0
- package/features/thread-list/model/{needs-you.ts → attention.ts} +48 -28
- 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 +26 -22
- 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 +96 -56
- package/features/thread-list/testing/fixtures.ts +8 -4
- 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/features/thread-list/data/useNeedsYouHold.ts +0 -19
|
@@ -20,10 +20,10 @@ import {
|
|
|
20
20
|
} from "./groups";
|
|
21
21
|
import { addCounters, countFamilies, EMPTY_COUNTERS, type Counters } from "./counters";
|
|
22
22
|
import { comparePinned, effectiveSortField, makeComparator, type SortKey } from "./sort";
|
|
23
|
-
import {
|
|
23
|
+
import { inAttention, isAttended, type SectionFamily } from "./attention";
|
|
24
24
|
import { mostUrgent, type Flag } from "./state";
|
|
25
25
|
import type { Targets } from "./expansion";
|
|
26
|
-
import {
|
|
26
|
+
import type { RowNote } from "./notes";
|
|
27
27
|
|
|
28
28
|
/** How many of a group's newest quiet roots stay out of its older fold. */
|
|
29
29
|
export const KEEP_QUIET = 5;
|
|
@@ -50,13 +50,21 @@ export interface ThreadRow {
|
|
|
50
50
|
/** Title of the thread this one attaches to, for tooltips and labels. */
|
|
51
51
|
parentTitle: string | null;
|
|
52
52
|
chip: Chip | null;
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
/**
|
|
53
|
+
/** The title is bold: the thread is unread and its family is not attended. */
|
|
54
|
+
bold: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The line under the title. In Needs attention, a row that itself needs
|
|
57
|
+
* attention says why, and an attended family's rows have none; any other
|
|
58
|
+
* row says why it waits on you or failed.
|
|
59
|
+
*/
|
|
60
|
+
note: RowNote | null;
|
|
61
|
+
/** The title, and the chip with it, step back: see `isDimmed`. */
|
|
62
|
+
dimmed: boolean;
|
|
63
|
+
/** A hidden thread shown because it needs attention or failed. */
|
|
56
64
|
hiddenBadge: boolean;
|
|
57
65
|
/** "In project X" when the thread is outside its family's group. */
|
|
58
66
|
crossGroupLabel: string | null;
|
|
59
|
-
/** A root in Needs
|
|
67
|
+
/** A root in Needs attention: its home group's name, drawn where the age goes. */
|
|
60
68
|
homeGroupLabel: string | null;
|
|
61
69
|
projectId: string;
|
|
62
70
|
}
|
|
@@ -71,7 +79,6 @@ export interface OlderRow {
|
|
|
71
79
|
count: number;
|
|
72
80
|
expanded: boolean;
|
|
73
81
|
depth: number;
|
|
74
|
-
rails: (Rail | null)[];
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
export interface EnvironmentRow {
|
|
@@ -86,10 +93,9 @@ export interface EnvironmentRow {
|
|
|
86
93
|
collapsed: boolean;
|
|
87
94
|
flag: Flag | null;
|
|
88
95
|
depth: number;
|
|
89
|
-
rails: (Rail | null)[];
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
/** Under a family in Needs
|
|
98
|
+
/** Under a closed family in Needs attention: how many of its child threads it leaves out ("+N more"). Opens the family. */
|
|
93
99
|
export interface LeftOutRow {
|
|
94
100
|
type: "left-out";
|
|
95
101
|
key: string;
|
|
@@ -97,7 +103,6 @@ export interface LeftOutRow {
|
|
|
97
103
|
scopeId: string;
|
|
98
104
|
count: number;
|
|
99
105
|
depth: number;
|
|
100
|
-
rails: (Rail | null)[];
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
export type Row = ThreadRow | OlderRow | EnvironmentRow | LeftOutRow;
|
|
@@ -111,16 +116,16 @@ export interface GroupView {
|
|
|
111
116
|
collapsed: boolean;
|
|
112
117
|
hidden: boolean;
|
|
113
118
|
rows: Row[];
|
|
114
|
-
/** Every family root bucketed in the group, those in Needs
|
|
119
|
+
/** Every family root bucketed in the group, those in Needs attention and behind folds included. */
|
|
115
120
|
rootIds: string[];
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
/** The id Needs
|
|
119
|
-
export const
|
|
123
|
+
/** The id Needs attention's rows are drawn under; no group has it. */
|
|
124
|
+
export const ATTENTION_GROUP_ID = "attention";
|
|
120
125
|
|
|
121
|
-
/** The Needs
|
|
122
|
-
export interface
|
|
123
|
-
/** How many families the section holds, for its header. */
|
|
126
|
+
/** The Needs attention section: every family with a thread that needs attention, or held there while one of its threads is open. */
|
|
127
|
+
export interface AttentionView {
|
|
128
|
+
/** How many families the section holds, attended ones left out, for its header. */
|
|
124
129
|
familyCount: number;
|
|
125
130
|
rows: Row[];
|
|
126
131
|
/** The home group of each thread drawn here, by thread id: a drop on its row acts there. */
|
|
@@ -129,7 +134,7 @@ export interface NeedsYouView {
|
|
|
129
134
|
|
|
130
135
|
export interface ListView {
|
|
131
136
|
/** Null when no family is in the section. */
|
|
132
|
-
|
|
137
|
+
attention: AttentionView | null;
|
|
133
138
|
groups: GroupView[];
|
|
134
139
|
/** Hidden groups, shown in the More popover. */
|
|
135
140
|
more: GroupView[];
|
|
@@ -147,12 +152,13 @@ export interface ViewInputs {
|
|
|
147
152
|
sections: readonly PluginSidebarSection[];
|
|
148
153
|
prefs: Preferences;
|
|
149
154
|
activeThreadId: string | null;
|
|
150
|
-
/** The
|
|
151
|
-
|
|
155
|
+
/** The family Needs attention keeps while one of its threads is open, as it was when opened: the section orders it by this. */
|
|
156
|
+
held: SectionFamily | null;
|
|
152
157
|
targets: Targets;
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
interface Context extends ViewInputs {
|
|
161
|
+
heldRootId: string | null;
|
|
156
162
|
compare: (a: SortKey, b: SortKey) => number;
|
|
157
163
|
expandedChildren: ReadonlySet<string>;
|
|
158
164
|
expandedOlder: ReadonlySet<string>;
|
|
@@ -164,6 +170,10 @@ interface Context extends ViewInputs {
|
|
|
164
170
|
activePath: ReadonlySet<string>;
|
|
165
171
|
projectNames: ReadonlyMap<string, string>;
|
|
166
172
|
sectionNames: ReadonlyMap<string, string>;
|
|
173
|
+
/** The rows are drawn in Needs attention. */
|
|
174
|
+
drawnInAttention: boolean;
|
|
175
|
+
/** The rows are an attended family's: they draw as in its home group, with no bold title or why line. */
|
|
176
|
+
attended: boolean;
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
function titleOf(context: Context, id: string | null): string | null {
|
|
@@ -190,12 +200,23 @@ function crossGroupLabel(context: Context, info: ThreadInfo, root: ThreadInfo):
|
|
|
190
200
|
}
|
|
191
201
|
}
|
|
192
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Whether a row's title steps back: a quiet thread does, at any depth, so
|
|
205
|
+
* brightness shows state and depth is left to the indent and smaller text.
|
|
206
|
+
* A root with children stays bright until its whole family is quiet.
|
|
207
|
+
*/
|
|
208
|
+
function isDimmed(context: Context, info: ThreadInfo, chip: Chip | null): boolean {
|
|
209
|
+
if (info.parentId === null && chip !== null) return context.forest.familyOf.get(info.thread.id)?.quiet ?? info.quiet;
|
|
210
|
+
return info.quiet;
|
|
211
|
+
}
|
|
212
|
+
|
|
193
213
|
function threadRow(
|
|
194
214
|
context: Context,
|
|
195
215
|
info: ThreadInfo,
|
|
196
216
|
root: ThreadInfo,
|
|
197
217
|
options: { depth: number; nested: boolean; chip: Chip | null },
|
|
198
218
|
): ThreadRow {
|
|
219
|
+
const needsAttention = !info.thread.isArchived && info.attentionFlags.size > 0;
|
|
199
220
|
return {
|
|
200
221
|
type: "thread",
|
|
201
222
|
key: `thread:${info.thread.id}`,
|
|
@@ -204,7 +225,9 @@ function threadRow(
|
|
|
204
225
|
nested: options.nested,
|
|
205
226
|
parentTitle: titleOf(context, info.parentId),
|
|
206
227
|
chip: options.chip,
|
|
207
|
-
|
|
228
|
+
bold: info.unread && !context.attended,
|
|
229
|
+
note: context.attended ? null : context.drawnInAttention && needsAttention ? info.attentionNote : info.note,
|
|
230
|
+
dimmed: isDimmed(context, info, options.chip),
|
|
208
231
|
hiddenBadge: info.thread.isHidden,
|
|
209
232
|
crossGroupLabel: crossGroupLabel(context, info, root),
|
|
210
233
|
homeGroupLabel: null,
|
|
@@ -212,11 +235,11 @@ function threadRow(
|
|
|
212
235
|
};
|
|
213
236
|
}
|
|
214
237
|
|
|
215
|
-
/** The children of `parent` that get a row: hidden ones only when they need
|
|
238
|
+
/** The children of `parent` that get a row: hidden ones only when they need attention or failed. */
|
|
216
239
|
function eligibleChildren(context: Context, parentId: string): ThreadInfo[] {
|
|
217
240
|
return (context.forest.children.get(parentId) ?? [])
|
|
218
241
|
.map((id) => context.forest.infos.get(id)!)
|
|
219
|
-
.filter((info) => !info.thread.isHidden || info.
|
|
242
|
+
.filter((info) => !info.thread.isHidden || info.attentionFlags.size > 0);
|
|
220
243
|
}
|
|
221
244
|
|
|
222
245
|
function subtreeOf(context: Context, id: string): Subtree {
|
|
@@ -248,7 +271,6 @@ function foldedChildren(
|
|
|
248
271
|
count,
|
|
249
272
|
expanded,
|
|
250
273
|
depth,
|
|
251
|
-
rails: [],
|
|
252
274
|
}
|
|
253
275
|
: null;
|
|
254
276
|
|
|
@@ -330,7 +352,6 @@ function clusterEnvironments(context: Context, units: Unit[], depth: number): Ro
|
|
|
330
352
|
collapsed,
|
|
331
353
|
flag: collapsed ? mostUrgent(flags) : null,
|
|
332
354
|
depth,
|
|
333
|
-
rails: [],
|
|
334
355
|
});
|
|
335
356
|
if (!collapsed) {
|
|
336
357
|
for (const member of members) {
|
|
@@ -357,7 +378,7 @@ function childProviders(parent: ThreadInfo, descendants: readonly ThreadInfo[]):
|
|
|
357
378
|
function chipOf(context: Context, info: ThreadInfo, expanded: boolean): Chip | null {
|
|
358
379
|
const subtree = subtreeOf(context, info.thread.id);
|
|
359
380
|
const hasChildren =
|
|
360
|
-
subtree.visibleCount > 0 || subtree.descendants.some((descendant) => descendant.thread.isHidden && descendant.
|
|
381
|
+
subtree.visibleCount > 0 || subtree.descendants.some((descendant) => descendant.thread.isHidden && descendant.attentionFlags.size > 0);
|
|
361
382
|
if (!hasChildren) return null;
|
|
362
383
|
return {
|
|
363
384
|
count: subtree.visibleCount,
|
|
@@ -369,7 +390,7 @@ function chipOf(context: Context, info: ThreadInfo, expanded: boolean): Chip | n
|
|
|
369
390
|
|
|
370
391
|
/** What a thread and everything under it carry, for an environment folder's glyph. */
|
|
371
392
|
function rollupFlags(context: Context, info: ThreadInfo): Set<Flag> {
|
|
372
|
-
const flags = new Set<Flag>([...info.
|
|
393
|
+
const flags = new Set<Flag>([...info.attentionFlags, ...subtreeOf(context, info.thread.id).flags]);
|
|
373
394
|
if (info.flags.has("working")) flags.add("working");
|
|
374
395
|
return flags;
|
|
375
396
|
}
|
|
@@ -407,8 +428,8 @@ function familyUnit(context: Context, family: Family): Unit {
|
|
|
407
428
|
}
|
|
408
429
|
|
|
409
430
|
/** Waiting on you, then failed, then offline, then unread, then the rest. */
|
|
410
|
-
export function urgency(family: Family): number {
|
|
411
|
-
const flags = family.
|
|
431
|
+
export function urgency(family: Pick<Family, "attentionFlags">): number {
|
|
432
|
+
const flags = family.attentionFlags;
|
|
412
433
|
if (flags.has("waits-on-you")) return 0;
|
|
413
434
|
if (flags.has("unread-failed") || flags.has("queue-failed")) return 1;
|
|
414
435
|
if (flags.has("offline")) return 2;
|
|
@@ -417,20 +438,34 @@ export function urgency(family: Family): number {
|
|
|
417
438
|
}
|
|
418
439
|
|
|
419
440
|
/**
|
|
420
|
-
* One family's rows in Needs
|
|
421
|
-
* that needs
|
|
422
|
-
*
|
|
441
|
+
* One family's rows in Needs attention. It arrives with the path from the root
|
|
442
|
+
* down to each thread that needs attention or is open. Closed, the path is all
|
|
443
|
+
* it draws, with one "+N more" line for the child threads left out; opened
|
|
444
|
+
* with the root's chip, it draws as in its home group and keeps the path.
|
|
423
445
|
*/
|
|
424
|
-
function
|
|
446
|
+
function attentionFamilyRows(groupContext: Context, family: Family, homeGroupLabel: string): Row[] {
|
|
447
|
+
const context: Context = {
|
|
448
|
+
...groupContext,
|
|
449
|
+
drawnInAttention: true,
|
|
450
|
+
attended: isAttended(family, groupContext.heldRootId),
|
|
451
|
+
};
|
|
425
452
|
const root = family.root;
|
|
426
453
|
const onPath = new Set<string>([root.thread.id]);
|
|
427
454
|
for (const info of family.descendants) {
|
|
428
|
-
const
|
|
429
|
-
if (!
|
|
455
|
+
const attention = !info.thread.isArchived && info.attentionFlags.size > 0;
|
|
456
|
+
if (!attention && !info.isActive) continue;
|
|
430
457
|
onPath.add(info.thread.id);
|
|
431
458
|
for (const ancestor of ancestorsOf(info.thread.id, context.forest.infos, root.thread.id)) onPath.add(ancestor);
|
|
432
459
|
}
|
|
433
|
-
const
|
|
460
|
+
const expanded = context.expandedChildren.has(root.thread.id);
|
|
461
|
+
if (expanded) {
|
|
462
|
+
// The path is kept the way a reveal target's is.
|
|
463
|
+
const kept: Context = { ...context, revealIds: new Set([...context.revealIds, ...onPath]) };
|
|
464
|
+
const [rootRow, ...rest] = foldedFamilyRows(kept, family);
|
|
465
|
+
return [{ ...(rootRow as ThreadRow), homeGroupLabel }, ...rest];
|
|
466
|
+
}
|
|
467
|
+
const chip = chipOf(context, root, false);
|
|
468
|
+
const rows: Row[] = [{ ...threadRow(context, root, root, { depth: 0, nested: false, chip }), homeGroupLabel }];
|
|
434
469
|
const walk = (parentId: string, depth: number) => {
|
|
435
470
|
for (const id of context.forest.children.get(parentId) ?? []) {
|
|
436
471
|
if (!onPath.has(id)) continue;
|
|
@@ -440,49 +475,54 @@ function needsYouFamilyRows(context: Context, family: Family, homeGroupLabel: st
|
|
|
440
475
|
};
|
|
441
476
|
walk(root.thread.id, 1);
|
|
442
477
|
const left = family.descendants.filter((info) => !info.thread.isHidden && !onPath.has(info.thread.id)).length;
|
|
443
|
-
if (left > 0) rows.push({ type: "left-out", key: `left-out:${root.thread.id}`, scopeId: root.thread.id, count: left, depth: 1
|
|
478
|
+
if (left > 0) rows.push({ type: "left-out", key: `left-out:${root.thread.id}`, scopeId: root.thread.id, count: left, depth: 1 });
|
|
444
479
|
return rows;
|
|
445
480
|
}
|
|
446
481
|
|
|
447
|
-
function
|
|
482
|
+
function buildAttention(
|
|
448
483
|
context: Context,
|
|
449
484
|
families: readonly Family[],
|
|
450
485
|
homeGroupOf: (family: Family) => GroupDescriptor,
|
|
451
|
-
):
|
|
486
|
+
): AttentionView | null {
|
|
452
487
|
if (families.length === 0) return null;
|
|
453
488
|
// Most urgent first, then the chosen field in its natural direction: the
|
|
454
|
-
// sort direction orders the groups only.
|
|
489
|
+
// sort direction orders the groups only. The held family sorts as it was
|
|
490
|
+
// when opened, so nothing inside it moves it.
|
|
455
491
|
const compare = makeComparator({
|
|
456
492
|
field: context.prefs.chronologicalSort,
|
|
457
493
|
direction: "default",
|
|
458
494
|
workingFirst: context.prefs.workingFirst,
|
|
459
495
|
});
|
|
460
|
-
const
|
|
461
|
-
|
|
496
|
+
const slotOf = (family: Family): SectionFamily =>
|
|
497
|
+
context.held !== null && family.root.thread.id === context.heldRootId ? context.held : family;
|
|
498
|
+
const sorted = [...families].sort((familyA, familyB) => {
|
|
499
|
+
const a = slotOf(familyA);
|
|
500
|
+
const b = slotOf(familyB);
|
|
501
|
+
return (
|
|
462
502
|
urgency(a) - urgency(b) ||
|
|
463
503
|
compare(
|
|
464
504
|
{ thread: a.root.thread, familyAttention: a.latestAttentionAt },
|
|
465
505
|
{ thread: b.root.thread, familyAttention: b.latestAttentionAt },
|
|
466
|
-
)
|
|
467
|
-
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
});
|
|
468
509
|
const homeGroupIds: Record<string, string> = {};
|
|
469
510
|
const rows = sorted.flatMap((family) => {
|
|
470
511
|
const home = homeGroupOf(family);
|
|
471
|
-
const familyRows =
|
|
512
|
+
const familyRows = attentionFamilyRows(context, family, home.label);
|
|
472
513
|
for (const row of familyRows) if (row.type === "thread") homeGroupIds[row.info.thread.id] = home.id;
|
|
473
514
|
return familyRows;
|
|
474
515
|
});
|
|
475
|
-
const rails = railsFor(rows.map((row) => row.depth));
|
|
476
516
|
return {
|
|
477
|
-
familyCount: sorted.length,
|
|
478
|
-
rows
|
|
517
|
+
familyCount: sorted.filter((family) => !isAttended(family, context.heldRootId)).length,
|
|
518
|
+
rows,
|
|
479
519
|
homeGroupIds,
|
|
480
520
|
};
|
|
481
521
|
}
|
|
482
522
|
|
|
483
523
|
/**
|
|
484
524
|
* One group. `families` is every family bucketed in it, for its counters and
|
|
485
|
-
* `rootIds`; `drawn` leaves out those in Needs
|
|
525
|
+
* `rootIds`; `drawn` leaves out those in Needs attention.
|
|
486
526
|
*/
|
|
487
527
|
function buildGroup(
|
|
488
528
|
context: Context,
|
|
@@ -544,15 +584,12 @@ function buildGroup(
|
|
|
544
584
|
count: foldedFamilies.length,
|
|
545
585
|
expanded: opened,
|
|
546
586
|
depth: 0,
|
|
547
|
-
rails: [],
|
|
548
587
|
};
|
|
549
588
|
}
|
|
550
589
|
}
|
|
551
590
|
|
|
552
591
|
const units = visible.map((family) => familyUnit(context, family));
|
|
553
|
-
const
|
|
554
|
-
const rails = railsFor(clustered.map((row) => row.depth));
|
|
555
|
-
const rows = clustered.map((row, index) => ({ ...row, rails: rails[index]! }));
|
|
592
|
+
const rows = clusterEnvironments(context, units, 0);
|
|
556
593
|
if (older !== null) rows.push(older);
|
|
557
594
|
return {
|
|
558
595
|
descriptor,
|
|
@@ -581,6 +618,7 @@ export function buildListView(inputs: ViewInputs): ListView {
|
|
|
581
618
|
}
|
|
582
619
|
const context: Context = {
|
|
583
620
|
...inputs,
|
|
621
|
+
heldRootId: inputs.held?.root.thread.id ?? null,
|
|
584
622
|
compare: makeComparator({
|
|
585
623
|
field: prefs.chronologicalSort,
|
|
586
624
|
direction: prefs.sortDirection,
|
|
@@ -594,6 +632,8 @@ export function buildListView(inputs: ViewInputs): ListView {
|
|
|
594
632
|
activePath,
|
|
595
633
|
projectNames: new Map(inputs.projects.map((project) => [project.id, project.name])),
|
|
596
634
|
sectionNames: new Map(inputs.sections.map((section) => [section.id, section.name])),
|
|
635
|
+
drawnInAttention: false,
|
|
636
|
+
attended: false,
|
|
597
637
|
};
|
|
598
638
|
|
|
599
639
|
const byGroup = new Map<string, Family[]>();
|
|
@@ -609,7 +649,7 @@ export function buildListView(inputs: ViewInputs): ListView {
|
|
|
609
649
|
list.push(family);
|
|
610
650
|
byGroup.set(id, list);
|
|
611
651
|
homeOf.set(family, id);
|
|
612
|
-
if (
|
|
652
|
+
if (inAttention(family, context.heldRootId, openRootId)) section.push(family);
|
|
613
653
|
}
|
|
614
654
|
const inSection = new Set(section);
|
|
615
655
|
|
|
@@ -637,7 +677,7 @@ export function buildListView(inputs: ViewInputs): ListView {
|
|
|
637
677
|
const families = byGroup.get(id) ?? [];
|
|
638
678
|
const drawn = families.filter((family) => !inSection.has(family));
|
|
639
679
|
// Pinned and the loose bucket appear only when they hold threads outside
|
|
640
|
-
// Needs
|
|
680
|
+
// Needs attention, except the loose bucket in custom-section mode, as in bb.
|
|
641
681
|
if (drawn.length === 0) {
|
|
642
682
|
if (id === PINNED_GROUP_ID) continue;
|
|
643
683
|
if (id === THREADS_GROUP_ID && prefs.organizationMode !== "chronological") continue;
|
|
@@ -655,8 +695,8 @@ export function buildListView(inputs: ViewInputs): ListView {
|
|
|
655
695
|
const hosts = new Set<string>();
|
|
656
696
|
for (const thread of inputs.threads) if (thread.host !== null) hosts.add(thread.host.id);
|
|
657
697
|
// Every root's group id has a descriptor: entity groups include unknown sections and every host.
|
|
658
|
-
const
|
|
659
|
-
return {
|
|
698
|
+
const attention = buildAttention(context, section, (family) => descriptors.get(homeOf.get(family)!)!);
|
|
699
|
+
return { attention, groups, more, moreCounters, order, multiHost: hosts.size > 1 };
|
|
660
700
|
}
|
|
661
701
|
|
|
662
702
|
/** Every thread row in visual order, for keyboard and windowing. */
|
|
@@ -8,6 +8,7 @@ import { defaultPreferences, type Preferences } from "@/shared/preferences";
|
|
|
8
8
|
import { buildForest, type Forest } from "../model/families";
|
|
9
9
|
import { buildListView, type ListView, type Row } from "../model/view";
|
|
10
10
|
import type { Targets } from "../model/expansion";
|
|
11
|
+
import type { SectionFamily } from "../model/attention";
|
|
11
12
|
|
|
12
13
|
export const T0 = 1_780_000_000_000;
|
|
13
14
|
|
|
@@ -108,7 +109,10 @@ export interface Scenario {
|
|
|
108
109
|
sections?: PluginSidebarSection[];
|
|
109
110
|
prefs?: Partial<Preferences>;
|
|
110
111
|
activeThreadId?: string | null;
|
|
112
|
+
/** The root of the held family, held as it stands now. */
|
|
111
113
|
heldRootId?: string | null;
|
|
114
|
+
/** The held family as it was when opened; wins over `heldRootId`. */
|
|
115
|
+
held?: SectionFamily | null;
|
|
112
116
|
targets?: Targets;
|
|
113
117
|
finishedAt?: Record<string, number>;
|
|
114
118
|
seenAt?: Record<string, number>;
|
|
@@ -141,7 +145,7 @@ export function viewOf(scenario: Scenario): ListView {
|
|
|
141
145
|
sections: scenario.sections ?? [],
|
|
142
146
|
prefs: { ...defaultPreferences(), ...scenario.prefs },
|
|
143
147
|
activeThreadId: scenario.activeThreadId ?? null,
|
|
144
|
-
heldRootId: scenario.heldRootId ?? null,
|
|
148
|
+
held: scenario.held ?? (scenario.heldRootId == null ? null : (forest.familyOf.get(scenario.heldRootId) ?? null)),
|
|
145
149
|
targets: scenario.targets ?? new Map(),
|
|
146
150
|
});
|
|
147
151
|
}
|
|
@@ -165,7 +169,7 @@ export function rowIds(view: ListView, groupId: string): string[] {
|
|
|
165
169
|
return idsOf(group.rows);
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
/** Thread ids in Needs
|
|
169
|
-
export function
|
|
170
|
-
return idsOf(view.
|
|
172
|
+
/** Thread ids in Needs attention, in order; "+N more" lines as `+N`. Empty when the section is absent. */
|
|
173
|
+
export function attentionIds(view: ListView): string[] {
|
|
174
|
+
return idsOf(view.attention?.rows ?? []);
|
|
171
175
|
}
|
package/package.json
CHANGED
package/server/cli.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function createCli(store: PreferenceStore): PluginCliRegistration {
|
|
|
38
38
|
name: "thread-glance",
|
|
39
39
|
summary: "Inspect and change the Thread Glance sidebar's layout preferences",
|
|
40
40
|
description:
|
|
41
|
-
"Organization, sort, group order, hidden and collapsed groups, child-thread folding and which child threads Needs
|
|
41
|
+
"Organization, sort, group order, hidden and collapsed groups, child-thread folding and which child threads Needs attention counts, for the Thread Glance sidebar. Values are JSON; a bare word is read as a string.",
|
|
42
42
|
commands: {
|
|
43
43
|
"prefs list": cliCommand({
|
|
44
44
|
summary: "List every preference and its current value",
|
package/shared/preferences.ts
CHANGED
|
@@ -131,7 +131,7 @@ export const PREFERENCES = {
|
|
|
131
131
|
childAttention: define(
|
|
132
132
|
childAttentionSchema,
|
|
133
133
|
"blocked" as ChildAttention,
|
|
134
|
-
"Needs
|
|
134
|
+
"Needs attention counts every child: blocked counts a child thread that waits on you, is offline or has an orphaned failure; everything also counts every failed or finished-unread child. The same children stay out of a family's older fold, as running ones do.",
|
|
135
135
|
),
|
|
136
136
|
harnessIcon: define(
|
|
137
137
|
harnessIconSchema,
|
|
@@ -212,7 +212,7 @@ export function mapBbPreferences(raw: unknown): Partial<Preferences> {
|
|
|
212
212
|
return mapped as Partial<Preferences>;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
/** Per-client preferences, kept in localStorage only. A saved `filter
|
|
215
|
+
/** Per-client preferences, kept in localStorage only. A saved `filter`, from the old All / Needs attention control, is dropped. */
|
|
216
216
|
export const clientPreferencesSchema = z.object({
|
|
217
217
|
density: z.enum(["compact", "comfortable"]).catch("compact"),
|
|
218
218
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thread-glance
|
|
3
|
-
description: "Reads and changes the Thread Glance sidebar's layout preferences with `bb thread-glance prefs`: grouping, sort, group order, hidden and collapsed groups, child-thread folding, which child threads Needs
|
|
3
|
+
description: "Reads and changes the Thread Glance sidebar's layout preferences with `bb thread-glance prefs`: grouping, sort, group order, hidden and collapsed groups, child-thread folding, which child threads Needs attention counts, harness icons. Use when asked to change how the Thread Glance sidebar lists, groups, sorts, hides, collapses or folds threads or draws their harness icon."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Thread Glance preferences
|
|
@@ -57,12 +57,12 @@ and `bb environment list`; a thread id is a `thr_…` id.
|
|
|
57
57
|
| `expandedOlder` | Group ids and parent thread ids whose "N older" or "N more child threads" row is open | `[]` |
|
|
58
58
|
| `expandedChildren` | Parent thread ids whose chip is open | `[]` |
|
|
59
59
|
| `showPullRequests` | `true` shows a pull request badge on rows | `true` |
|
|
60
|
-
| `childAttention` | Needs
|
|
60
|
+
| `childAttention` | Needs attention counts every child. Which child threads need attention (the Needs attention section, counters, auto-reveal) and stay out of a family's "N more child threads" fold alongside running ones: `blocked` counts a child that waits on you, is offline, or has an orphaned failure (its parent thread idle since); `everything` also counts every failed or unread child | `blocked` |
|
|
61
61
|
| `harnessIcon` | How rows draw the harness logo: `muted` (monochrome), `colour` (the provider's tint) or `hidden` | `muted` |
|
|
62
62
|
|
|
63
63
|
Row density is kept per browser, and the CLI cannot read or change it. No
|
|
64
|
-
preference filters the list: families that need
|
|
65
|
-
Needs
|
|
64
|
+
preference filters the list: families that need attention always move into the
|
|
65
|
+
Needs attention section.
|
|
66
66
|
|
|
67
67
|
Sections themselves and the section a thread is in are bb core state: use
|
|
68
68
|
`bb thread section` and `bb thread update`.
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// The guide rails of a nested row: one hairline per nesting level,
|
|
2
|
-
// drawn inside the row so they join up from row to row.
|
|
3
|
-
import { railLeft, type Rail } from "../model/layout";
|
|
4
|
-
|
|
5
|
-
// `start` begins just under the 16px status slot, which is centred in the row.
|
|
6
|
-
const SPAN: Record<Rail, { top: string; bottom: string }> = {
|
|
7
|
-
start: { top: "calc(50% + 10px)", bottom: "0px" },
|
|
8
|
-
full: { top: "0px", bottom: "0px" },
|
|
9
|
-
end: { top: "0px", bottom: "50%" },
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function RowRails({ rails }: { rails: readonly (Rail | null)[] }) {
|
|
13
|
-
if (rails.every((rail) => rail === null)) return null;
|
|
14
|
-
return (
|
|
15
|
-
<span aria-hidden data-rails="" className="pointer-events-none absolute inset-0">
|
|
16
|
-
{rails.map((rail, level) =>
|
|
17
|
-
rail === null ? null : (
|
|
18
|
-
<span
|
|
19
|
-
key={level}
|
|
20
|
-
className="absolute w-px bg-border"
|
|
21
|
-
style={{ left: railLeft(level), ...SPAN[rail] }}
|
|
22
|
-
/>
|
|
23
|
-
),
|
|
24
|
-
)}
|
|
25
|
-
</span>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Which family Needs you holds, and which family is open, while one of its threads is open. Diffed
|
|
2
|
-
// render to render, as auto-expansion is; nothing here is persisted.
|
|
3
|
-
import { useRef } from "react";
|
|
4
|
-
import type { Forest } from "../model/families";
|
|
5
|
-
import { holdNeedsYou, NO_HOLD, type NeedsYouHold } from "../model/needs-you";
|
|
6
|
-
|
|
7
|
-
export function useNeedsYouHold(forest: Forest | null, activeThreadId: string | null): string | null {
|
|
8
|
-
const held = useRef<NeedsYouHold>(NO_HOLD);
|
|
9
|
-
const lastForest = useRef<Forest | null>(null);
|
|
10
|
-
const lastActive = useRef<string | null | undefined>(undefined);
|
|
11
|
-
// Derived during render, so the family never leaves for a frame. Idempotent:
|
|
12
|
-
// a repeated render with the same inputs keeps what it stored.
|
|
13
|
-
if (forest !== null && (forest !== lastForest.current || activeThreadId !== lastActive.current)) {
|
|
14
|
-
held.current = holdNeedsYou(held.current, activeThreadId === null ? undefined : forest.familyOf.get(activeThreadId));
|
|
15
|
-
lastForest.current = forest;
|
|
16
|
-
lastActive.current = activeThreadId;
|
|
17
|
-
}
|
|
18
|
-
return held.current.heldRootId;
|
|
19
|
-
}
|