@gperezmz/bb-plugin-thread-glance 0.1.3 → 0.2.1

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 (38) hide show
  1. package/README.md +2 -2
  2. package/dist/app.css +1 -1
  3. package/dist/app.js +32 -32
  4. package/dist/app.meta.json +1 -1
  5. package/dist/server.js +8 -8
  6. package/dist/server.js.map +3 -3
  7. package/dist/server.meta.json +1 -1
  8. package/features/thread-list/components/FoldRows.tsx +14 -13
  9. package/features/thread-list/components/GroupSection.tsx +118 -69
  10. package/features/thread-list/components/Rails.tsx +2 -3
  11. package/features/thread-list/components/ThreadList.tsx +21 -16
  12. package/features/thread-list/components/ThreadRowView.tsx +12 -9
  13. package/features/thread-list/components/Toolbar.tsx +129 -187
  14. package/features/thread-list/components/controller.ts +1 -2
  15. package/features/thread-list/components/glyphs.tsx +1 -1
  16. package/features/thread-list/data/useClientPreferences.ts +1 -1
  17. package/features/thread-list/data/useNeedsYouHold.ts +19 -0
  18. package/features/thread-list/model/counters.ts +13 -19
  19. package/features/thread-list/model/details.ts +2 -2
  20. package/features/thread-list/model/expansion.ts +10 -10
  21. package/features/thread-list/model/families.ts +50 -48
  22. package/features/thread-list/model/labels.ts +2 -1
  23. package/features/thread-list/model/layout.ts +6 -11
  24. package/features/thread-list/model/needs-you.ts +133 -0
  25. package/features/thread-list/model/notes.ts +1 -1
  26. package/features/thread-list/model/settings.ts +54 -0
  27. package/features/thread-list/model/share.ts +7 -1
  28. package/features/thread-list/model/sort.ts +14 -6
  29. package/features/thread-list/model/state.ts +11 -11
  30. package/features/thread-list/model/time.ts +1 -1
  31. package/features/thread-list/model/toggles.ts +2 -7
  32. package/features/thread-list/model/view.ts +139 -143
  33. package/features/thread-list/testing/fixtures.ts +19 -10
  34. package/package.json +1 -1
  35. package/server/cli.ts +2 -2
  36. package/shared/preferences.ts +10 -23
  37. package/skills/thread-glance/SKILL.md +9 -10
  38. package/features/thread-list/model/attention.ts +0 -89
@@ -1,29 +1,20 @@
1
- // The top of the scroll area: the filter toggle and the settings
2
- // popover.
3
- import { useState } from "react";
1
+ // The top of the scroll area: the settings button and its popover.
4
2
  import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
5
3
  import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
6
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
7
4
  import { cn } from "@/lib/utils";
8
- import type { ClientPreferences, Lifecycle, Preferences } from "@/shared/preferences";
9
- import { ATTENTION_EXPLANATION, ATTENTION_SUMMARY } from "../model/counters";
5
+ import type { ClientPreferences, Preferences } from "@/shared/preferences";
6
+ import { childAttentionFor, countsEveryChild, lifecyclesFor, sortArrow, sortFieldPatch, threadsShown } from "../model/settings";
7
+ import { effectiveSortField } from "../model/sort";
10
8
  import { ICONS } from "../icons";
9
+ import { TOOLBAR_HEIGHT } from "./GroupSection";
11
10
  import { ROW_ICON_BUTTON } from "./ThreadRowView";
12
11
 
13
- type Page = "organize" | "sort" | "display" | "filter";
14
-
15
- const PAGES: readonly { id: Page; label: string }[] = [
16
- { id: "organize", label: "Organize" },
17
- { id: "sort", label: "Sort" },
18
- { id: "display", label: "Display" },
19
- { id: "filter", label: "Filter" },
20
- ];
21
-
22
12
  // bb's own segmented controls (the Reasoning picker, the diff view toggle)
