@gperezmz/bb-plugin-thread-glance 0.2.4 → 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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/app.tsx +3 -11
  3. package/dist/app.css +1 -1
  4. package/dist/app.js +31 -31
  5. package/dist/app.meta.json +1 -1
  6. package/dist/server.js +10 -10
  7. package/dist/server.js.map +3 -3
  8. package/dist/server.meta.json +1 -1
  9. package/features/thread-list/components/FoldRows.tsx +21 -30
  10. package/features/thread-list/components/GroupSection.tsx +7 -45
  11. package/features/thread-list/components/ListHeader.tsx +79 -0
  12. package/features/thread-list/components/ProviderBadge.tsx +0 -1
  13. package/features/thread-list/components/PullRequestProbes.tsx +27 -0
  14. package/features/thread-list/components/SettingsPanel.tsx +20 -34
  15. package/features/thread-list/components/ThreadDetails.tsx +3 -3
  16. package/features/thread-list/components/ThreadList.tsx +117 -83
  17. package/features/thread-list/components/ThreadRowView.tsx +70 -78
  18. package/features/thread-list/components/controller.ts +5 -10
  19. package/features/thread-list/components/glyphs.tsx +26 -13
  20. package/features/thread-list/data/useAutoExpand.ts +1 -1
  21. package/features/thread-list/data/useDefaultBranches.ts +35 -0
  22. package/features/thread-list/data/usePullRequestAnswers.ts +36 -0
  23. package/features/thread-list/data/useSystemFacts.ts +52 -0
  24. package/features/thread-list/icons.ts +1 -0
  25. package/features/thread-list/index.ts +0 -1
  26. package/features/thread-list/model/attention.ts +2 -66
  27. package/features/thread-list/model/branches.ts +27 -0
  28. package/features/thread-list/model/counters.ts +5 -5
  29. package/features/thread-list/model/details.ts +6 -6
  30. package/features/thread-list/model/expansion.ts +5 -5
  31. package/features/thread-list/model/groups.ts +2 -2
  32. package/features/thread-list/model/labels.ts +28 -17
  33. package/features/thread-list/model/menu.ts +1 -0
  34. package/features/thread-list/model/move.ts +1 -1
  35. package/features/thread-list/model/notes.ts +2 -28
  36. package/features/thread-list/model/settings.ts +1 -17
  37. package/features/thread-list/model/settled.ts +122 -0
  38. package/features/thread-list/model/share.ts +1 -7
  39. package/features/thread-list/model/sort.ts +5 -16
  40. package/features/thread-list/model/state.ts +19 -6
  41. package/features/thread-list/model/toggles.ts +35 -24
  42. package/features/thread-list/model/{families.ts → trees.ts} +40 -28
  43. package/features/thread-list/model/view.ts +200 -274
  44. package/features/thread-list/testing/fixtures.ts +41 -12
  45. package/package.json +1 -1
  46. package/server/cli.ts +1 -1
  47. package/server/import.ts +36 -6
  48. package/server/preference-store.ts +2 -1
  49. package/server.ts +1 -1
  50. package/shared/preferences.ts +38 -27
  51. package/skills/thread-glance/SKILL.md +11 -12
  52. package/features/thread-list/data/useAttentionHold.ts +0 -19
@@ -5,10 +5,10 @@ import type {
5
5
  PluginSidebarThread,
6
6
  } from "@get-bb/plugin-sdk/app";
7
7
  import { defaultPreferences, type Preferences } from "@/shared/preferences";
8
- import { buildForest, type Forest } from "../model/families";
8
+ import { buildForest, type Forest } from "../model/trees";
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
+ import { pullRequestFact, type PullRequestState, type SettleInputs } from "../model/settled";
12
12
 
13
13
  export const T0 = 1_780_000_000_000;
14
14
 
