@a-t-h-i/bot-lobby 0.6.7 → 0.6.8
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 +132 -15
- package/package.json +1 -1
- package/prompts/master.md +15 -0
- package/prompts/panel.md +4 -0
- package/prompts/planner.md +5 -0
- package/prompts/pr-review.md +62 -0
- package/prompts/splitter.md +58 -0
- package/src/ask/dialog.ts +19 -5
- package/src/ask/state.ts +8 -2
- package/src/ask/tool.ts +3 -1
- package/src/ask/view.ts +6 -3
- package/src/classifier/instance.ts +6 -0
- package/src/classifier/knowledge.ts +158 -0
- package/src/classifier/review.ts +146 -0
- package/src/execution/workspace.ts +185 -0
- package/src/knowledge/edit.ts +109 -0
- package/src/knowledge/notes.ts +143 -0
- package/src/knowledge/selector.ts +1 -1
- package/src/knowledge/store.ts +16 -2
- package/src/lobby/ask.ts +42 -21
- package/src/lobby/issues.ts +1 -1
- package/src/lobby/knowledge.ts +179 -0
- package/src/lobby/layout.ts +95 -18
- package/src/lobby/planner.ts +199 -7
- package/src/lobby/pr-review.ts +360 -0
- package/src/lobby/pulls.ts +250 -0
- package/src/lobby/runtime.ts +115 -13
- package/src/lobby/split.ts +230 -0
- package/src/lobby/tabs/git.ts +162 -0
- package/src/lobby/tabs/knowledge.ts +135 -0
- package/src/lobby/tabs/plan.ts +3 -1
- package/src/lobby/tabs/tasks.ts +11 -2
- package/src/lobby/view.ts +432 -32
- package/src/master/master.ts +39 -13
- package/src/pi/commands.ts +12 -7
- package/src/pi/model-support.ts +9 -0
- package/src/pi/plan-checklist.ts +42 -0
- package/src/pi/route.ts +2 -1
- package/src/pi/settings-ui.ts +41 -1
- package/src/pi/start-flags.ts +14 -0
- package/src/pi/start-task.ts +56 -5
- package/src/pi/tools.ts +4 -2
- package/src/schemas/configuration.ts +32 -3
- package/src/schemas/task.ts +15 -0
- package/src/state/backlog.ts +27 -4
- package/src/state/metrics.ts +2 -2
- package/src/state/persistence.ts +5 -5
- package/src/text.ts +38 -0
- package/src/workflow/workflow.ts +11 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull requests through the `gh` CLI, for the Git tab: the repository's open
|
|
3
|
+
* pull requests, one with its files, reviews and comments, and its diff.
|
|
4
|
+
* Like issues, `gh` owns authentication and repository detection; every
|
|
5
|
+
* failure becomes one readable line.
|
|
6
|
+
*/
|
|
7
|
+
import { gh, type Exec } from "./issues.ts";
|
|
8
|
+
|
|
9
|
+
export type ChecksState = "passing" | "failing" | "pending";
|
|
10
|
+
|
|
11
|
+
export interface PullSummary {
|
|
12
|
+
number: number;
|
|
13
|
+
title: string;
|
|
14
|
+
author?: string;
|
|
15
|
+
/** The branch the pull request comes from, and the one it merges into. */
|
|
16
|
+
headRef: string;
|
|
17
|
+
baseRef: string;
|
|
18
|
+
draft: boolean;
|
|
19
|
+
updatedAt?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
additions: number;
|
|
22
|
+
deletions: number;
|
|
23
|
+
changedFiles: number;
|
|
24
|
+
/** `APPROVED`, `CHANGES_REQUESTED` or `REVIEW_REQUIRED`, when the repository asks for reviews. */
|
|
25
|
+
decision?: string;
|
|
26
|
+
checks?: ChecksState;
|
|
27
|
+
/** How many checks ran. */
|
|
28
|
+
checkCount: number;
|
|
29
|
+
labels: string[];
|
|
30
|
+
/** The head commit: a saved review is stale once it moves. */
|
|
31
|
+
headSha?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PullFile {
|
|
35
|
+
path: string;
|
|
36
|
+
additions: number;
|
|
37
|
+
deletions: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PullNote {
|
|
41
|
+
author?: string;
|
|
42
|
+
body: string;
|
|
43
|
+
at?: string;
|
|
44
|
+
/** A review's verdict: `APPROVED`, `CHANGES_REQUESTED`, `COMMENTED`. */
|
|
45
|
+
state?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PullDetail extends PullSummary {
|
|
49
|
+
body: string;
|
|
50
|
+
state?: string;
|
|
51
|
+
mergeable?: string;
|
|
52
|
+
files: PullFile[];
|
|
53
|
+
/** Reviews and comments, oldest first. */
|
|
54
|
+
notes: PullNote[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const LIST_FIELDS = "number,title,author,headRefName,baseRefName,isDraft,updatedAt,url,additions,deletions,changedFiles,reviewDecision,statusCheckRollup,labels,headRefOid";
|
|
58
|
+
const VIEW_FIELDS = `${LIST_FIELDS},body,state,mergeable,files,comments,reviews`;
|
|
59
|
+
export const PULL_LIMIT = 50;
|
|
60
|
+
/** Longest diff kept for a review: `gh pr diff` can be enormous. */
|
|
61
|
+
export const DIFF_CHARS = 200_000;
|
|
62
|
+
|
|
63
|
+
interface RawCheck {
|
|
64
|
+
status?: unknown;
|
|
65
|
+
conclusion?: unknown;
|
|
66
|
+
state?: unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface RawUser {
|
|
70
|
+
login?: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface RawPull {
|
|
74
|
+
number?: unknown;
|
|
75
|
+
title?: unknown;
|
|
76
|
+
body?: unknown;
|
|
77
|
+
state?: unknown;
|
|
78
|
+
url?: unknown;
|
|
79
|
+
updatedAt?: unknown;
|
|
80
|
+
isDraft?: unknown;
|
|
81
|
+
author?: RawUser | null;
|
|
82
|
+
headRefName?: unknown;
|
|
83
|
+
baseRefName?: unknown;
|
|
84
|
+
headRefOid?: unknown;
|
|
85
|
+
additions?: unknown;
|
|
86
|
+
deletions?: unknown;
|
|
87
|
+
changedFiles?: unknown;
|
|
88
|
+
reviewDecision?: unknown;
|
|
89
|
+
mergeable?: unknown;
|
|
90
|
+
statusCheckRollup?: RawCheck[] | null;
|
|
91
|
+
labels?: Array<{ name?: unknown }> | null;
|
|
92
|
+
files?: Array<{ path?: unknown; additions?: unknown; deletions?: unknown }> | null;
|
|
93
|
+
comments?: Array<{ author?: RawUser | null; body?: unknown; createdAt?: unknown }> | null;
|
|
94
|
+
reviews?: Array<{ author?: RawUser | null; body?: unknown; submittedAt?: unknown; state?: unknown }> | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function str(value: unknown): string | undefined {
|
|
98
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function count(value: unknown): number {
|
|
102
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const FAILED = new Set(["FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STARTUP_FAILURE", "ERROR"]);
|
|
106
|
+
const PASSED = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
|
|
107
|
+
|
|
108
|
+
/** The checks as one state: any failure fails, any unfinished check is pending, otherwise passing; none at all, undefined. */
|
|
109
|
+
export function checksOf(rollup: readonly RawCheck[] | null | undefined): { state?: ChecksState; count: number } {
|
|
110
|
+
const checks = rollup ?? [];
|
|
111
|
+
if (checks.length === 0) return { count: 0 };
|
|
112
|
+
let pending = false;
|
|
113
|
+
for (const check of checks) {
|
|
114
|
+
// A check run has a status and a conclusion; a commit status just a state.
|
|
115
|
+
const verdict = String(check.conclusion || check.state || "").toUpperCase();
|
|
116
|
+
if (FAILED.has(verdict)) return { state: "failing", count: checks.length };
|
|
117
|
+
if (!PASSED.has(verdict)) pending = true;
|
|
118
|
+
}
|
|
119
|
+
return { state: pending ? "pending" : "passing", count: checks.length };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function summary(raw: RawPull): PullSummary | undefined {
|
|
123
|
+
if (typeof raw.number !== "number" || typeof raw.title !== "string") return undefined;
|
|
124
|
+
const author = str(raw.author?.login);
|
|
125
|
+
const updatedAt = str(raw.updatedAt);
|
|
126
|
+
const url = str(raw.url);
|
|
127
|
+
const decision = str(raw.reviewDecision);
|
|
128
|
+
const headSha = str(raw.headRefOid);
|
|
129
|
+
const checks = checksOf(raw.statusCheckRollup);
|
|
130
|
+
return {
|
|
131
|
+
number: raw.number,
|
|
132
|
+
title: raw.title,
|
|
133
|
+
headRef: str(raw.headRefName) ?? "",
|
|
134
|
+
baseRef: str(raw.baseRefName) ?? "",
|
|
135
|
+
draft: raw.isDraft === true,
|
|
136
|
+
additions: count(raw.additions),
|
|
137
|
+
deletions: count(raw.deletions),
|
|
138
|
+
changedFiles: count(raw.changedFiles),
|
|
139
|
+
checkCount: checks.count,
|
|
140
|
+
labels: (raw.labels ?? []).map((label) => str(label?.name)).filter((name): name is string => Boolean(name)),
|
|
141
|
+
...(author ? { author } : {}),
|
|
142
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
143
|
+
...(url ? { url } : {}),
|
|
144
|
+
...(decision ? { decision } : {}),
|
|
145
|
+
...(checks.state ? { checks: checks.state } : {}),
|
|
146
|
+
...(headSha ? { headSha } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseJson<T>(stdout: string): T {
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(stdout) as T;
|
|
153
|
+
} catch {
|
|
154
|
+
throw new Error("gh returned output bot-lobby could not read");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function listPulls(exec: Exec, cwd: string, limit = PULL_LIMIT): Promise<PullSummary[]> {
|
|
159
|
+
const stdout = await gh(exec, cwd, ["pr", "list", "--state", "open", "--limit", String(limit), "--json", LIST_FIELDS]);
|
|
160
|
+
const raw = parseJson<RawPull[]>(stdout);
|
|
161
|
+
return (Array.isArray(raw) ? raw : []).map(summary).filter((pull): pull is PullSummary => Boolean(pull));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function viewPull(exec: Exec, cwd: string, number: number): Promise<PullDetail> {
|
|
165
|
+
const stdout = await gh(exec, cwd, ["pr", "view", String(number), "--json", VIEW_FIELDS]);
|
|
166
|
+
const raw = parseJson<RawPull>(stdout);
|
|
167
|
+
const base = summary(raw);
|
|
168
|
+
if (!base) throw new Error(`gh returned no pull request #${number}`);
|
|
169
|
+
const state = str(raw.state);
|
|
170
|
+
const mergeable = str(raw.mergeable);
|
|
171
|
+
type Loose = { author?: string | undefined; body: string; at?: string | undefined; state?: string | undefined };
|
|
172
|
+
const notes: PullNote[] = ([
|
|
173
|
+
...(raw.reviews ?? []).map((review) => ({ author: str(review?.author?.login), body: typeof review?.body === "string" ? review.body : "", at: str(review?.submittedAt), state: str(review?.state) })),
|
|
174
|
+
...(raw.comments ?? []).map((comment) => ({ author: str(comment?.author?.login), body: typeof comment?.body === "string" ? comment.body : "", at: str(comment?.createdAt) })),
|
|
175
|
+
] as Loose[])
|
|
176
|
+
// A review with a verdict but no words still says something; a bare comment does not.
|
|
177
|
+
.filter((note) => note.body.trim() || (note.state && note.state !== "COMMENTED"))
|
|
178
|
+
.sort((a, b) => (a.at ?? "").localeCompare(b.at ?? ""))
|
|
179
|
+
.map(({ author, body, at, state: verdict }) => ({ body, ...(author ? { author } : {}), ...(at ? { at } : {}), ...(verdict ? { state: verdict } : {}) }));
|
|
180
|
+
return {
|
|
181
|
+
...base,
|
|
182
|
+
body: typeof raw.body === "string" ? raw.body : "",
|
|
183
|
+
...(state ? { state } : {}),
|
|
184
|
+
...(mergeable ? { mergeable } : {}),
|
|
185
|
+
files: (raw.files ?? []).flatMap((file) => (typeof file?.path === "string" ? [{ path: file.path, additions: count(file.additions), deletions: count(file.deletions) }] : [])),
|
|
186
|
+
notes,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The pull request's diff as `gh` prints it, bounded. */
|
|
191
|
+
export async function pullDiff(exec: Exec, cwd: string, number: number, limit = DIFF_CHARS): Promise<string> {
|
|
192
|
+
const stdout = await gh(exec, cwd, ["pr", "diff", String(number), "--color", "never"]);
|
|
193
|
+
return stdout.length > limit ? `${stdout.slice(0, limit)}\n[…the diff continues: ${stdout.length - limit} more characters]` : stdout;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Lobby state for the Git tab: the list, the open pull request, and what is loading. */
|
|
197
|
+
export class PullsState {
|
|
198
|
+
pulls: PullSummary[] = [];
|
|
199
|
+
details = new Map<number, PullDetail>();
|
|
200
|
+
loading = false;
|
|
201
|
+
loaded = false;
|
|
202
|
+
error?: string;
|
|
203
|
+
private readonly exec: Exec;
|
|
204
|
+
private readonly cwd: string;
|
|
205
|
+
private readonly onChange: () => void;
|
|
206
|
+
|
|
207
|
+
constructor(exec: Exec, cwd: string, onChange: () => void = () => {}) {
|
|
208
|
+
this.exec = exec;
|
|
209
|
+
this.cwd = cwd;
|
|
210
|
+
this.onChange = onChange;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async refresh(): Promise<void> {
|
|
214
|
+
if (this.loading) return;
|
|
215
|
+
this.loading = true;
|
|
216
|
+
this.error = undefined;
|
|
217
|
+
this.onChange();
|
|
218
|
+
try {
|
|
219
|
+
this.pulls = await listPulls(this.exec, this.cwd);
|
|
220
|
+
this.loaded = true;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.error = (error as Error).message;
|
|
223
|
+
} finally {
|
|
224
|
+
this.loading = false;
|
|
225
|
+
this.onChange();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The pull request with its files and discussion; cached until the list is reloaded. */
|
|
230
|
+
async detail(number: number): Promise<PullDetail | undefined> {
|
|
231
|
+
const cached = this.details.get(number);
|
|
232
|
+
if (cached) return cached;
|
|
233
|
+
try {
|
|
234
|
+
const detail = await viewPull(this.exec, this.cwd, number);
|
|
235
|
+
this.details.set(number, detail);
|
|
236
|
+
this.error = undefined;
|
|
237
|
+
this.onChange();
|
|
238
|
+
return detail;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.error = (error as Error).message;
|
|
241
|
+
this.onChange();
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Forget what was read, so the next look asks `gh` again. */
|
|
247
|
+
forget(): void {
|
|
248
|
+
this.details.clear();
|
|
249
|
+
}
|
|
250
|
+
}
|
package/src/lobby/runtime.ts
CHANGED
|
@@ -21,9 +21,12 @@ import { discardPlannedTask, listPlannedTasks, type PlannedTask } from "../state
|
|
|
21
21
|
import { appendMetrics, readClassifierMetrics, readMetrics, type MetricStatus } from "../state/metrics.ts";
|
|
22
22
|
import { describeToolCall } from "../pi/activity.ts";
|
|
23
23
|
import { applyStatus, currentZenTask, isMinimized, onMinimizeChange, onRunUpdates, setMinimized, taskSnapshot } from "../pi/ui.ts";
|
|
24
|
-
import {
|
|
24
|
+
import { taskName } from "../text.ts";
|
|
25
|
+
import { describeWorkspace, type WorkspaceInfo } from "../execution/workspace.ts";
|
|
26
|
+
import { basename } from "node:path";
|
|
27
|
+
import { stripStartFlags } from "../pi/start-flags.ts";
|
|
25
28
|
import { isSubagentProcess } from "../pi/quiet.ts";
|
|
26
|
-
import { modelRef, resolveLobbyProfile, resolvePanelProfile } from "../pi/model-support.ts";
|
|
29
|
+
import { modelRef, resolveLobbyProfile, resolvePanelProfile, resolveReviewProfile } from "../pi/model-support.ts";
|
|
27
30
|
import { modelLookup } from "../pi/tools.ts";
|
|
28
31
|
import { startPlannedTask } from "../pi/start-task.ts";
|
|
29
32
|
import { pendingRequest, setQuickFixHandoff, startRequest } from "../pi/route.ts";
|
|
@@ -34,11 +37,14 @@ import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
|
34
37
|
import { checkThinking } from "../pi/model-support.ts";
|
|
35
38
|
import { launchPi, SessionRegistry, type BackgroundSession, type SessionLauncher } from "./sessions.ts";
|
|
36
39
|
import { SessionChats } from "./session-files.ts";
|
|
37
|
-
import { answerMessage, askUser, questionnaires, type Asker } from "./ask.ts";
|
|
40
|
+
import { answerMessage, askUser, questionnaires, settledQuestions, type Asker } from "./ask.ts";
|
|
38
41
|
import { jobTitle, QuickFixQueue } from "./quickfix.ts";
|
|
39
42
|
import { miniLine, type MiniInput } from "./mini.ts";
|
|
40
43
|
import { PlanningSession, type PlannerSeed } from "./planner.ts";
|
|
41
44
|
import { execCommand, IssuesState } from "./issues.ts";
|
|
45
|
+
import { PullsState } from "./pulls.ts";
|
|
46
|
+
import { PullReviews } from "./pr-review.ts";
|
|
47
|
+
import { KnowledgeBook } from "./knowledge.ts";
|
|
42
48
|
import { LobbyView, type LiveSession, type LobbyHost, type SwitchTarget, type TabId } from "./view.ts";
|
|
43
49
|
import { lobbyTheme } from "./theme.ts";
|
|
44
50
|
import { deliverComments, onOwnerEvent, setAuto } from "../pi/owner.ts";
|
|
@@ -61,6 +67,9 @@ interface Runtime {
|
|
|
61
67
|
quickfix: QuickFixQueue;
|
|
62
68
|
planner?: PlanningSession;
|
|
63
69
|
issues: IssuesState;
|
|
70
|
+
pulls: PullsState;
|
|
71
|
+
reviews: PullReviews;
|
|
72
|
+
knowledge: KnowledgeBook;
|
|
64
73
|
unsubscribeFeed?: () => void;
|
|
65
74
|
/** Puts the panel's questions to the user: the questionnaire unless a test sets another. */
|
|
66
75
|
asker?: Asker;
|
|
@@ -70,6 +79,9 @@ interface Runtime {
|
|
|
70
79
|
mouse: boolean;
|
|
71
80
|
/** Settings opened from the lobby are on screen; the lobby stays aside until they close. */
|
|
72
81
|
inSettings: boolean;
|
|
82
|
+
/** The repository (or folder) and branch the lobby's title shows, and whether git is being asked now. */
|
|
83
|
+
workspace: WorkspaceInfo;
|
|
84
|
+
readingWorkspace: boolean;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
let runtime: Runtime | undefined;
|
|
@@ -265,7 +277,8 @@ function startSession(state: Runtime, start: { request?: string; plan?: PlannedT
|
|
|
265
277
|
const starting = backgroundSessions().find((session) => session.alive && session.planId === plan.id);
|
|
266
278
|
if (starting) return `${plan.id} is already starting in ${starting.name}`;
|
|
267
279
|
}
|
|
268
|
-
|
|
280
|
+
// A request may lead with flags (`--worktree add login`); the session is named after what follows them.
|
|
281
|
+
const name = taskName(plan?.title ?? (stripStartFlags(request!) || request!));
|
|
269
282
|
try {
|
|
270
283
|
const session = sessionRegistry().start(state.ctx.cwd, { name, ...(plan ? { planId: plan.id } : { request: request! }), ...(start.auto ? { auto: true } : {}) }, sessionModel(state.ctx));
|
|
271
284
|
lobbyFeed.log("LOBBY", `started "${name}" in a new session`, "success");
|
|
@@ -445,6 +458,15 @@ function deleteTask(state: Runtime, taskId: string, where: "list" | "archive"):
|
|
|
445
458
|
return `deleted ${taskId} for good`;
|
|
446
459
|
}
|
|
447
460
|
|
|
461
|
+
/** What a pull request review runs on: QA's model, thinking and time limit. */
|
|
462
|
+
function reviewProfile(state: Runtime) {
|
|
463
|
+
return resolveReviewProfile(loadConfig(), {
|
|
464
|
+
lookup: modelLookup(state.ctx),
|
|
465
|
+
sessionModel: sessionModel(state.ctx),
|
|
466
|
+
warn: (message) => state.ctx.ui.notify(message, "warning"),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
448
470
|
function seatProfile(state: Runtime, member: PanelMember) {
|
|
449
471
|
return resolvePanelProfile(loadConfig(), member, {
|
|
450
472
|
lookup: modelLookup(state.ctx),
|
|
@@ -485,6 +507,26 @@ function panelAsker(state: Runtime): Asker {
|
|
|
485
507
|
return state.asker ?? askUser;
|
|
486
508
|
}
|
|
487
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Save the plan as a pending task. A plan with many steps is first split by
|
|
512
|
+
* the oracle into up to five tasks, which the user takes, changes or declines
|
|
513
|
+
* in a questionnaire; the split's questions open over the lobby like the
|
|
514
|
+
* panel's. Returns a notice for the lobby.
|
|
515
|
+
*/
|
|
516
|
+
export async function savePlan(state: Runtime | undefined = runtime): Promise<string> {
|
|
517
|
+
const session = state?.planner;
|
|
518
|
+
if (!state || !session) return "no planning session";
|
|
519
|
+
if (state.asking) return "a questionnaire is already open";
|
|
520
|
+
state.asking = true;
|
|
521
|
+
try {
|
|
522
|
+
const asker = panelAsker(state);
|
|
523
|
+
return await session.saveWithSplit((questions, signal) => asker(questions, state.ctx, signal), { splitAbove: loadConfig().lobby.splitPlanAbove });
|
|
524
|
+
} finally {
|
|
525
|
+
state.asking = false;
|
|
526
|
+
rerender();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
488
530
|
/**
|
|
489
531
|
* The oracle puts the round's questions to the user, one at a time; answered
|
|
490
532
|
* questionnaires are kept if the user stops, so the next call resumes there.
|
|
@@ -514,7 +556,7 @@ export async function answerPanel(state: Runtime | undefined = runtime): Promise
|
|
|
514
556
|
session.answered = [];
|
|
515
557
|
return "nothing was answered — the questions stay open";
|
|
516
558
|
}
|
|
517
|
-
void session.send(message);
|
|
559
|
+
void session.send(message, settledQuestions(session.questions, session.answered));
|
|
518
560
|
return "answers sent — the panel is on the next round";
|
|
519
561
|
} catch (error) {
|
|
520
562
|
return `could not put the questions: ${(error as Error).message}`;
|
|
@@ -524,6 +566,22 @@ export async function answerPanel(state: Runtime | undefined = runtime): Promise
|
|
|
524
566
|
}
|
|
525
567
|
}
|
|
526
568
|
|
|
569
|
+
/** Read the repository name and branch again, off the render path; the title repaints when they changed. */
|
|
570
|
+
function refreshWorkspace(state: Runtime): void {
|
|
571
|
+
if (state.readingWorkspace) return;
|
|
572
|
+
state.readingWorkspace = true;
|
|
573
|
+
describeWorkspace(state.ctx.cwd)
|
|
574
|
+
.then((info) => {
|
|
575
|
+
if (runtime !== state || (info.name === state.workspace.name && info.branch === state.workspace.branch)) return;
|
|
576
|
+
state.workspace = info;
|
|
577
|
+
rerender();
|
|
578
|
+
})
|
|
579
|
+
.catch(() => {})
|
|
580
|
+
.finally(() => {
|
|
581
|
+
state.readingWorkspace = false;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
527
585
|
function host(state: Runtime, tui: TUI): LobbyHost {
|
|
528
586
|
return {
|
|
529
587
|
rows: () => tui.terminal.rows,
|
|
@@ -550,6 +608,7 @@ function host(state: Runtime, tui: TUI): LobbyHost {
|
|
|
550
608
|
planner: () => state.planner,
|
|
551
609
|
newPlanner: (seed, seats) => newPlanner(state, seed, seats),
|
|
552
610
|
answerPanel: () => answerPanel(state),
|
|
611
|
+
savePlan: () => savePlan(state),
|
|
553
612
|
defaultPanel: () => loadConfig().lobby.planningPanel,
|
|
554
613
|
planningRounds: () => loadConfig().lobby.maxPlanningRounds,
|
|
555
614
|
issuesEnabled: () => loadConfig().lobby.issues,
|
|
@@ -562,10 +621,16 @@ function host(state: Runtime, tui: TUI): LobbyHost {
|
|
|
562
621
|
return `${profile.model ?? "session model"} · ${profile.thinking}`;
|
|
563
622
|
},
|
|
564
623
|
issues: state.issues,
|
|
624
|
+
pulls: state.pulls,
|
|
625
|
+
reviews: state.reviews,
|
|
626
|
+
knowledge: state.knowledge,
|
|
627
|
+
editText: (title, text) => editText(state, title, text),
|
|
565
628
|
profileLabel: (kind) => {
|
|
566
629
|
const profile = lobbyProfile(state, kind);
|
|
567
630
|
return `${profile.model ?? "session model"} · ${profile.thinking}`;
|
|
568
631
|
},
|
|
632
|
+
workspace: () => state.workspace,
|
|
633
|
+
refreshWorkspace: () => refreshWorkspace(state),
|
|
569
634
|
sessionName: () => state.pi.getSessionName(),
|
|
570
635
|
sessions: () => backgroundSessions(),
|
|
571
636
|
startSession: (start) => startSession(state, start),
|
|
@@ -588,20 +653,20 @@ function host(state: Runtime, tui: TUI): LobbyHost {
|
|
|
588
653
|
}
|
|
589
654
|
|
|
590
655
|
/**
|
|
591
|
-
*
|
|
592
|
-
* lobby
|
|
593
|
-
* not flash between them
|
|
656
|
+
* Run something pi draws itself (its settings menus, its multi-line editor)
|
|
657
|
+
* with the lobby stepped aside for the whole visit, not only for each dialog,
|
|
658
|
+
* so it does not flash between them; the lobby rereads the config after.
|
|
594
659
|
*/
|
|
595
|
-
async function
|
|
596
|
-
if (state.inSettings) return;
|
|
660
|
+
async function whileAside<T>(state: Runtime, run: () => Promise<T>, failure: string): Promise<T | undefined> {
|
|
661
|
+
if (state.inSettings) return undefined;
|
|
597
662
|
state.inSettings = true;
|
|
598
663
|
setMouse(state, false);
|
|
599
664
|
state.handle?.setHidden(true);
|
|
600
665
|
try {
|
|
601
|
-
|
|
602
|
-
else await openSettings(state.pi, state.ctx);
|
|
666
|
+
return await run();
|
|
603
667
|
} catch (error) {
|
|
604
|
-
state.ctx.ui.notify(`bot-lobby:
|
|
668
|
+
state.ctx.ui.notify(`bot-lobby: ${failure} — ${(error as Error).message}`, "warning");
|
|
669
|
+
return undefined;
|
|
605
670
|
} finally {
|
|
606
671
|
state.inSettings = false;
|
|
607
672
|
state.asideForPrompt = false;
|
|
@@ -615,6 +680,16 @@ async function lobbySettings(state: Runtime, entry?: "quickfix" | "planner"): Pr
|
|
|
615
680
|
}
|
|
616
681
|
}
|
|
617
682
|
|
|
683
|
+
/** bot-lobby's settings (or one agent's entry) from inside the lobby. */
|
|
684
|
+
async function lobbySettings(state: Runtime, entry?: "quickfix" | "planner"): Promise<void> {
|
|
685
|
+
await whileAside(state, () => (entry ? openEntrySettings(state.pi, state.ctx, entry) : openSettings(state.pi, state.ctx)), "settings failed");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** A text edited in pi's multi-line editor (Enter saves, Shift+Enter is a new line); undefined when cancelled. */
|
|
689
|
+
async function editText(state: Runtime, title: string, text: string): Promise<string | undefined> {
|
|
690
|
+
return whileAside(state, () => state.ctx.ui.editor(title, text), "the editor failed");
|
|
691
|
+
}
|
|
692
|
+
|
|
618
693
|
/** Remember which panes show, keeping every other setting as the file has it now. */
|
|
619
694
|
function savePanels(panels: Record<LobbyPanel, boolean>): void {
|
|
620
695
|
try {
|
|
@@ -718,6 +793,7 @@ function shutdown(): void {
|
|
|
718
793
|
setMouse(state, false);
|
|
719
794
|
state.unsubscribeFeed?.();
|
|
720
795
|
state.quickfix.cancelAll();
|
|
796
|
+
state.reviews.cancelAll();
|
|
721
797
|
state.planner?.cancel();
|
|
722
798
|
state.view?.dispose();
|
|
723
799
|
state.handle?.hide();
|
|
@@ -759,9 +835,35 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
759
835
|
asking: false,
|
|
760
836
|
mouse: false,
|
|
761
837
|
inSettings: false,
|
|
838
|
+
workspace: { name: basename(ctx.cwd) || ctx.cwd },
|
|
839
|
+
readingWorkspace: false,
|
|
762
840
|
quickfix: undefined as unknown as QuickFixQueue,
|
|
763
841
|
issues: new IssuesState(execCommand, ctx.cwd, rerender),
|
|
842
|
+
pulls: new PullsState(execCommand, ctx.cwd, rerender),
|
|
843
|
+
reviews: undefined as unknown as PullReviews,
|
|
844
|
+
knowledge: new KnowledgeBook({
|
|
845
|
+
root,
|
|
846
|
+
configDir,
|
|
847
|
+
threshold: () => loadConfig().knowledge.compactionThreshold,
|
|
848
|
+
backups: () => loadConfig().knowledge.backupCount,
|
|
849
|
+
sessionId: () => ctx.sessionManager.getSessionId(),
|
|
850
|
+
}),
|
|
764
851
|
};
|
|
852
|
+
state.reviews = new PullReviews({
|
|
853
|
+
cwd: ctx.cwd,
|
|
854
|
+
root,
|
|
855
|
+
configDir,
|
|
856
|
+
exec: execCommand,
|
|
857
|
+
profile: () => reviewProfile(state),
|
|
858
|
+
stallTimeoutMs: workflow.stallTimeoutMs,
|
|
859
|
+
toolStallTimeoutMs: workflow.toolStallTimeoutMs,
|
|
860
|
+
feed: lobbyFeed,
|
|
861
|
+
onChange: rerender,
|
|
862
|
+
notify: (message, level) => {
|
|
863
|
+
if (!state.visible) ctx.ui.notify(message, level);
|
|
864
|
+
},
|
|
865
|
+
classifier: classifier(),
|
|
866
|
+
});
|
|
765
867
|
state.quickfix = new QuickFixQueue({
|
|
766
868
|
cwd: ctx.cwd,
|
|
767
869
|
root,
|