@gperezmz/bb-plugin-thread-glance 0.4.0 → 0.4.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/dist/app.js +28 -28
- package/dist/app.meta.json +1 -1
- package/dist/server.js.map +1 -1
- package/dist/server.meta.json +1 -1
- package/features/thread-list/components/ThreadList.tsx +2 -11
- package/features/thread-list/model/settled.ts +4 -66
- package/features/thread-list/testing/fixtures.ts +1 -7
- package/package.json +1 -1
- package/skills/thread-glance/SKILL.md +1 -1
- package/features/thread-list/components/PullRequestProbes.tsx +0 -27
- package/features/thread-list/data/usePullRequestAnswers.ts +0 -36
package/dist/server.meta.json
CHANGED
|
@@ -39,9 +39,7 @@ import { moveGroup, ORDER_PREFERENCE } from "../model/groups";
|
|
|
39
39
|
import { MARK_ALL_CONFIRM_ABOVE, type RowMenuAction } from "../model/menu";
|
|
40
40
|
import { assignProviderMarks, providerMark } from "../model/provider-mark";
|
|
41
41
|
import { markAllReadPlan, markReadPlanFor, toggleChip, toggleGroup, toggleOlder, toggleSettled, type ToggleOutcome } from "../model/toggles";
|
|
42
|
-
import {
|
|
43
|
-
import { usePullRequestAnswers } from "../data/usePullRequestAnswers";
|
|
44
|
-
import { PullRequestProbes } from "./PullRequestProbes";
|
|
42
|
+
import type { SettleInputs } from "../model/settled";
|
|
45
43
|
import { buildListView, countNeedYou, needYouActive, type GroupView, type ListView } from "../model/view";
|
|
46
44
|
import { shareView } from "../model/share";
|
|
47
45
|
import { ListLiveContext, type ListLive, type ModelInfo, type RowController } from "./controller";
|
|
@@ -174,21 +172,15 @@ function ThreadListBody({
|
|
|
174
172
|
(thread: PluginSidebarThread) => (defaultBranches.has(thread.projectId) ? (defaultBranches.get(thread.projectId) ?? null) : undefined),
|
|
175
173
|
[defaultBranches],
|
|
176
174
|
);
|
|
177
|
-
const [pullRequests, onPullRequest] = usePullRequestAnswers();
|
|
178
175
|
const system = useSystemFacts();
|
|
179
|
-
const pullRequestLookups = useMemo(
|
|
180
|
-
() => (forest === null ? [] : pullRequestLookupIds(forest.infos.values(), defaultBranchOf)),
|
|
181
|
-
[forest, defaultBranchOf],
|
|
182
|
-
);
|
|
183
176
|
const settle: SettleInputs = useMemo(
|
|
184
177
|
() => ({
|
|
185
178
|
now,
|
|
186
179
|
settleAfter: prefs.settleAfter,
|
|
187
180
|
startedAt: stamps.startedAt,
|
|
188
181
|
finishedAt: stamps.finishedAt,
|
|
189
|
-
pullRequestOf: (thread) => pullRequestFact(thread, defaultBranchOf(thread), pullRequests),
|
|
190
182
|
}),
|
|
191
|
-
[now, prefs.settleAfter, stamps.startedAt, stamps.finishedAt
|
|
183
|
+
[now, prefs.settleAfter, stamps.startedAt, stamps.finishedAt],
|
|
192
184
|
);
|
|
193
185
|
// Rows and groups that did not change keep their objects, so their
|
|
194
186
|
// memoized components skip the render.
|
|
@@ -723,7 +715,6 @@ function ThreadListBody({
|
|
|
723
715
|
onPrefs={update}
|
|
724
716
|
onClient={updateClient}
|
|
725
717
|
/>
|
|
726
|
-
<PullRequestProbes threadIds={pullRequestLookups} onAnswer={onPullRequest} />
|
|
727
718
|
{threads.length === 0 ? (
|
|
728
719
|
// bb's own pinned New thread button covers the empty list.
|
|
729
720
|
<p className="px-3 py-4 text-sm text-muted-foreground">No threads yet.</p>
|
|
@@ -15,46 +15,12 @@ export const SETTLE_AFTER_MS: Readonly<Record<Exclude<SettleAfter, "never">, num
|
|
|
15
15
|
"1w": 7 * 24 * HOUR,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
/** A pull request's state, as bb's lookup reports it. */
|
|
19
|
-
export type PullRequestState = "open" | "draft" | "merged" | "closed";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* What settling knows of a thread's pull request: its state, null when the
|
|
23
|
-
* thread has none, or `unknown` while a lookup it needs has not answered.
|
|
24
|
-
*/
|
|
25
|
-
export type PullRequestFact = PullRequestState | null | "unknown";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Whether a thread's branch can carry a pull request worth looking up: it is
|
|
29
|
-
* on a branch other than its project's default. A default branch the lookup
|
|
30
|
-
* could not find (null) is no reason to skip one; one still being looked up
|
|
31
|
-
* (undefined) is, until it answers.
|
|
32
|
-
*/
|
|
33
|
-
export function needsPullRequestLookup(branch: string | null, defaultBranch: string | null | undefined): boolean {
|
|
34
|
-
return branch !== null && defaultBranch !== undefined && branch !== defaultBranch;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A thread's pull request fact, from its project's default branch and the lookups that answered. */
|
|
38
|
-
export function pullRequestFact(
|
|
39
|
-
thread: Pick<PluginSidebarThread, "id" | "environment">,
|
|
40
|
-
defaultBranch: string | null | undefined,
|
|
41
|
-
answers: ReadonlyMap<string, PullRequestState | null>,
|
|
42
|
-
): PullRequestFact {
|
|
43
|
-
const branch = thread.environment?.branchName ?? null;
|
|
44
|
-
if (branch === null) return null;
|
|
45
|
-
if (defaultBranch === undefined) return "unknown";
|
|
46
|
-
if (!needsPullRequestLookup(branch, defaultBranch)) return null;
|
|
47
|
-
const answer = answers.get(thread.id);
|
|
48
|
-
return answer === undefined ? "unknown" : answer;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
18
|
export interface SettleInputs {
|
|
52
19
|
now: number;
|
|
53
20
|
settleAfter: SettleAfter;
|
|
54
21
|
/** Server stamps: when each thread last started and finished a turn. */
|
|
55
22
|
startedAt: Readonly<Record<string, number>>;
|
|
56
23
|
finishedAt: Readonly<Record<string, number>>;
|
|
57
|
-
pullRequestOf(thread: PluginSidebarThread): PullRequestFact;
|
|
58
24
|
}
|
|
59
25
|
|
|
60
26
|
/**
|
|
@@ -77,46 +43,18 @@ export function lastActivityAt(
|
|
|
77
43
|
|
|
78
44
|
/**
|
|
79
45
|
* A settled thread: quiet as if no thread were open, not needing attention,
|
|
80
|
-
* not pinned,
|
|
81
|
-
*
|
|
82
|
-
* Hidden and archived threads take the same test.
|
|
46
|
+
* not pinned, and its own last activity is older than the Settle after
|
|
47
|
+
* period. Hidden and archived threads take the same test.
|
|
83
48
|
*/
|
|
84
49
|
export function isSettledThread(info: ThreadInfo, inputs: SettleInputs): boolean {
|
|
85
50
|
const thread = info.thread;
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
if (pullRequest === "unknown" || pullRequest === "open" || pullRequest === "draft") return false;
|
|
89
|
-
if (pullRequest === "merged" || pullRequest === "closed") return true;
|
|
51
|
+
if (thread.pinnedAt !== null || thread.isPinned) return false;
|
|
52
|
+
if (!isQuietThread(info.state, info.unread, false) || info.attentionFlags.size > 0) return false;
|
|
90
53
|
if (inputs.settleAfter === "never") return false;
|
|
91
54
|
return inputs.now - lastActivityAt(thread, inputs) >= SETTLE_AFTER_MS[inputs.settleAfter];
|
|
92
55
|
}
|
|
93
56
|
|
|
94
|
-
/** A row's thread that settles once its pull request and activity allow: quiet as if none were open, needing no attention, not pinned. */
|
|
95
|
-
function mightSettle(info: ThreadInfo): boolean {
|
|
96
|
-
const thread = info.thread;
|
|
97
|
-
if (thread.pinnedAt !== null || thread.isPinned) return false;
|
|
98
|
-
return isQuietThread(info.state, info.unread, false) && info.attentionFlags.size === 0;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
57
|
/** A tree settles as one unit: when every thread in it is settled. */
|
|
102
58
|
export function isSettledTree(tree: Pick<ThreadTree, "root" | "descendants">, inputs: SettleInputs): boolean {
|
|
103
59
|
return isSettledThread(tree.root, inputs) && tree.descendants.every((info) => isSettledThread(info, inputs));
|
|
104
60
|
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The threads whose pull request settling has to look up: every thread that
|
|
108
|
-
* would settle but for its pull request, on a branch other than its
|
|
109
|
-
* project's default.
|
|
110
|
-
*/
|
|
111
|
-
export function pullRequestLookupIds(
|
|
112
|
-
infos: Iterable<ThreadInfo>,
|
|
113
|
-
defaultBranchOf: (thread: PluginSidebarThread) => string | null | undefined,
|
|
114
|
-
): string[] {
|
|
115
|
-
const ids: string[] = [];
|
|
116
|
-
for (const info of infos) {
|
|
117
|
-
const thread = info.thread;
|
|
118
|
-
if (!mightSettle(info)) continue;
|
|
119
|
-
if (needsPullRequestLookup(thread.environment?.branchName ?? null, defaultBranchOf(thread))) ids.push(thread.id);
|
|
120
|
-
}
|
|
121
|
-
return ids;
|
|
122
|
-
}
|
|
@@ -8,7 +8,7 @@ import { defaultPreferences, type Preferences } from "@/shared/preferences";
|
|
|
8
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
|
|
11
|
+
import type { SettleInputs } from "../model/settled";
|
|
12
12
|
|
|
13
13
|
export const T0 = 1_780_000_000_000;
|
|
14
14
|
|
|
@@ -125,8 +125,6 @@ export interface Scenario {
|
|
|
125
125
|
needYouOnly?: boolean;
|
|
126
126
|
/** bb's default harness; "claude-code", the fixtures' own, when absent. */
|
|
127
127
|
defaultProviderId?: string | null;
|
|
128
|
-
/** Pull request lookups that answered, by thread id. */
|
|
129
|
-
pullRequests?: Record<string, PullRequestState | null>;
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
export function forestOf(scenario: Scenario): Forest {
|
|
@@ -165,15 +163,11 @@ export function viewOf(scenario: Scenario): ListView {
|
|
|
165
163
|
|
|
166
164
|
/** The settle inputs a scenario stands for. */
|
|
167
165
|
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
166
|
return {
|
|
172
167
|
now: scenario.now ?? T0 + 60_000,
|
|
173
168
|
settleAfter: scenario.prefs?.settleAfter ?? defaultPreferences().settleAfter,
|
|
174
169
|
startedAt: scenario.startedAt ?? {},
|
|
175
170
|
finishedAt: scenario.finishedAt ?? {},
|
|
176
|
-
pullRequestOf: (thread) => pullRequestFact(thread, defaultBranchOf(thread.projectId), answers),
|
|
177
171
|
};
|
|
178
172
|
}
|
|
179
173
|
|
package/package.json
CHANGED
|
@@ -52,7 +52,7 @@ 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
|
-
| `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
|
|
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 nothing settles) | `1d` |
|
|
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 | `[]` |
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// Looks up the pull request of every thread settling has to know about and
|
|
2
|
-
// reports each answer. Draws nothing: the host shares one lookup per
|
|
3
|
-
// environment with the badges that ask for the same threads.
|
|
4
|
-
import { memo, useEffect } from "react";
|
|
5
|
-
import { experimental_useSidebarThreadPullRequest as usePullRequest } from "@get-bb/plugin-sdk/app";
|
|
6
|
-
import type { PullRequestState } from "../model/settled";
|
|
7
|
-
|
|
8
|
-
type Answer = (threadId: string, state: PullRequestState | null) => void;
|
|
9
|
-
|
|
10
|
-
const Probe = memo(function Probe({ threadId, onAnswer }: { threadId: string; onAnswer: Answer }) {
|
|
11
|
-
const { isLoading, pullRequest } = usePullRequest(threadId);
|
|
12
|
-
const state = isLoading ? undefined : (pullRequest?.state ?? null);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
if (state !== undefined) onAnswer(threadId, state);
|
|
15
|
-
}, [threadId, state, onAnswer]);
|
|
16
|
-
return null;
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export function PullRequestProbes({ threadIds, onAnswer }: { threadIds: readonly string[]; onAnswer: Answer }) {
|
|
20
|
-
return (
|
|
21
|
-
<>
|
|
22
|
-
{threadIds.map((threadId) => (
|
|
23
|
-
<Probe key={threadId} threadId={threadId} onAnswer={onAnswer} />
|
|
24
|
-
))}
|
|
25
|
-
</>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// The pull request lookups that answered, by thread id. Answers arriving in
|
|
2
|
-
// one burst (a list's worth of lookups on load) land in one state update.
|
|
3
|
-
//
|
|
4
|
-
// Debt: this copies server state the host's per-row pull request hook owns,
|
|
5
|
-
// because settling reads every thread's pull request at once and the SDK
|
|
6
|
-
// offers no query over many threads. It clears when the SDK does.
|
|
7
|
-
import { useCallback, useRef, useState } from "react";
|
|
8
|
-
import type { PullRequestState } from "../model/settled";
|
|
9
|
-
|
|
10
|
-
export type PullRequestAnswers = ReadonlyMap<string, PullRequestState | null>;
|
|
11
|
-
|
|
12
|
-
export function usePullRequestAnswers(): [PullRequestAnswers, (threadId: string, state: PullRequestState | null) => void] {
|
|
13
|
-
const [answers, setAnswers] = useState<PullRequestAnswers>(() => new Map());
|
|
14
|
-
const pending = useRef(new Map<string, PullRequestState | null>());
|
|
15
|
-
const scheduled = useRef(false);
|
|
16
|
-
const onAnswer = useCallback((threadId: string, state: PullRequestState | null) => {
|
|
17
|
-
pending.current.set(threadId, state);
|
|
18
|
-
if (scheduled.current) return;
|
|
19
|
-
scheduled.current = true;
|
|
20
|
-
setTimeout(() => {
|
|
21
|
-
scheduled.current = false;
|
|
22
|
-
const batch = pending.current;
|
|
23
|
-
pending.current = new Map();
|
|
24
|
-
setAnswers((current) => {
|
|
25
|
-
let next: Map<string, PullRequestState | null> | null = null;
|
|
26
|
-
for (const [id, value] of batch) {
|
|
27
|
-
if (current.has(id) && current.get(id) === value) continue;
|
|
28
|
-
next ??= new Map(current);
|
|
29
|
-
next.set(id, value);
|
|
30
|
-
}
|
|
31
|
-
return next ?? current;
|
|
32
|
-
});
|
|
33
|
-
}, 0);
|
|
34
|
-
}, []);
|
|
35
|
-
return [answers, onAnswer];
|
|
36
|
-
}
|