23
13
  // mark the chosen item with the state-active token and no shadow. The track
24
14
  // is a translucent tint, so the chosen item reads as raised in both themes.
25
- const SEGMENT_TRACK = "flex gap-0.5 rounded-md bg-surface-recessed p-0.5";
26
- const SEGMENT_ITEM = "flex items-center justify-center rounded-sm text-xs outline-none focus-visible:ring-2";
15
+ const SEGMENT_TRACK = "flex min-w-0 flex-1 gap-0.5 rounded-md bg-surface-recessed p-0.5";
16
+ const SEGMENT_ITEM =
17
+ "flex min-w-0 flex-1 items-center justify-center whitespace-nowrap rounded-sm px-1 py-0.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring";
27
18
 
28
19
  function segmentClass(selected: boolean): string {
29
20
  return cn(
@@ -34,36 +25,42 @@ function segmentClass(selected: boolean): string {
34
25
  );
35
26
  }
36
27
 
37
- function Choice<T extends string | boolean>({
28
+ /** A label, then its control, on one line. */
29
+ function Line({ label, children }: { label: string; children: React.ReactNode }) {
30
+ return (
31
+ <div className="flex items-center gap-2 px-2 py-1">
32
+ <span className="w-[5.5rem] shrink-0 text-sm">{label}</span>
33
+ {children}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ function Segmented<T extends string>({
38
39
  label,
39
- value,
40
+ options,
40
41
  current,
41
42
  onSelect,
42
- description,
43
43
  }: {
44
44
  label: string;
45
- value: T;
45
+ options: readonly (readonly [T, string])[];
46
46
  current: T;
47
47
  onSelect(value: T): void;
48
- description?: string;
49
48
  }) {
50
- const checked = value === current;
51
49
  return (
52
- <button
53
- type="button"
54
- role="menuitemradio"
55
- aria-checked={checked}
56
- onClick={() => onSelect(value)}
57
- className="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left text-sm outline-none hover:bg-accent focus-visible:bg-accent"
58
- >
59
- <span className="mt-0.5 inline-flex size-4 shrink-0 items-center justify-center">
60
- {checked ? <Icon name={ICONS.check} aria-hidden className="size-4" /> : null}
61
- </span>
62
- <span className="flex min-w-0 flex-col">
63
- <span>{label}</span>
64
- {description ? <span className="text-xs text-muted-foreground">{description}</span> : null}
65
- </span>
66
- </button>
50
+ <div role="radiogroup" aria-label={label} className={SEGMENT_TRACK}>
51
+ {options.map(([value, text]) => (
52
+ <button
53
+ key={value}
54
+ type="button"
55
+ role="radio"
56
+ aria-checked={value === current}
57
+ onClick={() => onSelect(value)}
58
+ className={segmentClass(value === current)}
59
+ >
60
+ {text}
61
+ </button>
62
+ ))}
63
+ </div>
67
64
  );
68
65
  }
69
66
 
@@ -81,7 +78,7 @@ function Toggle({
81
78
  return (
82
79
  <button
83
80
  type="button"
84
- role="menuitemcheckbox"
81
+ role="checkbox"
85
82
  aria-checked={checked}
86
83
  onClick={() => onChange(!checked)}
87
84
  className="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left text-sm outline-none hover:bg-accent focus-visible:bg-accent"
@@ -104,7 +101,7 @@ function Toggle({
104
101
  }
105
102
 
106
103
  function Heading({ children }: { children: React.ReactNode }) {
107
- return <p className="px-2 pb-0.5 pt-2 text-xs font-medium text-muted-foreground first:pt-0">{children}</p>;
104
+ return <h3 className="px-2 pb-0.5 pt-3 text-xs font-medium text-muted-foreground first:pt-0">{children}</h3>;
108
105
  }
109
106
 
110
107
  export function SettingsPanel({
@@ -118,176 +115,121 @@ export function SettingsPanel({
118
115
  onPrefs(patch: Partial<Preferences>): void;
119
116
  onClient(patch: Partial<ClientPreferences>): void;
120
117
  }) {
121
- const [page, setPage] = useState<Page>("organize");
122
- const sortField = prefs.chronologicalSort === "none" ? "updated" : prefs.chronologicalSort;
123
- const toggleLifecycle = (lifecycle: Lifecycle) => {
124
- const has = prefs.threadLifecycles.includes(lifecycle);
125
- const next = has
126
- ? prefs.threadLifecycles.filter((value) => value !== lifecycle)
127
- : [...prefs.threadLifecycles, lifecycle];
128
- if (next.length > 0) onPrefs({ threadLifecycles: next });
129
- };
118
+ const arrow = sortArrow(prefs);
130
119
  return (
131
- <div className="flex flex-col gap-2">
132
- <div role="tablist" aria-label="Settings pages" className={SEGMENT_TRACK}>
133
- {PAGES.map((candidate) => (
134
- <button
135
- key={candidate.id}
136
- type="button"
137
- role="tab"
138
- aria-selected={page === candidate.id}
139
- onClick={() => setPage(candidate.id)}
140
- className={cn(segmentClass(page === candidate.id), "flex-1 px-1.5 py-1 focus-visible:ring-ring")}
141
- >
142
- {candidate.label}
143
- </button>
144
- ))}
145
- </div>
146
- <div role="tabpanel" aria-label={PAGES.find((candidate) => candidate.id === page)?.label} className="flex flex-col">
147
- {page === "organize" ? (
148
- <>
149
- <Heading>Group by</Heading>
150
- <Choice label="By project" value="project" current={prefs.organizationMode} onSelect={(value) => onPrefs({ organizationMode: value })} />
151
- <Choice label="Custom" description="Your own sections" value="chronological" current={prefs.organizationMode} onSelect={(value) => onPrefs({ organizationMode: value })} />
152
- <Choice label="By machine" value="machine" current={prefs.organizationMode} onSelect={(value) => onPrefs({ organizationMode: value })} />
153
- <Heading>Child threads</Heading>
154
- <Choice label="Folded" description="One flat level behind a chip" value="folded" current={prefs.nesting} onSelect={(value) => onPrefs({ nesting: value })} />
155
- <Choice label="Tree" description="bb's indented tree" value="tree" current={prefs.nesting} onSelect={(value) => onPrefs({ nesting: value })} />
156
- <Heading>Groups</Heading>
157
- <Toggle
158
- label="By environment"
159
- description="Fold threads sharing a worktree into a folder"
160
- checked={prefs.environmentGrouping}
161
- onChange={(value) => onPrefs({ environmentGrouping: value })}
162
- />
163
- </>
164
- ) : null}
165
- {page === "sort" ? (
166
- <>
167
- <Heading>Sort by</Heading>
168
- <Choice label="Updated" value="updated" current={sortField} onSelect={(value) => onPrefs({ chronologicalSort: value })} />
169
- <Choice label="Created" value="created" current={sortField} onSelect={(value) => onPrefs({ chronologicalSort: value })} />
170
- <Choice label="Alphabetical" value="alpha" current={sortField} onSelect={(value) => onPrefs({ chronologicalSort: value })} />
171
- <Heading>Direction</Heading>
172
- <Choice label="Default" value="default" current={prefs.sortDirection} onSelect={(value) => onPrefs({ sortDirection: value })} />
173
- <Choice label="Ascending" value="ascending" current={prefs.sortDirection} onSelect={(value) => onPrefs({ sortDirection: value })} />
174
- <Choice label="Descending" value="descending" current={prefs.sortDirection} onSelect={(value) => onPrefs({ sortDirection: value })} />
175
- <Heading>Order</Heading>
176
- <Toggle
177
- label="Working first"
178
- description="Running threads on top under Updated"
179
- checked={prefs.workingFirst}
180
- onChange={(value) => onPrefs({ workingFirst: value })}
181
- />
182
- <Toggle
183
- label="Fold older threads"
184
- description="Keep the 5 most recent quiet threads per group"
185
- checked={prefs.foldOlder}
186
- onChange={(value) => onPrefs({ foldOlder: value })}
187
- />
188
- </>
189
- ) : null}
190
- {page === "display" ? (
191
- <>
192
- <Heading>Density</Heading>
193
- <Choice label="Compact" description="One line per thread" value="compact" current={client.density} onSelect={(value) => onClient({ density: value })} />
194
- <Choice label="Comfortable" description="Adds branch and machine" value="comfortable" current={client.density} onSelect={(value) => onClient({ density: value })} />
195
- <Heading>Harness icon</Heading>
196
- <Choice label="Muted" description="Monochrome, beside the age" value="muted" current={prefs.harnessIcon} onSelect={(value) => onPrefs({ harnessIcon: value })} />
197
- <Choice label="Colour" description="Each provider's own tint" value="colour" current={prefs.harnessIcon} onSelect={(value) => onPrefs({ harnessIcon: value })} />
198
- <Choice label="Hidden" description="Shown in the hover card only" value="hidden" current={prefs.harnessIcon} onSelect={(value) => onPrefs({ harnessIcon: value })} />
199
- <Heading>Rows</Heading>
200
- <Toggle label="Pull requests" checked={prefs.showPullRequests} onChange={(value) => onPrefs({ showPullRequests: value })} />
201
- </>
202
- ) : null}
203
- {page === "filter" ? (
204
- <>
205
- <Heading>Show</Heading>
206
- <Toggle label="Active" checked={prefs.threadLifecycles.includes("active")} onChange={() => toggleLifecycle("active")} />
207
- <Toggle label="Archived" checked={prefs.threadLifecycles.includes("archived")} onChange={() => toggleLifecycle("archived")} />
208
- <Heading>Child threads in Needs attention</Heading>
209
- <Choice
210
- label="Blocked and orphaned failures"
211
- description="A child that waits on you, is offline, or failed while its manager is idle"
212
- value="blocked"
213
- current={prefs.childAttention}
214
- onSelect={(value) => onPrefs({ childAttention: value })}
215
- />
216
- <Choice
217
- label="Everything"
218
- description="Also every failed or unread child"
219
- value="everything"
220
- current={prefs.childAttention}
221
- onSelect={(value) => onPrefs({ childAttention: value })}
222
- />
223
- </>
224
- ) : null}
225
- </div>
120
+ <div className="flex flex-col">
121
+ <Heading>List</Heading>
122
+ <Line label="Group by">
123
+ <Segmented
124
+ label="Group by"
125
+ options={[
126
+ ["project", "Project"],
127
+ ["chronological", "Custom"],
128
+ ["machine", "Machine"],
129
+ ]}
130
+ current={prefs.organizationMode}
131
+ onSelect={(value) => onPrefs({ organizationMode: value })}
132
+ />
133
+ </Line>
134
+ <Line label="Sort by">
135
+ <Segmented
136
+ label="Sort by"
137
+ options={[
138
+ ["updated", "Updated"],
139
+ ["created", "Created"],
140
+ ["alpha", "A–Z"],
141
+ ]}
142
+ current={effectiveSortField(prefs.chronologicalSort)}
143
+ onSelect={(value) => onPrefs(sortFieldPatch(value))}
144
+ />
145
+ <button
146
+ type="button"
147
+ aria-label={`Sort order: ${arrow.label}. Reverse`}
148
+ title={arrow.label}
149
+ onClick={() => onPrefs(arrow.patch)}
150
+ className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-surface-recessed text-xs outline-none hover:bg-state-hover focus-visible:ring-2 focus-visible:ring-ring"
151
+ >
152
+ {arrow.glyph}
153
+ </button>
154
+ </Line>
155
+ <Toggle label="Working threads first" checked={prefs.workingFirst} onChange={(value) => onPrefs({ workingFirst: value })} />
156
+ <Toggle
157
+ label="Worktrees as folders"
158
+ description="Threads sharing a worktree fold into one row"
159
+ checked={prefs.environmentGrouping}
160
+ onChange={(value) => onPrefs({ environmentGrouping: value })}
161
+ />
162
+ <Toggle label="Collapse older threads" checked={prefs.foldOlder} onChange={(value) => onPrefs({ foldOlder: value })} />
163
+ <Heading>Rows</Heading>
164
+ <Line label="Density">
165
+ <Segmented
166
+ label="Density"
167
+ options={[
168
+ ["compact", "Compact"],
169
+ ["comfortable", "Comfortable"],
170
+ ]}
171
+ current={client.density}
172
+ onSelect={(value) => onClient({ density: value })}
173
+ />
174
+ </Line>
175
+ <Line label="Harness icon">
176
+ <Segmented
177
+ label="Harness icon"
178
+ options={[
179
+ ["muted", "Muted"],
180
+ ["colour", "Colour"],
181
+ ["hidden", "Hidden"],
182
+ ]}
183
+ current={prefs.harnessIcon}
184
+ onSelect={(value) => onPrefs({ harnessIcon: value })}
185
+ />
186
+ </Line>
187
+ <Toggle label="Pull request badge" checked={prefs.showPullRequests} onChange={(value) => onPrefs({ showPullRequests: value })} />
188
+ <Heading>Show</Heading>
189
+ <Line label="Threads">
190
+ <Segmented
191
+ label="Threads"
192
+ options={[
193
+ ["active", "Active"],
194
+ ["archived", "Archived"],
195
+ ["both", "Both"],
196
+ ]}
197
+ current={threadsShown(prefs.threadLifecycles)}
198
+ onSelect={(value) => onPrefs({ threadLifecycles: lifecyclesFor(value) })}
199
+ />
200
+ </Line>
201
+ <Toggle
202
+ label="Needs you counts every child"
203
+ description="Every unread or failed child thread; otherwise only those blocked on you."
204
+ checked={countsEveryChild(prefs.childAttention)}
205
+ onChange={(value) => onPrefs({ childAttention: childAttentionFor(value) })}
206
+ />
226
207
  </div>
227
208
  );
228
209
  }
229
210
 
211
+ /** The slim row above the list: ⚙ alone at its right. */
230
212
  export function Toolbar({
231
213
  prefs,
232
214
  client,
233
- attentionCount,
234
- compact,
235
215
  onPrefs,
236
216
  onClient,
237
217
  }: {
238
218
  prefs: Preferences;
239
219
  client: ClientPreferences;
240
- /** Threads that need attention, for the filter's badge. */
241
- attentionCount: number;
242
- /** Phone width: the filter's long label would truncate. */
243
- compact: boolean;
244
220
  onPrefs(patch: Partial<Preferences>): void;
245
221
  onClient(patch: Partial<ClientPreferences>): void;
246
222
  }) {
247
223
  return (
248
- // Sticky with the group headers, so the filter stays in reach.
249
- <div className="sticky top-0 z-30 flex h-8 items-center gap-1 bg-sidebar pb-1">
250
- <TooltipProvider delayDuration={400}>
251
- <div role="radiogroup" aria-label="Filter threads" className={cn(SEGMENT_TRACK, "min-w-0 flex-1")}>
252
- {(
253
- [
254
- ["all", "All"],
255
- ["attention", "Needs attention"],
256
- ] as const
257
- ).map(([value, label]) => {
258
- const button = (
259
- <button
260
- key={value}
261
- type="button"
262
- role="radio"
263
- aria-checked={client.filter === value}
264
- aria-description={value === "attention" ? ATTENTION_EXPLANATION : undefined}
265
- onClick={() => onClient({ filter: value })}
266
- className={cn(segmentClass(client.filter === value), "min-w-0 flex-1 gap-1 truncate px-1.5 py-0.5 focus-visible:ring-sidebar-ring")}
267
- >
268
- <span className="truncate">{value === "attention" && compact ? "Attention" : label}</span>
269
- {value === "attention" && attentionCount > 0 ? (
270
- <span className="shrink-0 tabular-nums text-muted-foreground">{attentionCount}</span>
271
- ) : null}
272
- </button>
273
- );
274
- if (value !== "attention") return button;
275
- return (
276
- <Tooltip key={value}>
277
- <TooltipTrigger asChild>{button}</TooltipTrigger>
278
- <TooltipContent side="bottom">{ATTENTION_SUMMARY}</TooltipContent>
279
- </Tooltip>
280
- );
281
- })}
282
- </div>
283
- </TooltipProvider>
224
+ // Sticky with the group headers, so the settings stay in reach.
225
+ <div style={{ height: TOOLBAR_HEIGHT }} className="sticky top-0 z-30 flex items-center justify-end bg-sidebar">
284
226
  <Popover>
285
227
  <PopoverTrigger asChild>
286
228
  <button type="button" aria-label="Thread Glance settings" title="List settings" className={ROW_ICON_BUTTON}>
287
229
  <Icon name={ICONS.settings} aria-hidden className="size-4" />
288
230
  </button>
289
231
  </PopoverTrigger>
290
- <PopoverContent align="end" className="w-64 p-2">
232
+ <PopoverContent align="end" className="w-80 p-2">
291
233
  <SettingsPanel prefs={prefs} client={client} onPrefs={onPrefs} onClient={onClient} />
292
234
  </PopoverContent>
293
235
  </Popover>
@@ -4,7 +4,7 @@
4
4
  import { createContext } from "react";
5
5
  import type { PluginSidebarSection, PluginSidebarThread } from "@get-bb/plugin-sdk/app";
6
6
  import type { Stamps } from "@/shared/contract";
7
- import type { HarnessIcon, Nesting, OrganizationMode } from "@/shared/preferences";
7
+ import type { HarnessIcon, OrganizationMode } from "@/shared/preferences";
8
8
  import type { ThreadNotes } from "@/shared/contract";
9
9
  import type { Family } from "../model/families";
10
10
  import type { RowMenuAction } from "../model/menu";
@@ -30,7 +30,6 @@ export interface RowController {
30
30
  provider(providerId: string): ProviderDisplay;
31
31
  sections: readonly PluginSidebarSection[];
32
32
  mode: OrganizationMode;
33
- nesting: Nesting;
34
33
  onNavigate(): void;
35
34
  onToggleChip(row: ThreadRow): void;
36
35
  onToggleOlder(row: OlderRow): void;
@@ -101,7 +101,7 @@ const COUNTER_ITEMS: readonly {
101
101
  flag: Flag;
102
102
  label: (count: number) => string;
103
103
  }[] = [
104
- { key: "needsYou", flag: "needs-you", label: (n) => `${n} need${n === 1 ? "s" : ""} you` },
104
+ { key: "waitsOnYou", flag: "waits-on-you", label: (n) => `${n} need${n === 1 ? "s" : ""} you` },
105
105
  { key: "failed", flag: "unread-failed", label: (n) => `${n} failed` },
106
106
  { key: "offline", flag: "offline", label: (n) => `${n} waiting for an offline machine` },
107
107
  { key: "working", flag: "working", label: (n) => `${n} working` },
@@ -1,4 +1,4 @@
1
- // Per-client preferences: filter and density, localStorage only.
1
+ // Per-client preferences: density, localStorage only.
2
2
  import { useCallback, useState } from "react";
3
3
  import {
4
4
  CLIENT_PREFERENCES_STORAGE_KEY,
@@ -0,0 +1,19 @@
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
+ }
@@ -1,8 +1,8 @@
1
- // Group header counters and the Needs attention filter test. Pure.
1
+ // Group header counters. Pure.
2
2
  import type { Family } from "./families";
3
3
 
4
4
  export interface Counters {
5
- needsYou: number;
5
+ waitsOnYou: number;
6
6
  /** Unread-failed plus queue-failed threads. */
7
7
  failed: number;
8
8
  offline: number;
@@ -10,21 +10,22 @@ export interface Counters {
10
10
  unread: number;
11
11
  }
12
12
 
13
- export const EMPTY_COUNTERS: Counters = { needsYou: 0, failed: 0, offline: 0, working: 0, unread: 0 };
13
+ export const EMPTY_COUNTERS: Counters = { waitsOnYou: 0, failed: 0, offline: 0, working: 0, unread: 0 };
14
14
 
15
15
  /**
16
- * Counts the threads in the families. A thread counts what its attention
17
- * flags say, so a child adds to needs-you, failed, offline and unread only as
18
- * the Child threads in Needs attention setting lets it; working counts every thread that runs. Hidden threads carry
19
- * only needs-you and unread-failed flags already; archived threads never count.
16
+ * Counts the threads in the families. A thread counts what its Needs you
17
+ * flags say, so a child adds to waits-on-you, failed, offline and unread only
18
+ * as "Needs you counts every child" lets it; working counts every thread that
19
+ * runs. Hidden threads carry only waits-on-you and unread-failed flags
20
+ * already; archived threads never count.
20
21
  */
21
22
  export function countFamilies(families: readonly Family[]): Counters {
22
23
  const counters = { ...EMPTY_COUNTERS };
23
24
  for (const family of families) {
24
25
  for (const info of [family.root, ...family.descendants]) {
25
26
  if (info.thread.isArchived) continue;
26
- const flags = info.attention;
27
- if (flags.has("needs-you")) counters.needsYou += 1;
27
+ const flags = info.needsYou;
28
+ if (flags.has("waits-on-you")) counters.waitsOnYou += 1;
28
29
  if (flags.has("unread-failed") || flags.has("queue-failed")) counters.failed += 1;
29
30
  if (flags.has("offline")) counters.offline += 1;
30
31
  if (info.flags.has("working")) counters.working += 1;
@@ -36,7 +37,7 @@ export function countFamilies(families: readonly Family[]): Counters {
36
37
 
37
38
  export function addCounters(a: Counters, b: Counters): Counters {
38
39
  return {
39
- needsYou: a.needsYou + b.needsYou,
40
+ waitsOnYou: a.waitsOnYou + b.waitsOnYou,
40
41
  failed: a.failed + b.failed,
41
42
  offline: a.offline + b.offline,
42
43
  working: a.working + b.working,
@@ -44,21 +45,14 @@ export function addCounters(a: Counters, b: Counters): Counters {
44
45
  };
45
46
  }
46
47
 
47
- /** The Needs attention tooltip: short enough to read at a glance. */
48
- export const ATTENTION_SUMMARY = "Waiting on you, failed, offline, or finished and unread. Child threads count only when stuck.";
49
-
50
- /** What the Needs attention toggle keeps, in full, for its accessible description. */
51
- export const ATTENTION_EXPLANATION =
52
- "Threads only you can move forward: a question, approval or plan review waiting on you; a machine that is offline; a failed thread or queued message; finished threads you haven't opened. For a child thread, only what waits on you or is offline, or a failure whose manager is idle (Filter settings can widen this).";
53
-
54
48
  /**
55
- * Which counters a header draws. needs-you, failed and offline
49
+ * Which counters a header draws. waits-on-you, failed and offline
56
50
  * always; working only while collapsed, since the rows show it otherwise;
57
51
  * unread only on the More trigger, where the rows are out of sight.
58
52
  */
59
53
  export function visibleCounters(counters: Counters, where: { collapsed: boolean; more: boolean }): Counters {
60
54
  return {
61
- needsYou: counters.needsYou,
55
+ waitsOnYou: counters.waitsOnYou,
62
56
  failed: counters.failed,
63
57
  offline: counters.offline,
64
58
  working: where.collapsed || where.more ? counters.working : 0,
@@ -3,7 +3,7 @@ import type { Family, ThreadInfo } from "./families";
3
3
  import type { StateKind } from "./state";
4
4
 
5
5
  const SUMMARY_ORDER: readonly [StateKind, string][] = [
6
- ["needs-you", "needing you"],
6
+ ["waits-on-you", "needing you"],
7
7
  ["failed", "failed"],
8
8
  ["queue-failed", "queue failed"],
9
9
  ["offline", "offline"],
@@ -61,7 +61,7 @@ export function sinceLabel(kind: string, since: string | null): string | null {
61
61
  case "working":
62
62
  case "background":
63
63
  return `started ${ago}`;
64
- case "needs-you":
64
+ case "waits-on-you":
65
65
  return `waiting since ${ago}`;
66
66
  case "failed":
67
67
  return `failed ${ago}`;
@@ -1,6 +1,6 @@
1
1
  // Auto-expansion: transitions found by diffing snapshots, and the
2
2
  // transient targets they leave behind. Pure.
3
- import { revealsOn } from "./attention";
3
+ import { revealsOn } from "./needs-you";
4
4
  import type { Forest } from "./families";
5
5
 
6
6
  /**
@@ -15,20 +15,20 @@ export type Targets = ReadonlyMap<string, TargetKind>;
15
15
  export interface Snapshot {
16
16
  activeThreadId: string | null;
17
17
  unread: ReadonlySet<string>;
18
- /** Threads that wait on you or failed, as Needs attention counts them. */
19
- attention: ReadonlySet<string>;
18
+ /** Threads that wait on you or failed, as Needs you counts them. */
19
+ needsYou: ReadonlySet<string>;
20
20
  }
21
21
 
22
22
  export function snapshotOf(forest: Forest, activeThreadId: string | null): Snapshot {
23
23
  const unread = new Set<string>();
24
- const attention = new Set<string>();
24
+ const needsYou = new Set<string>();
25
25
  for (const info of forest.infos.values()) {
26
26
  if (info.thread.isArchived) continue;
27
- // A finished child that Needs attention does not count never opens anything.
28
- if (info.attention.has("unread") && !info.thread.isHidden) unread.add(info.thread.id);
29
- if (revealsOn(info.attention, info.parentId === null)) attention.add(info.thread.id);
27
+ // A finished child that does not need you never opens anything.
28
+ if (info.needsYou.has("unread") && !info.thread.isHidden) unread.add(info.thread.id);
29
+ if (revealsOn(info.needsYou, info.parentId === null)) needsYou.add(info.thread.id);
30
30
  }
31
- return { activeThreadId, unread, attention };
31
+ return { activeThreadId, unread, needsYou };
32
32
  }
33
33
 
34
34
  /**
@@ -40,8 +40,8 @@ export function detectTransitions(previous: Snapshot | null, next: Snapshot): Ma
40
40
  if (next.activeThreadId !== null && next.activeThreadId !== previous?.activeThreadId) {
41
41
  found.set(next.activeThreadId, "reveal");
42
42
  }
43
- for (const id of next.attention) {
44
- if (!previous?.attention.has(id)) found.set(id, "reveal");
43
+ for (const id of next.needsYou) {
44
+ if (!previous?.needsYou.has(id)) found.set(id, "reveal");
45
45
  }
46
46
  for (const id of next.unread) {
47
47
  if (id === next.activeThreadId || found.has(id)) continue;