@@ -109,10 +109,6 @@ export interface Scenario {
109
109
  sections?: PluginSidebarSection[];
110
110
  prefs?: Partial<Preferences>;
111
111
  activeThreadId?: string | null;
112
- /** The root of the held family, held as it stands now. */
113
- heldRootId?: string | null;
114
- /** The held family as it was when opened; wins over `heldRootId`. */
115
- held?: SectionFamily | null;
116
112
  targets?: Targets;
117
113
  finishedAt?: Record<string, number>;
118
114
  seenAt?: Record<string, number>;
@@ -120,6 +116,17 @@ export interface Scenario {
120
116
  draftIds?: string[];
121
117
  notes?: Record<string, import("@/shared/contract").ThreadNotes>;
122
118
  now?: number;
119
+ startedAt?: Record<string, number>;
120
+ /** Project id → default branch; "main" for every project when absent. */
121
+ defaultBranches?: Record<string, string | null>;
122
+ /** bb's primary machine; "host_1", the fixtures' own, when absent. */
123
+ primaryHostId?: string | null;
124
+ comfortable?: boolean;
125
+ needYouOnly?: boolean;
126
+ /** bb's default harness; "claude-code", the fixtures' own, when absent. */
127
+ defaultProviderId?: string | null;
128
+ /** Pull request lookups that answered, by thread id. */
129
+ pullRequests?: Record<string, PullRequestState | null>;
123
130
  }
124
131
 
125
132
  export function forestOf(scenario: Scenario): Forest {
@@ -145,19 +152,39 @@ export function viewOf(scenario: Scenario): ListView {
145
152
  sections: scenario.sections ?? [],
146
153
  prefs: { ...defaultPreferences(), ...scenario.prefs },
147
154
  activeThreadId: scenario.activeThreadId ?? null,
148
- held: scenario.held ?? (scenario.heldRootId == null ? null : (forest.familyOf.get(scenario.heldRootId) ?? null)),
149
155
  targets: scenario.targets ?? new Map(),
156
+ settle: settleOf(scenario),
157
+ defaultProviderId: scenario.defaultProviderId === undefined ? "claude-code" : scenario.defaultProviderId,
158
+ primaryHostId: scenario.primaryHostId === undefined ? "host_1" : scenario.primaryHostId,
159
+ comfortable: scenario.comfortable ?? false,
160
+ needYouOnly: scenario.needYouOnly ?? false,
161
+ defaultBranchOf: (thread) =>
162
+ scenario.defaultBranches === undefined ? "main" : scenario.defaultBranches[thread.projectId],
150
163
  });
151
164
  }
152
165
 
166
+ /** The settle inputs a scenario stands for. */
167
+ export function settleOf(scenario: Scenario): SettleInputs {
168
+ const answers = new Map(Object.entries(scenario.pullRequests ?? {}));
169
+ const defaultBranchOf = (projectId: string) =>
170
+ scenario.defaultBranches === undefined ? "main" : scenario.defaultBranches[projectId];
171
+ return {
172
+ now: scenario.now ?? T0 + 60_000,
173
+ settleAfter: scenario.prefs?.settleAfter ?? defaultPreferences().settleAfter,
174
+ startedAt: scenario.startedAt ?? {},
175
+ finishedAt: scenario.finishedAt ?? {},
176
+ pullRequestOf: (thread) => pullRequestFact(thread, defaultBranchOf(thread.projectId), answers),
177
+ };
178
+ }
179
+
153
180
  function idsOf(rows: readonly Row[]): string[] {
154
181
  return rows.map((row) =>
155
182
  row.type === "thread"
156
183
  ? row.info.thread.id
157
184
  : row.type === "older"
158
185
  ? `older:${row.count}`
159
- : row.type === "left-out"
160
- ? `+${row.count}`
186
+ : row.type === "settled"
187
+ ? `settled:${row.count}`
161
188
  : `env:${row.environmentId}`,
162
189
  );
163
190
  }
@@ -169,7 +196,9 @@ export function rowIds(view: ListView, groupId: string): string[] {
169
196
  return idsOf(group.rows);
170
197
  }
