@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.
- package/README.md +2 -2
- package/dist/app.css +1 -1
- package/dist/app.js +32 -32
- package/dist/app.meta.json +1 -1
- package/dist/server.js +8 -8
- package/dist/server.js.map +3 -3
- package/dist/server.meta.json +1 -1
- package/features/thread-list/components/FoldRows.tsx +14 -13
- package/features/thread-list/components/GroupSection.tsx +118 -69
- package/features/thread-list/components/Rails.tsx +2 -3
- package/features/thread-list/components/ThreadList.tsx +21 -16
- package/features/thread-list/components/ThreadRowView.tsx +12 -9
- package/features/thread-list/components/Toolbar.tsx +129 -187
- package/features/thread-list/components/controller.ts +1 -2
- package/features/thread-list/components/glyphs.tsx +1 -1
- package/features/thread-list/data/useClientPreferences.ts +1 -1
- package/features/thread-list/data/useNeedsYouHold.ts +19 -0
- package/features/thread-list/model/counters.ts +13 -19
- package/features/thread-list/model/details.ts +2 -2
- package/features/thread-list/model/expansion.ts +10 -10
- package/features/thread-list/model/families.ts +50 -48
- package/features/thread-list/model/labels.ts +2 -1
- package/features/thread-list/model/layout.ts +6 -11
- package/features/thread-list/model/needs-you.ts +133 -0
- package/features/thread-list/model/notes.ts +1 -1
- package/features/thread-list/model/settings.ts +54 -0
- package/features/thread-list/model/share.ts +7 -1
- package/features/thread-list/model/sort.ts +14 -6
- package/features/thread-list/model/state.ts +11 -11
- package/features/thread-list/model/time.ts +1 -1
- package/features/thread-list/model/toggles.ts +2 -7
- package/features/thread-list/model/view.ts +139 -143
- package/features/thread-list/testing/fixtures.ts +19 -10
- package/package.json +1 -1
- package/server/cli.ts +2 -2
- package/shared/preferences.ts +10 -23
- package/skills/thread-glance/SKILL.md +9 -10
- package/features/thread-list/model/attention.ts +0 -89
package/shared/preferences.ts
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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 : {
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
| `
|
|
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
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
}
|