@gperezmz/bb-plugin-thread-glance 0.4.1 → 0.4.2
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/dist/app.css +1 -1
- package/dist/app.js +30 -30
- 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/GroupSection.tsx +10 -7
- package/features/thread-list/components/ListHeader.tsx +8 -3
- package/features/thread-list/components/ThreadList.tsx +34 -13
- package/features/thread-list/components/ThreadRowView.tsx +5 -4
- package/features/thread-list/components/controller.ts +1 -1
- package/features/thread-list/components/input-modality.ts +51 -0
- package/features/thread-list/components/row-card.ts +1 -1
- package/features/thread-list/data/useIdleSince.ts +27 -0
- package/features/thread-list/data/useNow.ts +4 -3
- package/features/thread-list/data/useStamps.ts +19 -4
- package/features/thread-list/model/attention.ts +71 -7
- package/features/thread-list/model/expansion.ts +1 -1
- package/features/thread-list/model/settled.ts +1 -1
- package/features/thread-list/model/state.ts +15 -2
- package/features/thread-list/model/toggles.ts +3 -3
- package/features/thread-list/model/trees.ts +46 -9
- package/features/thread-list/model/view.ts +10 -4
- package/features/thread-list/testing/fixtures.ts +10 -0
- package/package.json +1 -1
- package/server/stamps.ts +21 -3
- package/server.ts +5 -0
- package/shared/contract.ts +8 -0
- package/shared/signals.ts +2 -0
- package/skills/thread-glance/SKILL.md +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Preferences } from "@/shared/preferences";
|
|
4
4
|
import { ancestorsOf, type ThreadTree, type Forest } from "./trees";
|
|
5
5
|
import { isGroupCollapsed, toggleGroupCollapse } from "./groups";
|
|
6
|
-
import { isDoneUnseen } from "./state";
|
|
6
|
+
import { isDoneUnseen, type ThreadContext } from "./state";
|
|
7
7
|
import type { GroupView, OlderRow, SettledRow, ThreadRow } from "./view";
|
|
8
8
|
|
|
9
9
|
export interface ToggleOutcome {
|
|
@@ -76,7 +76,7 @@ export interface MarkAllRead {
|
|
|
76
76
|
/** Every unread thread in the trees, descendants included: Mark all read, and a root's Mark read for its tree. */
|
|
77
77
|
export function markAllReadPlan(
|
|
78
78
|
trees: readonly ThreadTree[],
|
|
79
|
-
context:
|
|
79
|
+
context: ThreadContext,
|
|
80
80
|
): MarkAllRead {
|
|
81
81
|
const read: string[] = [];
|
|
82
82
|
const seen: string[] = [];
|
|
@@ -97,7 +97,7 @@ export function markAllReadPlan(
|
|
|
97
97
|
export function markReadPlanFor(
|
|
98
98
|
threadId: string,
|
|
99
99
|
forest: Pick<Forest, "infos" | "treeOf">,
|
|
100
|
-
context:
|
|
100
|
+
context: ThreadContext,
|
|
101
101
|
): MarkAllRead {
|
|
102
102
|
const tree = forest.treeOf.get(threadId);
|
|
103
103
|
if (tree !== undefined && tree.root.thread.id === threadId) return markAllReadPlan([tree], context);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type StateKind,
|
|
14
14
|
type ThreadState,
|
|
15
15
|
} from "./state";
|
|
16
|
-
import { attentionFlagsOf,
|
|
16
|
+
import { attentionFlagsOf, orphanedAt } from "./attention";
|
|
17
17
|
import { compareCreationAscending } from "./sort";
|
|
18
18
|
import { needsKindOf, rowNote, type RowNote } from "./notes";
|
|
19
19
|
import type { ThreadNotes } from "@/shared/contract";
|
|
@@ -40,9 +40,9 @@ export interface ThreadInfo {
|
|
|
40
40
|
/** Quiet test for the thread alone. */
|
|
41
41
|
quiet: boolean;
|
|
42
42
|
/**
|
|
43
|
-
* A quiet thread, as if no thread were
|
|
43
|
+
* A quiet thread, as if no thread were focused: nothing to see behind a
|
|
44
44
|
* tree's fold. A root, or any child when `childAttention` is
|
|
45
|
-
* `everything`, takes the quiet test without the
|
|
45
|
+
* `everything`, takes the quiet test without the focused thread's exemption.
|
|
46
46
|
* Otherwise a child is quiet unless it runs or needs attention, so finishing
|
|
47
47
|
* unread folds it; an archived child is always quiet. The fold reads this,
|
|
48
48
|
* so opening a thread never changes which children stay shown.
|
|
@@ -67,7 +67,7 @@ export interface Subtree {
|
|
|
67
67
|
visibleCount: number;
|
|
68
68
|
/** Visible direct children: the chip's number. */
|
|
69
69
|
childCount: number;
|
|
70
|
-
/** Every visible descendant is quiet, as if no thread were
|
|
70
|
+
/** Every visible descendant is quiet, as if no thread were focused, so the subtree adds nothing to see when folded. */
|
|
71
71
|
quietIgnoringOpen: boolean;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -88,7 +88,7 @@ export interface ThreadTree {
|
|
|
88
88
|
/** Every thread quiet and the active thread not in it. */
|
|
89
89
|
quiet: boolean;
|
|
90
90
|
/**
|
|
91
|
-
* The quiet test without the
|
|
91
|
+
* The quiet test without the focused thread's exemption. A group's fold reads
|
|
92
92
|
* this, so opening a thread never changes which roots stay shown.
|
|
93
93
|
*/
|
|
94
94
|
quietIgnoringOpen: boolean;
|
|
@@ -103,6 +103,8 @@ export interface Forest {
|
|
|
103
103
|
treeOf: ReadonlyMap<string, ThreadTree>;
|
|
104
104
|
/** The rollup under every thread with a row, by thread id. */
|
|
105
105
|
subtrees: ReadonlyMap<string, Subtree>;
|
|
106
|
+
/** The earliest moment after `now` a child's failure becomes an orphaned failure, or null for none. */
|
|
107
|
+
nextOrphanAt: number | null;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
export interface ForestInputs extends ThreadContext {
|
|
@@ -114,6 +116,12 @@ export interface ForestInputs extends ThreadContext {
|
|
|
114
116
|
notes?: Readonly<Record<string, ThreadNotes>>;
|
|
115
117
|
/** Which children can need attention; `blocked` when absent. */
|
|
116
118
|
childAttention?: ChildAttention;
|
|
119
|
+
/** When this list last saw each thread go from busy to idle (see `trackIdle`). */
|
|
120
|
+
idleSince?: Readonly<Record<string, number>>;
|
|
121
|
+
/** The server's `idleAt` stamps: when any window last saw each thread go idle. */
|
|
122
|
+
idleAt?: Readonly<Record<string, number>>;
|
|
123
|
+
/** False until the stamps have loaded: no failure counts as orphaned before then. */
|
|
124
|
+
stampsLoaded?: boolean;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
function isPinned(thread: PluginSidebarThread): boolean {
|
|
@@ -148,6 +156,18 @@ export function attachParent(
|
|
|
148
156
|
return attach;
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
/**
|
|
160
|
+
* When a thread last went idle, as this list or any window saw it; null while
|
|
161
|
+
* the stamps that may hold it have not loaded.
|
|
162
|
+
*/
|
|
163
|
+
function idleSinceOf(id: string, inputs: ForestInputs): number | null | undefined {
|
|
164
|
+
if (inputs.stampsLoaded === false) return null;
|
|
165
|
+
const seen = inputs.idleSince?.[id];
|
|
166
|
+
const stamped = inputs.idleAt?.[id];
|
|
167
|
+
if (seen === undefined) return stamped;
|
|
168
|
+
return stamped === undefined ? seen : Math.max(seen, stamped);
|
|
169
|
+
}
|
|
170
|
+
|
|
151
171
|
export function buildForest(inputs: ForestInputs): Forest {
|
|
152
172
|
const byId = new Map(inputs.threads.map((thread) => [thread.id, thread]));
|
|
153
173
|
const infos = new Map<string, ThreadInfo>();
|
|
@@ -178,14 +198,22 @@ export function buildForest(inputs: ForestInputs): Forest {
|
|
|
178
198
|
}
|
|
179
199
|
|
|
180
200
|
const mode = inputs.childAttention ?? "blocked";
|
|
201
|
+
let nextOrphanAt: number | null = null;
|
|
181
202
|
for (const info of infos.values()) {
|
|
182
203
|
const parent = info.parentId === null ? undefined : infos.get(info.parentId);
|
|
204
|
+
const orphaned =
|
|
205
|
+
parent === undefined
|
|
206
|
+
? null
|
|
207
|
+
: orphanedAt(info.thread, info.flags, {
|
|
208
|
+
...parent,
|
|
209
|
+
finishedAt: inputs.finishedAt[parent.thread.id],
|
|
210
|
+
idleSince: idleSinceOf(parent.thread.id, inputs),
|
|
211
|
+
});
|
|
212
|
+
if (orphaned !== null && orphaned > inputs.now) nextOrphanAt = Math.min(nextOrphanAt ?? orphaned, orphaned);
|
|
183
213
|
info.attentionFlags = attentionFlagsOf(info.flags, {
|
|
184
214
|
isRoot: parent === undefined,
|
|
185
215
|
mode,
|
|
186
|
-
orphaned:
|
|
187
|
-
parent !== undefined &&
|
|
188
|
-
isOrphanedFailure(info.thread, info.flags, { ...parent, finishedAt: inputs.finishedAt[parent.thread.id] }),
|
|
216
|
+
orphaned: orphaned !== null && orphaned <= inputs.now,
|
|
189
217
|
});
|
|
190
218
|
info.quietIgnoringOpen =
|
|
191
219
|
parent === undefined || mode === "everything"
|
|
@@ -294,7 +322,16 @@ export function buildForest(inputs: ForestInputs): Forest {
|
|
|
294
322
|
treeOf.set(root.thread.id, tree);
|
|
295
323
|
for (const info of descendants) treeOf.set(info.thread.id, tree);
|
|
296
324
|
}
|
|
297
|
-
return { infos, children, trees, treeOf, subtrees };
|
|
325
|
+
return { infos, children, trees, treeOf, subtrees, nextOrphanAt };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Whether any thread in the trees is unread, descendants, hidden and
|
|
330
|
+
* archived threads included: the threads Mark all read marks, so it is
|
|
331
|
+
* offered only when this holds.
|
|
332
|
+
*/
|
|
333
|
+
export function anyUnread(trees: readonly ThreadTree[]): boolean {
|
|
334
|
+
return trees.some((tree) => tree.root.unread || tree.descendants.some((info) => info.unread));
|
|
298
335
|
}
|
|
299
336
|
|
|
300
337
|
/** Ids from `id` up to (not including) `stopAt`, nearest first. */
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
PluginSidebarThread,
|
|
7
7
|
} from "@get-bb/plugin-sdk/app";
|
|
8
8
|
import type { Preferences } from "@/shared/preferences";
|
|
9
|
-
import { ancestorsOf, type ThreadTree, type Forest, type Subtree, type ThreadInfo } from "./trees";
|
|
9
|
+
import { ancestorsOf, anyUnread, type ThreadTree, type Forest, type Subtree, type ThreadInfo } from "./trees";
|
|
10
10
|
import {
|
|
11
11
|
builtinGroup,
|
|
12
12
|
entityGroups,
|
|
@@ -142,6 +142,8 @@ export interface GroupView {
|
|
|
142
142
|
rows: Row[];
|
|
143
143
|
/** Every tree root bucketed in the group, those behind folds included. */
|
|
144
144
|
rootIds: string[];
|
|
145
|
+
/** A thread in the group's trees is unread: its menu offers Mark all read. */
|
|
146
|
+
hasUnread: boolean;
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
export interface ListView {
|
|
@@ -153,6 +155,8 @@ export interface ListView {
|
|
|
153
155
|
order: string[];
|
|
154
156
|
/** Thread trees that need attention, in every group, hidden ones included: the need-you filter's N. */
|
|
155
157
|
needYouCount: number;
|
|
158
|
+
/** A thread in the list is unread, hidden groups included: the list header offers Mark all read. */
|
|
159
|
+
hasUnread: boolean;
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
export interface ViewInputs {
|
|
@@ -188,7 +192,7 @@ interface Context extends ViewInputs {
|
|
|
188
192
|
/** Parent ids on the path to a reveal target. */
|
|
189
193
|
revealPath: ReadonlySet<string>;
|
|
190
194
|
revealIds: ReadonlySet<string>;
|
|
191
|
-
/** The
|
|
195
|
+
/** The focused thread and its ancestors. */
|
|
192
196
|
activePath: ReadonlySet<string>;
|
|
193
197
|
projectNames: ReadonlyMap<string, string>;
|
|
194
198
|
sectionNames: ReadonlyMap<string, string>;
|
|
@@ -325,7 +329,7 @@ function foldedChildren(
|
|
|
325
329
|
}
|
|
326
330
|
|
|
327
331
|
// The stable set reads `quietIgnoringOpen` for the child and everything under it, and
|
|
328
|
-
// ignores which thread is
|
|
332
|
+
// ignores which thread is focused and the reveal targets: they join afterwards
|
|
329
333
|
// and take no other row's place.
|
|
330
334
|
const quietTree = (kid: ThreadInfo) => kid.quietIgnoringOpen && subtreeOf(context, kid.thread.id).quietIgnoringOpen;
|
|
331
335
|
const stable = new Set<string>();
|
|
@@ -514,6 +518,7 @@ function buildGroup(context: Context, descriptor: GroupDescriptor, trees: Thread
|
|
|
514
518
|
hidden,
|
|
515
519
|
rows,
|
|
516
520
|
rootIds: trees.map((tree) => tree.root.thread.id),
|
|
521
|
+
hasUnread: anyUnread(trees),
|
|
517
522
|
};
|
|
518
523
|
}
|
|
519
524
|
|
|
@@ -539,6 +544,7 @@ function needYouGroup(context: Context, descriptor: GroupDescriptor, trees: Thre
|
|
|
539
544
|
hidden,
|
|
540
545
|
rows: clusterEnvironments(context, sorted.filter(needsAttention).map((tree) => treeUnit(context, tree)), 0),
|
|
541
546
|
rootIds: trees.map((tree) => tree.root.thread.id),
|
|
547
|
+
hasUnread: anyUnread(trees),
|
|
542
548
|
};
|
|
543
549
|
}
|
|
544
550
|
|
|
@@ -624,7 +630,7 @@ export function buildListView(inputs: ViewInputs): ListView {
|
|
|
624
630
|
}
|
|
625
631
|
}
|
|
626
632
|
|
|
627
|
-
return { groups, more, moreCounters, order, needYouCount: countNeedYou(forest) };
|
|
633
|
+
return { groups, more, moreCounters, order, needYouCount: countNeedYou(forest), hasUnread: anyUnread(forest.trees) };
|
|
628
634
|
}
|
|
629
635
|
|
|
630
636
|
/** Every thread row in visual order, for keyboard and windowing. */
|
|
@@ -109,6 +109,8 @@ export interface Scenario {
|
|
|
109
109
|
sections?: PluginSidebarSection[];
|
|
110
110
|
prefs?: Partial<Preferences>;
|
|
111
111
|
activeThreadId?: string | null;
|
|
112
|
+
/** Threads other split panes show. */
|
|
113
|
+
openThreadIds?: string[];
|
|
112
114
|
targets?: Targets;
|
|
113
115
|
finishedAt?: Record<string, number>;
|
|
114
116
|
seenAt?: Record<string, number>;
|
|
@@ -117,6 +119,10 @@ export interface Scenario {
|
|
|
117
119
|
notes?: Record<string, import("@/shared/contract").ThreadNotes>;
|
|
118
120
|
now?: number;
|
|
119
121
|
startedAt?: Record<string, number>;
|
|
122
|
+
/** When this list saw each thread go idle, and the server's idleAt stamps. */
|
|
123
|
+
idleSince?: Record<string, number>;
|
|
124
|
+
idleAt?: Record<string, number>;
|
|
125
|
+
stampsLoaded?: boolean;
|
|
120
126
|
/** Project id → default branch; "main" for every project when absent. */
|
|
121
127
|
defaultBranches?: Record<string, string | null>;
|
|
122
128
|
/** bb's primary machine; "host_1", the fixtures' own, when absent. */
|
|
@@ -131,6 +137,7 @@ export function forestOf(scenario: Scenario): Forest {
|
|
|
131
137
|
return buildForest({
|
|
132
138
|
threads: scenario.threads,
|
|
133
139
|
activeThreadId: scenario.activeThreadId ?? null,
|
|
140
|
+
openThreadIds: new Set(scenario.openThreadIds ?? []),
|
|
134
141
|
finishedAt: scenario.finishedAt ?? {},
|
|
135
142
|
seenAt: scenario.seenAt ?? {},
|
|
136
143
|
scheduled: scenario.scheduled ?? {},
|
|
@@ -138,6 +145,9 @@ export function forestOf(scenario: Scenario): Forest {
|
|
|
138
145
|
notes: scenario.notes ?? {},
|
|
139
146
|
childAttention: scenario.prefs?.childAttention,
|
|
140
147
|
now: scenario.now ?? T0 + 60_000,
|
|
148
|
+
idleSince: scenario.idleSince,
|
|
149
|
+
idleAt: scenario.idleAt,
|
|
150
|
+
stampsLoaded: scenario.stampsLoaded,
|
|
141
151
|
});
|
|
142
152
|
}
|
|
143
153
|
|
package/package.json
CHANGED
package/server/stamps.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Per-thread timestamps: one kv row per thread, `stamp:<threadId>`,
|
|
2
|
-
// holding whichever of the
|
|
2
|
+
// holding whichever of the kinds it has. Rows are read into memory once
|
|
3
3
|
// and written through, so listing does not read every row per call.
|
|
4
4
|
import type { BbPluginApi } from "@get-bb/plugin-sdk";
|
|
5
5
|
import { CHANNELS, type StampKind, type StampSignal, type Stamps } from "../shared/contract";
|
|
6
6
|
import { createSerialQueue } from "./serial";
|
|
7
7
|
|
|
8
8
|
export const STAMP_KEY_PREFIX = "stamp:";
|
|
9
|
-
export const STAMP_KINDS: readonly StampKind[] = ["startedAt", "finishedAt", "pendingAt", "seenAt"];
|
|
9
|
+
export const STAMP_KINDS: readonly StampKind[] = ["startedAt", "finishedAt", "pendingAt", "seenAt", "idleAt"];
|
|
10
10
|
|
|
11
11
|
export type ThreadStamps = Partial<Record<StampKind, number>>;
|
|
12
12
|
|
|
@@ -30,6 +30,12 @@ export interface StampStore {
|
|
|
30
30
|
list(): Promise<Stamps>;
|
|
31
31
|
/** Sets `kind` to `at` for each thread and publishes one signal. */
|
|
32
32
|
stamp(kind: StampKind, threadIds: readonly string[], at: number): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Sets `kind` to `at` for each thread whose stored value is earlier or
|
|
35
|
+
* absent, and publishes one signal for those. A later value is kept, so
|
|
36
|
+
* several windows reporting one moment cannot move it back.
|
|
37
|
+
*/
|
|
38
|
+
advance(kind: StampKind, threadIds: readonly string[], at: number): Promise<void>;
|
|
33
39
|
/** Deletes `kind` for each thread and publishes one signal with `value: null`. */
|
|
34
40
|
clear(kind: StampKind, threadIds: readonly string[]): Promise<void>;
|
|
35
41
|
/** Deletes every stamp of each thread. Publishes nothing. */
|
|
@@ -89,7 +95,7 @@ export function createStampStore(
|
|
|
89
95
|
return {
|
|
90
96
|
list: () =>
|
|
91
97
|
serial(async () => {
|
|
92
|
-
const result: Stamps = { startedAt: {}, finishedAt: {}, pendingAt: {}, seenAt: {} };
|
|
98
|
+
const result: Stamps = { startedAt: {}, finishedAt: {}, pendingAt: {}, seenAt: {}, idleAt: {} };
|
|
93
99
|
for (const [threadId, stamps] of await load()) {
|
|
94
100
|
for (const kind of STAMP_KINDS) {
|
|
95
101
|
const value = stamps[kind];
|
|
@@ -107,6 +113,18 @@ export function createStampStore(
|
|
|
107
113
|
}
|
|
108
114
|
publish({ kind, threadIds: [...threadIds], value: at });
|
|
109
115
|
}),
|
|
116
|
+
advance: (kind, threadIds, at) =>
|
|
117
|
+
serial(async () => {
|
|
118
|
+
const rows = await load();
|
|
119
|
+
const moved: string[] = [];
|
|
120
|
+
for (const threadId of new Set(threadIds)) {
|
|
121
|
+
const current = rows.get(threadId);
|
|
122
|
+
if ((current?.[kind] ?? -Infinity) >= at) continue;
|
|
123
|
+
await save(rows, threadId, { ...current, [kind]: at });
|
|
124
|
+
moved.push(threadId);
|
|
125
|
+
}
|
|
126
|
+
if (moved.length > 0) publish({ kind, threadIds: moved, value: at });
|
|
127
|
+
}),
|
|
110
128
|
clear: (kind, threadIds) =>
|
|
111
129
|
serial(async () => {
|
|
112
130
|
if (threadIds.length === 0) return;
|
package/server.ts
CHANGED
|
@@ -51,6 +51,11 @@ export default function threadGlance(bb: BbPluginApi): void {
|
|
|
51
51
|
await stamps.stamp("seenAt", threadIds, at);
|
|
52
52
|
return { at };
|
|
53
53
|
},
|
|
54
|
+
async markIdle({ threadIds }) {
|
|
55
|
+
const at = Date.now();
|
|
56
|
+
await stamps.advance("idleAt", threadIds, at);
|
|
57
|
+
return { at };
|
|
58
|
+
},
|
|
54
59
|
async clearSeen({ threadIds }) {
|
|
55
60
|
await stamps.clear("seenAt", threadIds);
|
|
56
61
|
return { ok: true as const };
|
package/shared/contract.ts
CHANGED
|
@@ -56,6 +56,14 @@ export const rpcContract = defineRpcContract({
|
|
|
56
56
|
input: z.object({ threadIds: threadIdsSchema }).strict(),
|
|
57
57
|
output: z.object({ ok: z.literal(true) }).strict(),
|
|
58
58
|
},
|
|
59
|
+
/**
|
|
60
|
+
* Records `idleAt` for threads a window saw go from busy to idle. A thread
|
|
61
|
+
* keeps the later of its stored moment and this one.
|
|
62
|
+
*/
|
|
63
|
+
markIdle: {
|
|
64
|
+
input: z.object({ threadIds: threadIdsSchema }).strict(),
|
|
65
|
+
output: z.object({ at: z.number() }).strict(),
|
|
66
|
+
},
|
|
59
67
|
/** Notes per thread id (see `noteSchema`). */
|
|
60
68
|
listNotes: {
|
|
61
69
|
input: z.null(),
|
package/shared/signals.ts
CHANGED
|
@@ -11,6 +11,8 @@ export const stampsSchema = z.object({
|
|
|
11
11
|
finishedAt: stampMapSchema,
|
|
12
12
|
pendingAt: stampMapSchema,
|
|
13
13
|
seenAt: stampMapSchema,
|
|
14
|
+
/** When a window saw the thread go from busy to idle, the latest such moment. */
|
|
15
|
+
idleAt: stampMapSchema,
|
|
14
16
|
});
|
|
15
17
|
export type Stamps = z.infer<typeof stampsSchema>;
|
|
16
18
|
export type StampKind = keyof Stamps;
|
|
@@ -56,7 +56,7 @@ and `bb environment list`; a thread id is a `thr_…` id.
|
|
|
56
56
|
| `openSettledFolds` | Group ids whose settled fold is open | `[]` |
|
|
57
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
|
-
| `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` |
|
|
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 for 5 seconds since it failed); `everything` also counts every failed or unread child | `blocked` |
|
|
60
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` |
|
|
61
61
|
|
|
62
62
|
Row density and the need-you filter are kept per browser window, and the CLI
|