@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
@@ -9,7 +9,6 @@ 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 nestingSchema = z.enum(["folded", "tree"]);
13
12
  export const lifecycleSchema = z.enum(["active", "archived"]);
14
13
  export const harnessIconSchema = z.enum(["muted", "colour", "hidden"]);
15
14
  export const childAttentionSchema = z.enum(["blocked", "everything"]);
@@ -22,7 +21,6 @@ const hiddenGroupsSchema = z
22
21
  export type OrganizationMode = z.infer<typeof organizationModeSchema>;
23
22
  export type SortField = z.infer<typeof sortFieldSchema>;
24
23
  export type SortDirection = z.infer<typeof sortDirectionSchema>;
25
- export type Nesting = z.infer<typeof nestingSchema>;
26
24
  export type Lifecycle = z.infer<typeof lifecycleSchema>;
27
25
  export type HarnessIcon = z.infer<typeof harnessIconSchema>;
28
26
  export type ChildAttention = z.infer<typeof childAttentionSchema>;
@@ -50,7 +48,7 @@ export const PREFERENCES = {
50
48
  .max(2)
51
49
  .refine((values) => new Set(values).size === values.length, "Lifecycles must be unique"),
52
50
  ["active"] as Lifecycle[],
53
- "Thread lifecycles shown: active, archived, or both. At least one is required.",
51
+ "Threads shown: [\"active\"], [\"archived\"] or both. At least one is required.",
54
52
  ),
