@gperezmz/bb-plugin-thread-glance 0.2.3 → 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.
- package/README.md +1 -1
- package/app.tsx +2 -1
- package/dist/app.css +1 -1
- package/dist/app.js +31 -31
- package/dist/app.meta.json +1 -1
- package/dist/server.js +10 -10
- package/dist/server.js.map +3 -3
- package/dist/server.meta.json +1 -1
- package/features/thread-list/components/FoldRows.tsx +21 -30
- package/features/thread-list/components/GroupSection.tsx +8 -50
- package/features/thread-list/components/ListHeader.tsx +79 -0
- package/features/thread-list/components/ProviderBadge.tsx +0 -1
- package/features/thread-list/components/PullRequestProbes.tsx +27 -0
- package/features/thread-list/components/{Toolbar.tsx → SettingsPanel.tsx} +26 -57
- package/features/thread-list/components/ThreadDetails.tsx +3 -3
- package/features/thread-list/components/ThreadList.tsx +116 -84
- package/features/thread-list/components/ThreadRowView.tsx +70 -78
- package/features/thread-list/components/controller.ts +5 -10
- package/features/thread-list/components/glyphs.tsx +39 -14
- package/features/thread-list/data/same-window.ts +34 -0
- package/features/thread-list/data/useAutoExpand.ts +1 -1
- package/features/thread-list/data/useClientPreferences.ts +15 -7
- package/features/thread-list/data/useDefaultBranches.ts +35 -0
- package/features/thread-list/data/usePreferences.ts +16 -7
- package/features/thread-list/data/usePullRequestAnswers.ts +36 -0
- package/features/thread-list/data/useSystemFacts.ts +52 -0
- package/features/thread-list/icons.ts +1 -1
- package/features/thread-list/model/attention.ts +2 -66
- package/features/thread-list/model/branches.ts +27 -0
- package/features/thread-list/model/counters.ts +5 -5
- package/features/thread-list/model/details.ts +6 -6
- package/features/thread-list/model/expansion.ts +5 -5
- package/features/thread-list/model/groups.ts +2 -2
- package/features/thread-list/model/labels.ts +28 -17
- package/features/thread-list/model/layout.ts +5 -2
- package/features/thread-list/model/menu.ts +1 -0
- package/features/thread-list/model/move.ts +1 -1
- package/features/thread-list/model/notes.ts +2 -28
- package/features/thread-list/model/settings.ts +2 -18
- package/features/thread-list/model/settled.ts +122 -0
- package/features/thread-list/model/share.ts +1 -7
- package/features/thread-list/model/sort.ts +5 -16
- package/features/thread-list/model/state.ts +25 -9
- package/features/thread-list/model/toggles.ts +35 -24
- package/features/thread-list/model/{families.ts → trees.ts} +40 -28
- package/features/thread-list/model/view.ts +200 -274
- package/features/thread-list/testing/fixtures.ts +41 -12
- package/package.json +1 -1
- package/server/cli.ts +1 -1
- package/server/import.ts +36 -6
- package/server/preference-store.ts +2 -1
- package/server.ts +1 -1
- package/shared/preferences.ts +38 -27
- package/skills/thread-glance/SKILL.md +11 -12
- 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/
|
|
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
|
|
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 === "
|
|
160
|
-
?
|
|
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
|
-
/**
|
|
173
|
-
export function
|
|
174
|
-
return
|
|
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
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
|
|
25
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
{
|
|
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 =
|
|
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, {
|
package/shared/preferences.ts
CHANGED
|
@@ -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
|
|
13
|
-
export const
|
|
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
|
-
|
|
45
|
-
z
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
"
|
|
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
|
|
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)
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `expandedOlder` |
|
|
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
|
-
| `
|
|
60
|
-
| `
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
}
|