@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.
Files changed (35) hide show
  1. package/README.md +2 -2
  2. package/dist/app.css +1 -1
  3. package/dist/app.js +29 -29
  4. package/dist/app.meta.json +1 -1
  5. package/dist/server.js +1 -1
  6. package/dist/server.js.map +2 -2
  7. package/dist/server.meta.json +1 -1
  8. package/features/thread-list/components/FoldRows.tsx +12 -11
  9. package/features/thread-list/components/GroupSection.tsx +135 -104
  10. package/features/thread-list/components/ThreadDetails.tsx +2 -3
  11. package/features/thread-list/components/ThreadList.tsx +12 -11
  12. package/features/thread-list/components/ThreadRowView.tsx +23 -19
  13. package/features/thread-list/components/Toolbar.tsx +1 -1
  14. package/features/thread-list/components/controller.ts +2 -0
  15. package/features/thread-list/components/glyphs.tsx +19 -1
  16. package/features/thread-list/data/useAttentionHold.ts +19 -0
  17. package/features/thread-list/model/{needs-you.ts → attention.ts} +48 -28
  18. package/features/thread-list/model/counters.ts +3 -3
  19. package/features/thread-list/model/details.ts +1 -1
  20. package/features/thread-list/model/expansion.ts +10 -10
  21. package/features/thread-list/model/families.ts +26 -22
  22. package/features/thread-list/model/layout.ts +0 -43
  23. package/features/thread-list/model/notes.ts +37 -4
  24. package/features/thread-list/model/settings.ts +1 -1
  25. package/features/thread-list/model/share.ts +3 -3
  26. package/features/thread-list/model/state.ts +1 -1
  27. package/features/thread-list/model/toggles.ts +7 -5
  28. package/features/thread-list/model/view.ts +96 -56
  29. package/features/thread-list/testing/fixtures.ts +8 -4
  30. package/package.json +1 -1
  31. package/server/cli.ts +1 -1
  32. package/shared/preferences.ts +2 -2
  33. package/skills/thread-glance/SKILL.md +4 -4
  34. package/features/thread-list/components/Rails.tsx +0 -27
  35. 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 { inNeedsYou } from "./needs-you";
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 { railsFor, type Rail } from "./layout";
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
- /** Guide rails by nesting level. */
54
- rails: (Rail | null)[];
55
- /** A hidden thread shown because it needs you or failed. */
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 you: its home group's name, drawn where the age goes. */
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 you: how many of its child threads it leaves out ("+N more"). Not a control. */
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 you and behind folds included. */
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 you's rows are drawn under; no group has it. */
119
- export const NEEDS_YOU_GROUP_ID = "needs-you";
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 you section: every family with a thread that needs you, or held there while one of its threads is open. */
122
- export interface NeedsYouView {
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
- needsYou: NeedsYouView | null;
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 root of the family Needs you keeps while one of its threads is open. */
151
- heldRootId: string | null;
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
- rails: [],
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 you or failed. */
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.needsYou.size > 0);
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.needsYou.size > 0);
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.needsYou, ...subtreeOf(context, info.thread.id).flags]);
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.needsYouFlags;
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 you: the path from the root down to each thread
421
- * that needs you or is open, then one "+N more" line for the child threads
422
- * left out. No row draws a chip, since the section never opens one.
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 needsYouFamilyRows(context: Context, family: Family, homeGroupLabel: string): Row[] {
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 needsYou = !info.thread.isArchived && info.needsYou.size > 0;
429
- if (!needsYou && !info.isActive) continue;
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 rows: Row[] = [{ ...threadRow(context, root, root, { depth: 0, nested: false, chip: null }), homeGroupLabel }];
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, rails: [] });
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 buildNeedsYou(
482
+ function buildAttention(
448
483
  context: Context,
449
484
  families: readonly Family[],
450
485
  homeGroupOf: (family: Family) => GroupDescriptor,
451
- ): NeedsYouView | null {
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 sorted = [...families].sort(
461
- (a, b) =>
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 = needsYouFamilyRows(context, family, home.label);
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: rows.map((row, index) => ({ ...row, rails: rails[index]! })),
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 you.
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 clustered = clusterEnvironments(context, units, 0);
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 (inNeedsYou(family, inputs.heldRootId, openRootId)) section.push(family);
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 you, except the loose bucket in custom-section mode, as in bb.
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 needsYou = buildNeedsYou(context, section, (family) => descriptors.get(homeOf.get(family)!)!);
659
- return { needsYou, groups, more, moreCounters, order, multiHost: hosts.size > 1 };
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 you, in order; "+N more" lines as `+N`. Empty when the section is absent. */
169
- export function needsYouIds(view: ListView): string[] {
170
- return idsOf(view.needsYou?.rows ?? []);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gperezmz/bb-plugin-thread-glance",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bb": ">=0.43",
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 you counts, for the Thread Glance sidebar. Values are JSON; a bare word is read as a string.",
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",
@@ -131,7 +131,7 @@ export const PREFERENCES = {
131
131
  childAttention: define(
132
132
  childAttentionSchema,
133
133
  "blocked" as ChildAttention,
134
- "Needs you 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.",
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` from before Needs you is dropped. */
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 you 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."
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 you counts every child. Which child threads need you (the Needs you 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` |
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 you always move into the
65
- Needs you section.
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
- }