171
198
 
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 ?? []);
199
+ /** Roots of the trees that need attention, in the forest's order. */
200
+ export function attentionRootIds(scenario: Scenario): string[] {
201
+ return forestOf(scenario)
202
+ .trees.filter((tree) => tree.attentionFlags.size > 0)
203
+ .map((tree) => tree.root.thread.id);
175
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gperezmz/bb-plugin-thread-glance",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
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 attention 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, settling 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/server/import.ts CHANGED
@@ -21,8 +21,9 @@ export interface ImportResult {
21
21
  }
22
22
 
23
23
  /**
24
- * Reads bb's preferences from its CLI and returns the parsed JSON, or null
25
- * when the command fails or prints something that is not JSON.
24
+ * Reads bb's preferences from its CLI, asking the server Thread Glance runs
25
+ * in, and returns the parsed JSON, or null when that server is unknown, the
26
+ * command fails or it prints something that is not JSON.
26
27
  */
27
28
  export type BbCliReader = () => Promise<unknown>;
28
29
 
@@ -66,28 +67,57 @@ export function bbCliPath(env: NodeJS.ProcessEnv = process.env): string {
66
67
  return configured ? configured : "bb";
67
68
  }
68
69
 
69
- /** Runs `bb thread-list prefs list --json` with a 5 s timeout. */
70
+ /**
71
+ * Runs `bb thread-list prefs list --json` with a 5 s timeout against the bb
72
+ * server Thread Glance runs in.
73
+ *
74
+ * `ownServerUrl` returns that server's URL, which goes to the CLI as
75
+ * `BB_SERVER_URL`: without it the CLI asks the machine's default server, which
76
+ * is another bb when two run on one machine. Where `ownServerUrl` throws or
77
+ * returns nothing, the CLI does not run, so no other server is asked.
78
+ */
70
79
  export function createBbCliReader(
71
80
  log: BbPluginApi["log"],
81
+ ownServerUrl: () => string,
72
82
  env: NodeJS.ProcessEnv = process.env,
73
83
  ): BbCliReader {
74
84
  return () =>
75
85
  new Promise((resolve) => {
86
+ let serverUrl = "";
87
+ let reason = "it has no URL";
88
+ try {
89
+ serverUrl = ownServerUrl().trim();
90
+ } catch (error) {
91
+ reason = error instanceof Error ? error.message : String(error);
92
+ }
93
+ if (!serverUrl) {
94
+ log.warn(
95
+ `could not tell which bb server Thread Glance runs in (${reason}); ` +
96
+ "importing nothing from bb's CLI and asking no other server",
97
+ );
98
+ resolve(null);
99
+ return;
100
+ }
76
101
  const file = bbCliPath(env);
77
102
  execFile(
78
103
  file,
79
104
  ["thread-list", "prefs", "list", "--json"],
80
- { timeout: CLI_TIMEOUT_MS, maxBuffer: CLI_MAX_BUFFER_BYTES, encoding: "utf8" },
105
+ {
106
+ env: { ...env, BB_SERVER_URL: serverUrl },
107
+ timeout: CLI_TIMEOUT_MS,
108
+ maxBuffer: CLI_MAX_BUFFER_BYTES,
109
+ encoding: "utf8",
110
+ },
81
111
  (error, stdout) => {
82
112
  if (error) {
83
- log.warn(`could not run ${file} thread-list prefs list: ${error.message}`);
113
+ log.warn(`could not run ${file} thread-list prefs list against ${serverUrl}: ${error.message}`);
84
114
  resolve(null);
85
115
  return;
86
116
  }
87
117
  try {
88
118
  resolve(JSON.parse(stdout) as unknown);
89
119
  } catch {
90
- log.warn(`${file} thread-list prefs list printed output that is not JSON`);
120
+ log.warn(`${file} thread-list prefs list against ${serverUrl} printed output that is not JSON`);
91
121
  resolve(null);
92
122
  }
93
123
  },
@@ -6,6 +6,7 @@ import {
6
6
  PREFERENCE_KEYS,
7
7
  defaultPreferences,
8
8
  parsePreference,
9
+ parseStoredPreference,
9
10
  preferenceDefault,
10
11
  type PreferenceKey,
11
12
  type Preferences,
@@ -59,7 +60,7 @@ export function createPreferenceStore(
59
60
  async function read<K extends PreferenceKey>(key: K): Promise<Preferences[K]> {
60
61
  const stored = await kv.get<unknown>(preferenceKvKey(key));
61
62
  if (stored === undefined) return preferenceDefault(key);
62
- const parsed = parsePreference(key, stored);
63
+ const parsed = parseStoredPreference(key, stored);
63
64
  if (parsed.success) return parsed.value;
64
65
  bb.log.warn(`stored preference ${key} is invalid (${parsed.message}); using the default`);
65
66
  return preferenceDefault(key);
package/server.ts CHANGED
@@ -25,7 +25,7 @@ export default function threadGlance(bb: BbPluginApi): void {
25
25
  const scheduled = createScheduledTracker({
26
26
  publish: (signal) => bb.realtime.publish(CHANNELS.scheduled, signal),
27
27
  });
28
- const readBbCli = createBbCliReader(bb.log);
28
+ const readBbCli = createBbCliReader(bb.log, () => bb.server.loopbackBaseUrl);
29
29
  const importOnce = createSerialQueue();
30
30
 
31
31
  bb.rpc.register(rpcContract, {
@@ -9,8 +9,8 @@ const idListSchema = z.array(idSchema).max(MAX_ITEMS);
9
9
  export const organizationModeSchema = z.enum(["project", "chronological", "machine"]);
10
10
  export const sortFieldSchema = z.enum(["updated", "created", "alpha", "none"]);
11
11
  export const sortDirectionSchema = z.enum(["default", "ascending", "descending"]);
12
- export const lifecycleSchema = z.enum(["active", "archived"]);
13
- export const harnessIconSchema = z.enum(["muted", "colour", "hidden"]);
12
+ export const harnessIconSchema = z.enum(["muted", "colour"]);
13
+ export const settleAfterSchema = z.enum(["12h", "1d", "3d", "1w", "never"]);
14
14
  export const childAttentionSchema = z.enum(["blocked", "everything"]);
15
15
 
16
16
  const hiddenGroupsSchema = z
@@ -21,34 +21,33 @@ const hiddenGroupsSchema = z
21
21
  export type OrganizationMode = z.infer<typeof organizationModeSchema>;
22
22
  export type SortField = z.infer<typeof sortFieldSchema>;
23
23
  export type SortDirection = z.infer<typeof sortDirectionSchema>;
24
- export type Lifecycle = z.infer<typeof lifecycleSchema>;
25
24
  export type HarnessIcon = z.infer<typeof harnessIconSchema>;
26
25
  export type ChildAttention = z.infer<typeof childAttentionSchema>;
26
+ export type SettleAfter = z.infer<typeof settleAfterSchema>;
27
27
 
28
28
  interface PreferenceDefinition<T> {
29
29
  schema: z.ZodType<T, unknown>;
30
30
  defaultValue: T;
31
31
  description: string;
32
+ /** Maps a value an earlier version stored, which the schema now refuses, onto a current one. */
33
+ readStored?: (stored: unknown) => unknown;
32
34
  }
33
35
 
34
36
  function define<T>(
35
37
  schema: z.ZodType<T, unknown>,
36
38
  defaultValue: T,
37
39
  description: string,
40
+ readStored?: (stored: unknown) => unknown,
38
41
  ): PreferenceDefinition<T> {
39
- return { schema, defaultValue, description };
42
+ return { schema, defaultValue, description, ...(readStored ? { readStored } : {}) };
40
43
  }
41
44
 
42
45
  /** Server-side preferences, shared across windows and devices. */
43
46
  export const PREFERENCES = {
44
- threadLifecycles: define(
45
- z
46
- .array(lifecycleSchema)
47
- .min(1)
48
- .max(2)
49
- .refine((values) => new Set(values).size === values.length, "Lifecycles must be unique"),
50
- ["active"] as Lifecycle[],
51
- "Threads shown: [\"active\"], [\"archived\"] or both. At least one is required.",
47
+ showArchived: define(
48
+ z.boolean(),
49
+ false,
50
+ "Show archived threads: whether every group also lists archived threads, as each group's menu toggles.",
52
51
  ),
53
52
  organizationMode: define(
54
53
  organizationModeSchema,
@@ -107,36 +106,36 @@ export const PREFERENCES = {
107
106
  [] as string[],
108
107
  "Environment ids whose folder row is collapsed.",
109
108
  ),
110
- foldOlder: define(
111
- z.boolean(),
112
- true,
113
- "Collapse older threads: whether each group's quiet roots past its 5 newest fold behind an N older row.",
114
- ),
115
- workingFirst: define(
116
- z.boolean(),
117
- false,
118
- "Working threads first: whether working threads sort first under Updated, as bb's list does.",
119
- ),
120
109
  expandedOlder: define(
121
110
  idListSchema,
122
111
  [] as string[],
123
- "Group ids and parent thread ids whose N older fold the user opened.",
112
+ "Parent thread ids whose \"N more child threads\" row the user opened, once that tree's children chip was open.",
113
+ ),
114
+ openSettledFolds: define(
115
+ idListSchema,
116
+ [] as string[],
117
+ "Group ids whose settled fold the user opened.",
118
+ ),
119
+ settleAfter: define(
120
+ settleAfterSchema,
121
+ "1d" as SettleAfter,
122
+ "Settle after: how long a quiet thread goes without activity before it settles into its group's settled fold: 12h, 1d, 3d, 1w or never.",
124
123
  ),
125
124
  expandedChildren: define(
126
125
  idListSchema,
127
126
  [] as string[],
128
127
  "Parent thread ids whose chip the user opened.",
129
128
  ),
130
- showPullRequests: define(z.boolean(), true, "Whether rows show a pull request badge."),
131
129
  childAttention: define(
132
130
  childAttentionSchema,
133
131
  "blocked" as ChildAttention,
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.",
132
+ "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 an open tree's N more child threads fold, as running ones do.",
135
133
  ),
136
134
  harnessIcon: define(
137
135
  harnessIconSchema,
138
136
  "muted" as HarnessIcon,
139
- "How rows draw the harness logo: muted (monochrome), colour (the provider's tint) or hidden.",
137
+ "How rows draw the harness logo, where a row draws one: muted (monochrome) or colour (the provider's tint). A stored hidden reads as muted.",
138
+ (stored) => (stored === "hidden" ? "muted" : stored),
140
139
  ),
141
140
  } as const;
142
141
 
@@ -176,6 +175,18 @@ export function parsePreference<K extends PreferenceKey>(
176
175
  return { success: false, message: result.error.issues.map((issue) => issue.message).join("; ") };
177
176
  }
178
177
 
178
+ /**
179
+ * Parses a value read back from storage: what `parsePreference` accepts, and
180
+ * the values earlier versions stored that the key maps onto current ones.
181
+ */
182
+ export function parseStoredPreference<K extends PreferenceKey>(
183
+ key: K,
184
+ stored: unknown,
185
+ ): ParseResult<Preferences[K]> {
186
+ const definition: PreferenceDefinition<unknown> = PREFERENCES[key];
187
+ return parsePreference(key, definition.readStored ? definition.readStored(stored) : stored);
188
+ }
189
+
179
190
  /**
180
191
  * Reads a whole preference object, falling back to the default for each key
181
192
  * that is missing or invalid. Used for the localStorage mirror.
@@ -186,7 +197,7 @@ export function coercePreferences(raw: unknown): Preferences {
186
197
  const record = raw as Record<string, unknown>;
187
198
  for (const key of PREFERENCE_KEYS) {
188
199
  if (!(key in record)) continue;
189
- const parsed = parsePreference(key, record[key]);
200
+ const parsed = parseStoredPreference(key, record[key]);
190
201
  if (parsed.success) (result as Record<string, unknown>)[key] = parsed.value;
191
202
  }
192
203
  return result;
@@ -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 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."
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, when threads settle, archived threads, which child threads Needs attention counts, harness icons. Use when asked to change how the Thread Glance sidebar lists, groups, sorts, hides, collapses, folds or settles threads or draws their harness icon."
4
4
  ---
5
5
 
6
6
  # Thread Glance preferences
@@ -20,7 +20,7 @@ bb thread-glance prefs reset <key> [--json]
20
20
 
21
21
  ```sh
22
22
  bb thread-glance prefs set sortDirection ascending
23
- bb thread-glance prefs set threadLifecycles '["active","archived"]'
23
+ bb thread-glance prefs set settleAfter 3d
24
24
  bb thread-glance prefs set hiddenGroups '["threads","project:<project-id>"]'
25
25
  ```
26
26
 
@@ -38,7 +38,7 @@ and `bb environment list`; a thread id is a `thr_…` id.
38
38
 
39
39
  | Key | Values | Default |
40
40
  | --- | --- | --- |
41
- | `threadLifecycles` | `active`, `archived`, or both; at least one | `["active"]` |
41
+ | `showArchived` | `true` lists archived threads in every group | `false` |
42
42
  | `organizationMode` | `project`, `chronological` (custom sections) or `machine` | `project` |
43
43
  | `environmentGrouping` | `true` folds sibling threads sharing a worktree into a folder row | `false` |
44
44
  | `chronologicalSort` | `updated`, `created` or `alpha`; `none` reads as `updated` | `updated` |
@@ -52,17 +52,16 @@ and `bb environment list`; a thread id is a `thr_…` id.
52
52
  | `collapsedThreadSections` | Custom section keys, `section:<id>` | `[]` |
53
53
  | `collapsedMachines` | Machine ids | `[]` |
54
54
  | `collapsedEnvironments` | Environment ids of collapsed folder rows | `[]` |
55
- | `foldOlder` | Collapse older threads: `true` folds a group's quiet top-level threads past its 5 newest behind an "N older" row | `true` |
56
- | `workingFirst` | `true` sorts working threads first under `updated` | `false` |
57
- | `expandedOlder` | Group ids and parent thread ids whose "N older" or "N more child threads" row is open | `[]` |
55
+ | `settleAfter` | Settle after: how long a quiet thread goes without activity before its tree can settle into the group's "Settled (N)" fold: `12h`, `1d`, `3d`, `1w` or `never` (then only a merged or closed pull request settles it) | `1d` |
56
+ | `openSettledFolds` | Group ids whose settled fold is open | `[]` |
57
+ | `expandedOlder` | Parent thread ids whose "N more child threads" row is open, once that tree's children chip is open | `[]` |
58
58
  | `expandedChildren` | Parent thread ids whose chip is open | `[]` |
59
- | `showPullRequests` | `true` shows a pull request badge on rows | `true` |
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
- | `harnessIcon` | How rows draw the harness logo: `muted` (monochrome), `colour` (the provider's tint) or `hidden` | `muted` |
59
+ | `childAttention` | Needs attention counts every child. Which child threads need attention (collapsed groups, the need-you filter, counters, auto-reveal) and stay out of a tree'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
+ | `harnessIcon` | How rows that draw a harness logo draw it: `muted` (monochrome) or `colour` (the provider's tint). A row draws one only where its harness differs from bb's default (a root) or its parent thread's (a child) | `muted` |
62
61
 
63
- Row density is kept per browser, and the CLI cannot read or change it. No
64
- preference filters the list: families that need attention always move into the
65
- Needs attention section.
62
+ Row density and the need-you filter are kept per browser window, and the CLI
63
+ cannot read or change them. A tree that needs attention always stays in its
64
+ group, drawn even when the group is collapsed.
66
65
 
67
66
  Sections themselves and the section a thread is in are bb core state: use
68
67
  `bb thread section` and `bb thread update`.
@@ -1,19 +0,0 @@
1
- // Which family Needs attention holds, as it was when opened, and which family is open, while one of its
2
- // threads is open. Diffed 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 { holdAttention, NO_HOLD, type AttentionHold, type SectionFamily } from "../model/attention";
6
-
7
- export function useAttentionHold(forest: Forest | null, activeThreadId: string | null): SectionFamily | null {
8
- const hold = useRef<AttentionHold>(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
- hold.current = holdAttention(hold.current, forest, activeThreadId);
15
- lastForest.current = forest;
16
- lastActive.current = activeThreadId;
17
- }
18
- return hold.current.held;
19
- }