55
53
  organizationMode: define(
56
54
  organizationModeSchema,
@@ -60,7 +58,7 @@ export const PREFERENCES = {
60
58
  environmentGrouping: define(
61
59
  z.boolean(),
62
60
  false,
63
- "Whether sibling threads sharing a worktree environment fold into a folder row.",
61
+ "Worktrees as folders: whether sibling threads sharing a worktree environment fold into a folder row.",
64
62
  ),
65
63
  chronologicalSort: define(
66
64
  sortFieldSchema,
@@ -70,7 +68,7 @@ export const PREFERENCES = {
70
68
  sortDirection: define(
71
69
  sortDirectionSchema,
72
70
  "default" as SortDirection,
73
- "Sort direction; default keeps the field's natural direction.",
71
+ "Sort direction: ascending or descending. A saved default reads as the field's own direction (descending for dates, ascending for alpha).",
74
72
  ),
75
73
  sectionOrder: define(
76
74
  idListSchema,
@@ -109,20 +107,15 @@ export const PREFERENCES = {
109
107
  [] as string[],
110
108
  "Environment ids whose folder row is collapsed.",
111
109
  ),
112
- nesting: define(
113
- nestingSchema,
114
- "folded" as Nesting,
115
- "Child thread nesting: folded (one flat level behind a chip) or tree (bb's tree).",
116
- ),
117
110
  foldOlder: define(
118
111
  z.boolean(),
119
112
  true,
120
- "Whether quiet roots past the 5 most recent fold behind an N older row.",
113
+ "Collapse older threads: whether each group's quiet roots past its 5 newest fold behind an N older row.",
121
114
  ),
122
115
  workingFirst: define(
123
116
  z.boolean(),
124
117
  false,
125
- "Whether working threads sort first under Updated, as bb's list does.",
118
+ "Working threads first: whether working threads sort first under Updated, as bb's list does.",
126
119
  ),
127
120
  expandedOlder: define(
128
121
  idListSchema,
@@ -132,18 +125,13 @@ export const PREFERENCES = {
132
125
  expandedChildren: define(
133
126
  idListSchema,
134
127
  [] as string[],
135
- "Parent thread ids the user expanded (folded nesting).",
136
- ),
137
- collapsedChildren: define(
138
- idListSchema,
139
- [] as string[],
140
- "Parent thread ids the user collapsed (tree nesting).",
128
+ "Parent thread ids whose chip the user opened.",
141
129
  ),
142
130
  showPullRequests: define(z.boolean(), true, "Whether rows show a pull request badge."),
143
131
  childAttention: define(
144
132
  childAttentionSchema,
145
133
  "blocked" as ChildAttention,
146
- "Which child threads count under Needs attention, in badges and counters, and stay out of a family's fold along with running children: blocked (waiting on you, offline, or an orphaned failure) or everything (also every failed and unread child).",
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.",
147
135
  ),
148
136
  harnessIcon: define(
149
137
  harnessIconSchema,
@@ -207,7 +195,7 @@ export function coercePreferences(raw: unknown): Preferences {
207
195
  /**
208
196
  * Maps bb's own thread-list preferences onto ours for the first-run import
209
197
  *. `environmentGrouping: "auto"` becomes off, `collapsedThreads` is
210
- * skipped because it means the inverse of folded nesting, and invalid values
198
+ * skipped because it means the inverse of `expandedChildren`, and invalid values
211
199
  * are skipped. Returns only the keys that parsed.
212
200
  */
213
201
  export function mapBbPreferences(raw: unknown): Partial<Preferences> {
@@ -224,9 +212,8 @@ export function mapBbPreferences(raw: unknown): Partial<Preferences> {
224
212
  return mapped as Partial<Preferences>;
225
213
  }
226
214
 
227
- /** Per-client preferences, kept in localStorage only. */
215
+ /** Per-client preferences, kept in localStorage only. A saved `filter` from before Needs you is dropped. */
228
216
  export const clientPreferencesSchema = z.object({
229
- filter: z.enum(["all", "attention"]).catch("all"),
230
217
  density: z.enum(["compact", "comfortable"]).catch("compact"),
231
218
  });
232
219
  export type ClientPreferences = z.infer<typeof clientPreferencesSchema>;
@@ -237,5 +224,5 @@ export const BB_PREFERENCES_MIRROR_STORAGE_KEY = "bb.thread-list.preferences.v1"
237
224
 
238
225
  export function parseClientPreferences(raw: unknown): ClientPreferences {
239
226
  const result = clientPreferencesSchema.safeParse(raw ?? {});
240
- return result.success ? result.data : { filter: "all", density: "compact" };
227
+ return result.success ? result.data : { density: "compact" };
241
228
  }
@@ -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 nesting and folding, harness icons. Use when asked to change how the Thread Glance sidebar lists, groups, sorts, hides, collapses or nests 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 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."
4
4
  ---
5
5
 
6
6
  # Thread Glance preferences
@@ -19,7 +19,7 @@ bb thread-glance prefs reset <key> [--json]
19
19
  `set` takes JSON, and a bare word is read as a string:
20
20
 
21
21
  ```sh
22
- bb thread-glance prefs set nesting tree
22
+ bb thread-glance prefs set sortDirection ascending
23
23
  bb thread-glance prefs set threadLifecycles '["active","archived"]'
24
24
  bb thread-glance prefs set hiddenGroups '["threads","project:<project-id>"]'
25
25
  ```
@@ -42,7 +42,7 @@ and `bb environment list`; a thread id is a `thr_…` id.
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` |
45
- | `sortDirection` | `default` (the field's own direction), `ascending`, `descending` | `default` |
45
+ | `sortDirection` | `ascending` or `descending`; a saved `default` reads as the field's own direction (descending for dates, ascending for `alpha`) | `default` |
46
46
  | `sectionOrder` | Top-level order by project: `pinned`, `projects`, `threads` | all three |
47
47
  | `manualSectionOrder` | Top-level order chronologically: `pinned`, `sections`, `threads` | all three |
48
48
  | `machineSectionOrder` | Top-level order by machine: `pinned`, `machines`, `threads` | all three |
@@ -52,18 +52,17 @@ 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
- | `nesting` | `folded`: children one level deep behind a chip; `tree`: bb's full tree | `folded` |
56
- | `foldOlder` | `true` folds quiet top-level threads past the 5 most recent behind an "N older" row | `true` |
55
+ | `foldOlder` | Collapse older threads: `true` folds a group's quiet top-level threads past its 5 newest behind an "N older" row | `true` |
57
56
  | `workingFirst` | `true` sorts working threads first under `updated` | `false` |
58
57
  | `expandedOlder` | Group ids and parent thread ids whose "N older" or "N more child threads" row is open | `[]` |
59
- | `expandedChildren` | Parent thread ids whose children are shown (`folded` nesting) | `[]` |
60
- | `collapsedChildren` | Parent thread ids whose children are hidden (`tree` nesting) | `[]` |
58
+ | `expandedChildren` | Parent thread ids whose chip is open | `[]` |
61
59
  | `showPullRequests` | `true` shows a pull request badge on rows | `true` |
62
- | `childAttention` | Which child threads count as needing attention (filter, badge, counters, ordering, 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 failed with its manager idle; `everything` also counts every failed or unread child | `blocked` |
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` |
63
61
  | `harnessIcon` | How rows draw the harness logo: `muted` (monochrome), `colour` (the provider's tint) or `hidden` | `muted` |
64
62
 
65
- The filter (all or needs attention) and the row density are kept per
66
- browser, and the CLI cannot read or change them.
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.
67
66
 
68
67
  Sections themselves and the section a thread is in are bb core state: use
69
68
  `bb thread section` and `bb thread update`.
@@ -1,89 +0,0 @@
1
- // What counts as "needs attention", thread by thread. The filter, its
2
- // badge, the header counters, the urgency order and auto-reveal all read the
3
- // set this builds. Pure.
4
- import type { PluginSidebarThread } from "@get-bb/plugin-sdk/app";
5
- import type { ChildAttention } from "@/shared/preferences";
6
- import { type Flag, type StateKind, type ThreadState } from "./state";
7
-
8
- /** The flags that count for a thread nobody manages. Working is not one. */
9
- export const ROOT_ATTENTION: ReadonlySet<Flag> = new Set<Flag>([
10
- "needs-you",
11
- "unread-failed",
12
- "queue-failed",
13
- "offline",
14
- "unread",
15
- ]);
16
-
17
- /** States in which a manager is busy and may still deal with a failed child. */
18
- const BUSY: ReadonlySet<StateKind> = new Set<StateKind>(["working", "background", "queued", "scheduled"]);
19
-
20
- export interface Manager {
21
- thread: Pick<PluginSidebarThread, "latestAttentionAt">;
22
- state: Pick<ThreadState, "kind">;
23
- /** When the plugin server last saw the manager finish a turn, if it did. */
24
- finishedAt?: number;
25
- }
26
-
27
- /** A manager is idle when it is not working, setting up, running background work or holding a queued message. */
28
- export function isManagerIdle(manager: Manager): boolean {
29
- return !BUSY.has(manager.state.kind);
30
- }
31
-
32
- /** When the thread's failure happened: an error bumps `latestAttentionAt`, a failed queue only `updatedAt`. */
33
- export function failureTime(
34
- thread: Pick<PluginSidebarThread, "latestAttentionAt" | "updatedAt">,
35
- flags: ReadonlySet<Flag>,
36
- ): number {
37
- let at = 0;
38
- if (flags.has("unread-failed")) at = Math.max(at, thread.latestAttentionAt);
39
- if (flags.has("queue-failed")) at = Math.max(at, thread.updatedAt);
40
- return at;
41
- }
42
-
43
- /**
44
- * A failure nobody is dealing with: the manager is idle and has not run or
45
- * finished since the child failed. A busy manager, or one that finished after
46
- * the failure, is taken to know about it. The manager's own clock is its last
47
- * finish (`finishedAt`, or `latestAttentionAt` when it failed): `updatedAt`
48
- * also moves when someone merely opens or renames it.
49
- */
50
- export function isOrphanedFailure(
51
- thread: Pick<PluginSidebarThread, "latestAttentionAt" | "updatedAt">,
52
- flags: ReadonlySet<Flag>,
53
- manager: Manager,
54
- ): boolean {
55
- if (!flags.has("unread-failed") && !flags.has("queue-failed")) return false;
56
- if (!isManagerIdle(manager)) return false;
57
- const managerActiveAt = Math.max(manager.thread.latestAttentionAt, manager.finishedAt ?? 0);
58
- return managerActiveAt <= failureTime(thread, flags);
59
- }
60
-
61
- /**
62
- * The flags of one thread that count towards Needs attention. A root keeps
63
- * its own. A child counts when it waits on you or is offline, or when it
64
- * failed with an idle manager; a finished-unread child never does. With
65
- * `everything`, a child counts like a root.
66
- */
67
- export function attentionFlagsOf(
68
- flags: ReadonlySet<Flag>,
69
- options: { isRoot: boolean; mode: ChildAttention; orphaned: boolean },
70
- ): Set<Flag> {
71
- const kept = new Set<Flag>();
72
- if (options.isRoot || options.mode === "everything") {
73
- for (const flag of flags) if (ROOT_ATTENTION.has(flag)) kept.add(flag);
74
- return kept;
75
- }
76
- if (flags.has("needs-you")) kept.add("needs-you");
77
- if (flags.has("offline")) kept.add("offline");
78
- if (options.orphaned) {
79
- if (flags.has("unread-failed")) kept.add("unread-failed");
80
- if (flags.has("queue-failed")) kept.add("queue-failed");
81
- }
82
- return kept;
83
- }
84
-
85
- /** Whether a set of attention flags is worth an auto-reveal: it waits on you or failed. */
86
- export function revealsOn(attention: ReadonlySet<Flag>, isRoot: boolean): boolean {
87
- if (attention.has("needs-you") || attention.has("unread-failed")) return true;
88
- return !isRoot && attention.has("queue-failed");
89
- }