@a-t-h-i/bot-lobby 0.6.24 → 0.6.26
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 +11 -3
- package/package.json +1 -5
- package/prompts/designer.md +14 -0
- package/prompts/master.md +30 -7
- package/prompts/panel.md +21 -1
- package/prompts/planner.md +22 -3
- package/prompts/qa.md +23 -3
- package/prompts/quickfix.md +4 -0
- package/prompts/reviewer.md +17 -0
- package/prompts/worker.md +21 -6
- package/src/ask/tool.ts +3 -3
- package/src/classifier/hosts.ts +0 -11
- package/src/classifier/instance.ts +0 -5
- package/src/delivery/review.ts +1 -1
- package/src/execution/agent-runner.ts +0 -18
- package/src/execution/git.ts +77 -0
- package/src/execution/lint.ts +435 -0
- package/src/knowledge/compactor.ts +0 -70
- package/src/lobby/ask.ts +4 -17
- package/src/lobby/feed.ts +39 -4
- package/src/lobby/host.ts +4 -0
- package/src/lobby/issues.ts +0 -8
- package/src/lobby/planner.ts +54 -5
- package/src/lobby/pr-review.ts +2 -1
- package/src/lobby/pulls.ts +1 -44
- package/src/lobby/quickfix.ts +34 -1
- package/src/lobby/runtime.ts +20 -3
- package/src/lobby/service.ts +5 -1
- package/src/lobby/session-files.ts +1 -45
- package/src/lobby/split.ts +33 -1
- package/src/lobby/task-rows.ts +2 -0
- package/src/master/master.ts +3 -0
- package/src/master/research.ts +0 -16
- package/src/pi/commands.ts +25 -10
- package/src/pi/events.ts +12 -4
- package/src/pi/model-settings.ts +0 -1
- package/src/pi/plan-checklist.ts +98 -8
- package/src/pi/route.ts +0 -1
- package/src/pi/run-summary.ts +0 -57
- package/src/pi/switch.ts +69 -0
- package/src/pi/tools.ts +7 -0
- package/src/pi/ui.ts +2 -14
- package/src/schemas/agent.ts +0 -4
- package/src/schemas/configuration.ts +51 -3
- package/src/schemas/lint.ts +55 -0
- package/src/schemas/task.ts +5 -0
- package/src/state/budget.ts +49 -22
- package/src/state/changes.ts +8 -2
- package/src/state/metrics.ts +0 -1
- package/src/state/persistence.ts +0 -5
- package/src/state/work-time.ts +123 -0
- package/src/webui/api/index.ts +2 -6
- package/src/webui/api/knowledge.ts +11 -3
- package/src/webui/api/lobby.ts +13 -6
- package/src/webui/api/plan-facts.ts +36 -6
- package/src/webui/api/settings.ts +6 -1
- package/src/webui/api/tasks.ts +27 -6
- package/src/webui/command.ts +14 -3
- package/src/webui/dev/fake-service.ts +7 -0
- package/src/webui/dev/fixtures.ts +4 -1
- package/src/webui/protocol.ts +30 -4
- package/src/workflow/lint.ts +221 -0
- package/src/workflow/track.ts +12 -4
- package/src/workflow/workflow.ts +105 -21
- package/webui/dist/assets/index-C1IwecNS.css +2 -0
- package/webui/dist/assets/index-C4Ww36TW.js +105 -0
- package/webui/dist/build.json +58 -62
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-BSewK1QO.css +0 -2
- package/webui/dist/assets/index-CHQYSZXA.js +0 -105
package/src/lobby/planner.ts
CHANGED
|
@@ -61,8 +61,13 @@ const MEMBER_SEATS: Record<PanelMember, string> = {
|
|
|
61
61
|
export interface PanelOption {
|
|
62
62
|
label: string;
|
|
63
63
|
description: string;
|
|
64
|
+
/** A static HTML/CSS mockup of the option (a fenced `mockup` block under it), shown as a thumbnail the user expands. */
|
|
65
|
+
mockup?: { html: string; css?: string };
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/** Longest mockup kept per option; a longer one is dropped rather than cut mid-tag. */
|
|
69
|
+
export const MAX_MOCKUP_CHARS = 30_000;
|
|
70
|
+
|
|
66
71
|
/** A question as a seat or the oracle wrote it, with the options it offers (recommended first). */
|
|
67
72
|
export interface AskedQuestion {
|
|
68
73
|
text: string;
|
|
@@ -273,7 +278,22 @@ export function questionItems(body: string | undefined): AskedQuestion[] {
|
|
|
273
278
|
if (!body) return [];
|
|
274
279
|
const items: AskedQuestion[] = [];
|
|
275
280
|
let current: AskedQuestion | undefined;
|
|
281
|
+
// A fenced `mockup` (or `html`) block under an option is that option's mockup, never more question text.
|
|
282
|
+
let fence: { option?: PanelOption | undefined; lines: string[]; marker: string } | undefined;
|
|
276
283
|
for (const line of body.split("\n")) {
|
|
284
|
+
if (fence) {
|
|
285
|
+
if (line.trim() === fence.marker) {
|
|
286
|
+
const html = dedent(fence.lines).trim();
|
|
287
|
+
if (fence.option && html && html.length <= MAX_MOCKUP_CHARS) fence.option.mockup = { html };
|
|
288
|
+
fence = undefined;
|
|
289
|
+
} else fence.lines.push(line);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const opening = /^\s*(`{3,}|~{3,})\s*(?:mockup|html)\s*$/i.exec(line);
|
|
293
|
+
if (opening) {
|
|
294
|
+
fence = { lines: [], marker: opening[1]!, ...(current?.options.length ? { option: current.options.at(-1) } : {}) };
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
277
297
|
const match = /^(\s*)(?:\d+[.)]|[-*])\s+(.*\S)\s*$/.exec(line);
|
|
278
298
|
if (match && (match[1]!.length === 0 || !current)) {
|
|
279
299
|
current = { text: match[2]!.replace(/\*\*/g, ""), options: [] };
|
|
@@ -289,6 +309,34 @@ export function questionItems(body: string | undefined): AskedQuestion[] {
|
|
|
289
309
|
return items.map(inlineOptions);
|
|
290
310
|
}
|
|
291
311
|
|
|
312
|
+
/** Lines with their shared indentation removed. */
|
|
313
|
+
function dedent(lines: readonly string[]): string {
|
|
314
|
+
const indents = lines.filter((line) => line.trim()).map((line) => /^\s*/.exec(line)![0].length);
|
|
315
|
+
const cut = indents.length ? Math.min(...indents) : 0;
|
|
316
|
+
return lines.map((line) => line.slice(cut)).join("\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const plainOption = (label: string): string => optionLabel({ label, description: "" }).toLowerCase();
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The oracle relays a seat's question in its own words, without repeating the
|
|
323
|
+
* seat's mockups: an option it keeps by label keeps the seat's mockup (from
|
|
324
|
+
* the seat it tags first, then any seat).
|
|
325
|
+
*/
|
|
326
|
+
export function carryMockups(questions: readonly PanelQuestion[], seatQuestions: readonly PanelQuestion[]): PanelQuestion[] {
|
|
327
|
+
const drawn = seatQuestions.flatMap((question) => question.options.filter((option) => option.mockup).map((option) => ({ from: question.from, label: plainOption(option.label), mockup: option.mockup! })));
|
|
328
|
+
if (drawn.length === 0) return [...questions];
|
|
329
|
+
return questions.map((question) => ({
|
|
330
|
+
...question,
|
|
331
|
+
options: question.options.map((option) => {
|
|
332
|
+
if (option.mockup) return option;
|
|
333
|
+
const label = plainOption(option.label);
|
|
334
|
+
const match = drawn.find((entry) => entry.label === label && entry.from === question.from) ?? drawn.find((entry) => entry.label === label);
|
|
335
|
+
return match ? { ...option, mockup: match.mockup } : option;
|
|
336
|
+
}),
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
|
|
292
340
|
/**
|
|
293
341
|
* The round's questions for the user. The oracle chooses them from its own
|
|
294
342
|
* and the seats' (tagged with the seat each serves) and decides the rest; only
|
|
@@ -296,7 +344,7 @@ export function questionItems(body: string | undefined): AskedQuestion[] {
|
|
|
296
344
|
* `MAX_QUESTIONS`, so a round is answered in one questionnaire.
|
|
297
345
|
*/
|
|
298
346
|
export function roundQuestions(reply: PlannerReply | undefined, seatQuestions: readonly PanelQuestion[]): PanelQuestion[] {
|
|
299
|
-
const chosen = reply ? reply.questions.map((question) => tagged(question, ORACLE_LABEL)) : [...seatQuestions];
|
|
347
|
+
const chosen = reply ? carryMockups(reply.questions.map((question) => tagged(question, ORACLE_LABEL)), seatQuestions) : [...seatQuestions];
|
|
300
348
|
return chosen.slice(0, MAX_QUESTIONS);
|
|
301
349
|
}
|
|
302
350
|
|
|
@@ -330,8 +378,8 @@ export function parseMemberReply(text: string): MemberReply {
|
|
|
330
378
|
return { status: ready ? "ready" : "open", questions, notes: listItems(parts.get("notes")) };
|
|
331
379
|
}
|
|
332
380
|
|
|
333
|
-
function optionLines(options: readonly PanelOption[]): string[] {
|
|
334
|
-
return options.map((option) => ` - ${option.label}${option.description ? ` — ${option.description}` : ""}`);
|
|
381
|
+
function optionLines(options: readonly PanelOption[], marks = false): string[] {
|
|
382
|
+
return options.map((option) => ` - ${option.label}${option.description ? ` — ${option.description}` : ""}${marks && option.mockup ? " [mockup]" : ""}`);
|
|
335
383
|
}
|
|
336
384
|
|
|
337
385
|
function questionLine(question: PanelQuestion, index: number): string {
|
|
@@ -429,7 +477,7 @@ export function panelSection(outcomes: readonly MemberOutcome[]): string {
|
|
|
429
477
|
const { status, questions, notes } = outcome.reply;
|
|
430
478
|
return [
|
|
431
479
|
`### ${label} — ${status === "ready" ? "READY" : "OPEN"}`,
|
|
432
|
-
questions.length > 0 ? `Questions for the user:\n${questions.map((question) => [`- ${question.text}`, ...optionLines(question.options)].join("\n")).join("\n")}` : "",
|
|
480
|
+
questions.length > 0 ? `Questions for the user:\n${questions.map((question) => [`- ${question.text}`, ...optionLines(question.options, true)].join("\n")).join("\n")}` : "",
|
|
433
481
|
notes.length > 0 ? `Notes:\n${notes.map((note) => `- ${note}`).join("\n")}` : "",
|
|
434
482
|
].filter(Boolean).join("\n");
|
|
435
483
|
});
|
|
@@ -798,7 +846,7 @@ export class PlanningSession {
|
|
|
798
846
|
const step = describeToolCall(event.toolName, event.args);
|
|
799
847
|
setStep(step);
|
|
800
848
|
this.deps.feed?.step(label, step, this.stepKey(label));
|
|
801
|
-
} else if (event.type === "thought") this.deps.feed?.
|
|
849
|
+
} else if (event.type === "thought") this.deps.feed?.liveThought(label, event.text, this.stepKey(label));
|
|
802
850
|
else if (event.type === "thinking" || event.type === "writing") setStep(event.type);
|
|
803
851
|
else return;
|
|
804
852
|
this.deps.onChange?.();
|
|
@@ -828,6 +876,7 @@ export class PlanningSession {
|
|
|
828
876
|
outcome = { status: "failed", output: "", error: (error as Error).message, usage: { input: 0, output: 0, cost: 0, turns: 0 } };
|
|
829
877
|
}
|
|
830
878
|
this.deps.feed?.end(this.stepKey(label), outcome.status !== "success");
|
|
879
|
+
this.deps.feed?.settleThought(this.stepKey(label));
|
|
831
880
|
const metric = planningMetric(kind, label, this.turns, startedAt, outcome.status, profile, outcome.model, outcome.usage);
|
|
832
881
|
appendMetrics(this.deps.root, this.deps.configDir, [routedFrom ? { ...metric, routedFrom } : metric]);
|
|
833
882
|
return outcome;
|
package/src/lobby/pr-review.ts
CHANGED
|
@@ -258,7 +258,7 @@ export class PullReviews {
|
|
|
258
258
|
|
|
259
259
|
private onEvent(review: PullReview, event: PiStreamEvent): void {
|
|
260
260
|
if (event.type === "tool_execution_start") this.step(review, describeToolCall(event.toolName, event.args));
|
|
261
|
-
else if (event.type === "thought") this.deps.feed?.
|
|
261
|
+
else if (event.type === "thought") this.deps.feed?.liveThought(REVIEW_SOURCE, event.text, `pr-${review.number}-${review.startedAt}`);
|
|
262
262
|
else if (event.type === "retry") this.step(review, `provider retry ${event.attempt}/${event.maxAttempts}`);
|
|
263
263
|
}
|
|
264
264
|
|
|
@@ -299,6 +299,7 @@ export class PullReviews {
|
|
|
299
299
|
const ok = review.status === "done";
|
|
300
300
|
const outcome = ok ? `reviewed #${review.number}${review.verdict ? `: ${review.verdict === "changes" ? "request changes" : review.verdict}` : ""}` : `review of #${review.number} ${review.status}${review.error ? ` — ${review.error.split("\n")[0]}` : ""}`;
|
|
301
301
|
this.deps.feed?.end(`pr-${review.number}-${review.startedAt}`, !ok);
|
|
302
|
+
this.deps.feed?.settleThought(`pr-${review.number}-${review.startedAt}`);
|
|
302
303
|
this.deps.feed?.log(REVIEW_SOURCE, outcome, ok ? "success" : review.status === "cancelled" ? "warning" : "error", review.finishedAt);
|
|
303
304
|
appendMetrics(this.deps.root, this.deps.configDir, [{
|
|
304
305
|
id: `review-${review.number}-${review.startedAt}`,
|
package/src/lobby/pulls.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Like issues, `gh` owns authentication and repository detection; every
|
|
5
5
|
* failure becomes one readable line.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { gh, type Exec } from "./issues.ts";
|
|
8
8
|
|
|
9
9
|
export type ChecksState = "passing" | "failing" | "pending";
|
|
10
10
|
|
|
@@ -193,49 +193,6 @@ export async function pullDiff(exec: Exec, cwd: string, number: number, limit =
|
|
|
193
193
|
return stdout.length > limit ? `${stdout.slice(0, limit)}\n[…the diff continues: ${stdout.length - limit} more characters]` : stdout;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
/** A pull request opened for a completed task's branch. */
|
|
197
|
-
export interface CreatedPull {
|
|
198
|
-
number: number;
|
|
199
|
-
url: string;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const PUSH_TIMEOUT_MS = 60_000;
|
|
203
|
-
|
|
204
|
-
async function run(exec: Exec, cwd: string, command: string, args: string[]): Promise<string> {
|
|
205
|
-
let result: Awaited<ReturnType<Exec>>;
|
|
206
|
-
try {
|
|
207
|
-
result = await exec(command, args, { cwd, timeout: PUSH_TIMEOUT_MS });
|
|
208
|
-
} catch (error) {
|
|
209
|
-
throw new Error(`${command} could not run — ${(error as Error).message}`);
|
|
210
|
-
}
|
|
211
|
-
if (result.code !== 0) throw new Error(result.stderr.trim() || `${command} exited with code ${result.code}`);
|
|
212
|
-
return result.stdout;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** `gh pr create` ends with the URL on its own last line; the number is its tail. */
|
|
216
|
-
function parseCreated(stdout: string): CreatedPull {
|
|
217
|
-
const url = stdout.trim().split("\n").map((line) => line.trim()).filter((line) => /^https?:\/\/\S+$/.test(line)).pop();
|
|
218
|
-
if (!url) throw new Error("gh pr create printed no pull request URL");
|
|
219
|
-
const match = /\/pull\/([1-9]\d*)$/.exec(url);
|
|
220
|
-
const number = Number(match?.[1]);
|
|
221
|
-
if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`gh pr create printed an unreadable URL: ${url}`);
|
|
222
|
-
return { number, url };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Push a finished task's branch and open its pull request, so the work lands
|
|
227
|
-
* in review without the oracle having to remember a second step.
|
|
228
|
-
*/
|
|
229
|
-
export async function createPullRequest(options: { exec?: Exec; cwd: string; branch: string; base: string; title: string; body: string }): Promise<CreatedPull> {
|
|
230
|
-
for (const ref of [options.branch, options.base]) {
|
|
231
|
-
if (!ref || ref.startsWith("-") || /[\s~^:?*\[\\]|\.\.|@\{|\/\//.test(ref) || /[/.]$/.test(ref)) throw new Error("invalid pull request branch");
|
|
232
|
-
}
|
|
233
|
-
const exec = options.exec ?? execCommand;
|
|
234
|
-
await run(exec, options.cwd, "git", ["push", "-u", "origin", options.branch]);
|
|
235
|
-
const stdout = await gh(exec, options.cwd, ["pr", "create", "--base", options.base, "--head", options.branch, "--title", options.title, "--body", options.body.slice(0, 60_000)]);
|
|
236
|
-
return parseCreated(stdout);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
196
|
/** Lobby state for the Git tab: the list, the open pull request, and what is loading. */
|
|
240
197
|
export class PullsState {
|
|
241
198
|
pulls: PullSummary[] = [];
|
package/src/lobby/quickfix.ts
CHANGED
|
@@ -16,6 +16,12 @@ import type { FileHinter } from "../classifier/files.ts";
|
|
|
16
16
|
import type { Classifier } from "../classifier/classifier.ts";
|
|
17
17
|
import { quickFixSize } from "../classifier/triage.ts";
|
|
18
18
|
import { fellShort, profileLabel, routeLabel, type EffortRoute, type EffortRouter } from "../classifier/effort.ts";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
21
|
+
import type { LintConfig } from "../schemas/configuration.ts";
|
|
22
|
+
import { repositoryTop } from "../execution/git.ts";
|
|
23
|
+
import type { LintExec } from "../execution/lint.ts";
|
|
24
|
+
import { lintFeed, runLintGate } from "../workflow/lint.ts";
|
|
19
25
|
|
|
20
26
|
/** A quick fix edits code, so it gets the full coding tool set. */
|
|
21
27
|
export const QUICK_FIX_TOOLS: readonly string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
@@ -97,6 +103,10 @@ export interface QuickFixDeps {
|
|
|
97
103
|
classifier?: Classifier;
|
|
98
104
|
/** Lowers thinking or the model for a request the classifier judges simple or trivial. */
|
|
99
105
|
effort?: EffortRouter;
|
|
106
|
+
/** The lint settings, read as each job finishes: a finished job's touched files are linted, for the activity log only. */
|
|
107
|
+
lint?: () => LintConfig;
|
|
108
|
+
/** Runs a linter (tests); the real one otherwise. */
|
|
109
|
+
lintExec?: LintExec;
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
export const MAX_JOBS = 30;
|
|
@@ -222,7 +232,7 @@ export class QuickFixQueue {
|
|
|
222
232
|
if (edits?.note(event.toolName, event.args)) job.files = edits.shown();
|
|
223
233
|
this.addStep(job, describeToolCall(event.toolName, event.args), now);
|
|
224
234
|
} else if (event.type === "turn_start") job.turns += 1;
|
|
225
|
-
else if (event.type === "thought") this.deps.feed?.
|
|
235
|
+
else if (event.type === "thought") this.deps.feed?.liveThought(QUICK_FIX_SOURCE, event.text, job.id, now);
|
|
226
236
|
else if (event.type === "retry") this.addStep(job, `provider retry ${event.attempt}/${event.maxAttempts}`, now);
|
|
227
237
|
else return;
|
|
228
238
|
this.changed();
|
|
@@ -360,14 +370,37 @@ export class QuickFixQueue {
|
|
|
360
370
|
});
|
|
361
371
|
}
|
|
362
372
|
this.deps.feed?.end(job.id, job.status !== "success");
|
|
373
|
+
this.deps.feed?.settleThought(job.id);
|
|
363
374
|
const ok = job.status === "success";
|
|
364
375
|
const outcome = ok ? "done" : `${job.status}${job.error ? ` — ${job.error.split("\n")[0]}` : ""}`;
|
|
365
376
|
this.deps.feed?.log(QUICK_FIX_SOURCE, `${outcome}: ${jobTitle(job)}`, ok ? "success" : job.status === "cancelled" ? "warning" : "error", job.finishedAt);
|
|
366
377
|
appendMetrics(this.deps.root, this.deps.configDir, [quickFixMetric(job)]);
|
|
367
378
|
this.deps.notify?.(`bot-lobby quick fix ${outcome}: ${jobTitle(job)}`, ok ? "info" : "warning");
|
|
379
|
+
if (ok && edited.length > 0) void this.lintAfter(job, edited);
|
|
368
380
|
this.changed();
|
|
369
381
|
this.pump();
|
|
370
382
|
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The lint gate's pass over what a quick fix touched (only problems on the
|
|
386
|
+
* lines it changed are its own): a line in the activity log, never a hold,
|
|
387
|
+
* as a quick fix has no gate. Lint failing to run says nothing.
|
|
388
|
+
*/
|
|
389
|
+
private async lintAfter(job: QuickFixJob, edited: readonly string[]): Promise<void> {
|
|
390
|
+
const config = this.deps.lint?.();
|
|
391
|
+
if (!config || config.mode === "off") return;
|
|
392
|
+
try {
|
|
393
|
+
const top = await repositoryTop(this.deps.cwd);
|
|
394
|
+
if (!top) return;
|
|
395
|
+
const files = edited.map((file) => relative(top, file).split("\\").join("/")).filter((file) => file && !file.startsWith("..") && !isAbsolute(file) && existsSync(join(top, file)));
|
|
396
|
+
const report = await runLintGate({ top, files, config, ...(this.deps.lintExec ? { exec: this.deps.lintExec } : {}) });
|
|
397
|
+
if (report.state === "skipped") return;
|
|
398
|
+
const line = lintFeed(job.id, report);
|
|
399
|
+
this.deps.feed?.log("LINT", line.text, line.kind);
|
|
400
|
+
} catch {
|
|
401
|
+
// Advisory: a quick fix is never held or failed for lint.
|
|
402
|
+
}
|
|
403
|
+
}
|
|
371
404
|
}
|
|
372
405
|
|
|
373
406
|
export function quickFixMetric(job: QuickFixJob): MetricRecord {
|
package/src/lobby/runtime.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
9
9
|
import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
10
10
|
import { appendMetrics, type MetricStatus } from "../state/metrics.ts";
|
|
11
11
|
import { describeToolCall } from "../pi/activity.ts";
|
|
12
|
-
import { currentZenTask, onRunUpdates } from "../pi/ui.ts";
|
|
12
|
+
import { currentZenTask, isMinimized, onRunUpdates } from "../pi/ui.ts";
|
|
13
13
|
import type { WorkspaceInfo } from "../execution/workspace.ts";
|
|
14
14
|
import { basename } from "node:path";
|
|
15
15
|
import { isSubagentProcess } from "../pi/quiet.ts";
|
|
@@ -20,7 +20,7 @@ import type { Domain } from "../schemas/agent.ts";
|
|
|
20
20
|
import { chatFromEntries, lobbyFeed, narrateEvent, type AgentEventLike } from "./feed.ts";
|
|
21
21
|
import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
22
22
|
import { checkThinking } from "../pi/model-support.ts";
|
|
23
|
-
import {
|
|
23
|
+
import { QuickFixQueue } from "./quickfix.ts";
|
|
24
24
|
import type { Asker } from "../ask/types.ts";
|
|
25
25
|
import type { PlanningSession } from "./planner.ts";
|
|
26
26
|
import { execCommand, IssuesState } from "./issues.ts";
|
|
@@ -77,6 +77,21 @@ export async function answerPanel(state: Runtime | undefined = runtime): Promise
|
|
|
77
77
|
return serviceAnswerPanel(state);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** What the lobby is running now (a quick fix, a review, a planning round), in words, or undefined. */
|
|
81
|
+
export function lobbyWorkRunning(): string | undefined {
|
|
82
|
+
const state = runtime;
|
|
83
|
+
if (!state) return undefined;
|
|
84
|
+
if (state.quickfix.running) return "A quick fix";
|
|
85
|
+
if (state.planner?.busy) return "A planning round";
|
|
86
|
+
if (state.reviews.busy) return "A pull request review";
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Stop the lobby's backend (bot-lobby turned off, or the session ended). */
|
|
91
|
+
export function stopLobbyService(): void {
|
|
92
|
+
shutdown();
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
function shutdown(): void {
|
|
81
96
|
const state = runtime;
|
|
82
97
|
if (!state) return;
|
|
@@ -165,6 +180,7 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
|
|
|
165
180
|
hints: hintsFor({ cwd: ctx.cwd, root, configDir }),
|
|
166
181
|
classifier: classifier(),
|
|
167
182
|
effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
|
|
183
|
+
lint: () => loadConfig().lint,
|
|
168
184
|
});
|
|
169
185
|
runtime = state;
|
|
170
186
|
lobbyService = createLobbyService(state);
|
|
@@ -233,7 +249,8 @@ export function registerLobbyEvents(pi: ExtensionAPI, configDir: string): void {
|
|
|
233
249
|
if (runtime) runtime.ctx = ctx;
|
|
234
250
|
};
|
|
235
251
|
pi.on("session_start", (_event, ctx) => {
|
|
236
|
-
|
|
252
|
+
// Off (the default for a new session) runs no lobby until bot-lobby is turned on (see pi/switch.ts).
|
|
253
|
+
if (!isMinimized()) startLobbyService(pi, ctx, configDir);
|
|
237
254
|
});
|
|
238
255
|
pi.on("session_shutdown", () => shutdown());
|
|
239
256
|
pi.on("ui_prompt_start", () => {
|
package/src/lobby/service.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import { realpathSync } from "node:fs";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { loadTask, ownedTask, peekTasks, saveTask } from "../state/persistence.ts";
|
|
13
|
+
import { taskWork } from "../state/work-time.ts";
|
|
13
14
|
import { refreshReview, deferReview } from "../delivery/review.ts";
|
|
14
15
|
import { deliver, type DeliveryRequest, type DeliveryStore } from "../delivery/operations.ts";
|
|
15
16
|
import type { LockIdentity } from "../delivery/lock.ts";
|
|
@@ -34,7 +35,7 @@ import { pendingRequest, startRequest } from "../pi/route.ts";
|
|
|
34
35
|
import { deliverComments, setAuto } from "../pi/owner.ts";
|
|
35
36
|
import { chatFromEntries, lobbyFeed, type ChatEntry } from "./feed.ts";
|
|
36
37
|
import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
37
|
-
import { launchPi, SessionRegistry, type BackgroundSession, type
|
|
38
|
+
import { launchPi, SessionRegistry, type BackgroundSession, type SessionLauncher } from "./sessions.ts";
|
|
38
39
|
import { SessionChats } from "./session-files.ts";
|
|
39
40
|
import { answerMessage, questionnaires, settledQuestions } from "./ask.ts";
|
|
40
41
|
import { askUser } from "../ask/web.ts";
|
|
@@ -47,6 +48,7 @@ import type { LobbyAgentKind, PanelMember } from "../schemas/configuration.ts";
|
|
|
47
48
|
import type { LiveSession, LobbyService, SwitchTarget } from "./host.ts";
|
|
48
49
|
import { pushNotice, type NoticeLevel } from "../webui/notices.ts";
|
|
49
50
|
import type { Runtime } from "./runtime.ts";
|
|
51
|
+
import { findLinters } from "../execution/lint.ts";
|
|
50
52
|
|
|
51
53
|
function deliveryTask(state: Runtime, taskId: string): Task {
|
|
52
54
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,199}$/.test(taskId) || taskId === "." || taskId === "..") throw new Error("Invalid task ID.");
|
|
@@ -562,6 +564,7 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
562
564
|
let appliedMaster = masterKey();
|
|
563
565
|
return {
|
|
564
566
|
projectRoot: () => state.root,
|
|
567
|
+
linters: () => findLinters(state.root),
|
|
565
568
|
sessionId: () => state.ctx.sessionManager.getSessionId(),
|
|
566
569
|
zen: () => {
|
|
567
570
|
const snapshot = taskSnapshot();
|
|
@@ -570,6 +573,7 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
570
573
|
feed: lobbyFeed,
|
|
571
574
|
masterBusy: () => !state.ctx.isIdle(),
|
|
572
575
|
tasks: () => peekTasks(state.root, state.configDir),
|
|
576
|
+
work: (taskId) => taskWork(state.root, state.configDir, taskId),
|
|
573
577
|
deliveryDeliver: (taskId, request: DeliveryRequest) => deliver(request, { cwd: state.root, exec: execCommand }, deliveryStore(state, taskId)),
|
|
574
578
|
deliveryReview: async (taskId) => {
|
|
575
579
|
const task = deliveryTask(state, taskId);
|
|
@@ -7,51 +7,7 @@
|
|
|
7
7
|
* parent, and the text of user and assistant messages (never tool output).
|
|
8
8
|
*/
|
|
9
9
|
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
interface EntryLike {
|
|
13
|
-
type?: string;
|
|
14
|
-
id?: string;
|
|
15
|
-
parentId?: string | null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** The entries on the branch that ends at the newest entry, oldest first (a session file is a tree). */
|
|
19
|
-
export function currentBranch(entries: readonly unknown[]): unknown[] {
|
|
20
|
-
const byId = new Map<string, EntryLike>();
|
|
21
|
-
let leaf: EntryLike | undefined;
|
|
22
|
-
for (const entry of entries) {
|
|
23
|
-
const record = entry as EntryLike;
|
|
24
|
-
if (!record || typeof record.id !== "string") continue;
|
|
25
|
-
byId.set(record.id, record);
|
|
26
|
-
leaf = record;
|
|
27
|
-
}
|
|
28
|
-
const branch: unknown[] = [];
|
|
29
|
-
const seen = new Set<string>();
|
|
30
|
-
for (let entry = leaf; entry?.id && !seen.has(entry.id); entry = entry.parentId ? byId.get(entry.parentId) : undefined) {
|
|
31
|
-
seen.add(entry.id);
|
|
32
|
-
branch.push(entry);
|
|
33
|
-
}
|
|
34
|
-
return branch.reverse();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Parse a session file's JSON lines, skipping torn ones. */
|
|
38
|
-
export function parseEntries(text: string): unknown[] {
|
|
39
|
-
const entries: unknown[] = [];
|
|
40
|
-
for (const line of text.split("\n")) {
|
|
41
|
-
if (!line.trim()) continue;
|
|
42
|
-
try {
|
|
43
|
-
entries.push(JSON.parse(line));
|
|
44
|
-
} catch {
|
|
45
|
-
// A line being written right now; the next read has it.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return entries;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The conversation a session file holds, as the lobby's chat entries. */
|
|
52
|
-
export function chatFromFile(text: string, max = MAX_CHAT): ChatEntry[] {
|
|
53
|
-
return chatFromEntries(currentBranch(parseEntries(text)), max).map((line, index) => ({ id: index + 1, at: line.at ?? 0, role: line.role, text: line.text }));
|
|
54
|
-
}
|
|
10
|
+
import { chatText, CLEARED_NOTE, isClearedMark, MAX_CHAT, textOf, type ChatEntry } from "./feed.ts";
|
|
55
11
|
|
|
56
12
|
/** What a session log keeps of one entry: its parent, and its conversation lines if it is a message. */
|
|
57
13
|
interface LogNode {
|
package/src/lobby/split.ts
CHANGED
|
@@ -202,7 +202,39 @@ function planWithSteps(plan: string, covers: readonly number[], stepCount: numbe
|
|
|
202
202
|
const lines = plan.split("\n");
|
|
203
203
|
const kept = covers.map((step, index) => found.blocks[step - 1]!.replace(/^(\s*)\d+([.)])/, (_all, indent: string, mark: string) => `${indent}${index + 1}${mark}`));
|
|
204
204
|
const spacer = found.to > found.from && !(lines[found.to - 1] ?? "").trim() ? [""] : [];
|
|
205
|
-
return [...lines.slice(0, found.from), ...kept.flatMap((block) => block.split("\n")), ...spacer, ...lines.slice(found.to)].join("\n");
|
|
205
|
+
return [...lines.slice(0, found.from), ...kept.flatMap((block) => block.split("\n")), ...spacer, ...stepDetails(lines.slice(found.to), covers)].join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** `### Step 3: …` (or `**Step 3 — …**`): the heading of one step's detail. */
|
|
209
|
+
const DETAIL_HEADING = /^(\s*(?:#{1,6}\s+)?(?:\*\*)?\s*step\s+#?)(\d+)(?!\d)/i;
|
|
210
|
+
const ANY_HEADING = /^\s*(?:(#{1,6})\s+\S.*|\*\*[^*]+\*\*.*)$/;
|
|
211
|
+
|
|
212
|
+
/** A heading's depth: `#` is 1, a bold line deeper than any `#`. */
|
|
213
|
+
const depth = (line: string): number => ANY_HEADING.exec(line)?.[1]?.length ?? 7;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The lines after a plan's steps list with only the detail of the steps in `covers`, renumbered as
|
|
217
|
+
* the list was: a step's detail runs from its `Step N` heading to the next heading as deep or deeper.
|
|
218
|
+
*/
|
|
219
|
+
function stepDetails(lines: readonly string[], covers: readonly number[]): string[] {
|
|
220
|
+
const out: string[] = [];
|
|
221
|
+
let skipping: number | undefined;
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
const heading = ANY_HEADING.test(line) ? depth(line) : undefined;
|
|
224
|
+
if (skipping !== undefined && heading !== undefined && heading <= skipping) skipping = undefined;
|
|
225
|
+
const detail = heading !== undefined ? DETAIL_HEADING.exec(line) : null;
|
|
226
|
+
if (detail) {
|
|
227
|
+
const at = covers.indexOf(Number(detail[2]));
|
|
228
|
+
if (at < 0) {
|
|
229
|
+
skipping = heading;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
out.push(line.replace(DETAIL_HEADING, (_all, lead: string) => `${lead}${at + 1}`));
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (skipping === undefined) out.push(line);
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
206
238
|
}
|
|
207
239
|
|
|
208
240
|
/** One part's brief: where it sits, what it delivers, then the plan as written with only its own steps. */
|
package/src/lobby/task-rows.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface TaskRow {
|
|
|
22
22
|
check: CheckState;
|
|
23
23
|
/** Plan steps done, once the task has a plan. */
|
|
24
24
|
progress?: { done: number; total: number };
|
|
25
|
+
/** How long the task's agents have worked on it, idle time left out (ms, as of the reply). */
|
|
26
|
+
work?: { workedMs: number; running: boolean };
|
|
25
27
|
/** Who drives it, for tasks other sessions own. */
|
|
26
28
|
owner?: string;
|
|
27
29
|
/** How long ago it finished (finished tasks), was saved (plans) or was archived, as `3h`. */
|
package/src/master/master.ts
CHANGED
|
@@ -342,6 +342,8 @@ export interface ReviewerRequest {
|
|
|
342
342
|
provenance?: string;
|
|
343
343
|
/** What the previous QA round asked for, when there was one: this round verifies it. */
|
|
344
344
|
previousRound?: string;
|
|
345
|
+
/** The engine's lint result on the touched files and the suppressions the task added. */
|
|
346
|
+
lint?: string;
|
|
345
347
|
/** Under a task time budget: the gate's time. */
|
|
346
348
|
time?: AgentTime;
|
|
347
349
|
instruction?: string;
|
|
@@ -367,6 +369,7 @@ function reviewerContext(request: ReviewerRequest): string {
|
|
|
367
369
|
request.workerSummary ? `Worker summary (each domain's newest entries):\n${truncate(request.workerSummary, 4800)}` : "No worker summary available.",
|
|
368
370
|
owns.length > 0 ? `Scout findings:\n${summarizeOutcomes(owns, 1200)}` : "",
|
|
369
371
|
request.provenance ? `Who changed each file (bot-lobby's record of every agent's edit and write calls; judge each as your role's Change provenance says):\n${request.provenance}` : "",
|
|
372
|
+
request.lint ?? "",
|
|
370
373
|
request.time?.note ?? "",
|
|
371
374
|
`Repository changes:\n${request.diff}`,
|
|
372
375
|
]
|
package/src/master/research.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import type { Domain } from "../schemas/agent.ts";
|
|
4
3
|
import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schemas/configuration.ts";
|
|
@@ -39,21 +38,6 @@ export function saveResearchResult(taskDir: string, outcome: ResearchOutcome): v
|
|
|
39
38
|
writeFileEnsured(researchResultPath(taskDir, outcome.result.domain), JSON.stringify(outcome, null, 2));
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
/** Re-read persisted research reports for the given domains. */
|
|
43
|
-
export function loadResearchResults(taskDir: string, domains: Domain[]): ResearchOutcome[] {
|
|
44
|
-
const outcomes: ResearchOutcome[] = [];
|
|
45
|
-
for (const domain of domains) {
|
|
46
|
-
const path = researchResultPath(taskDir, domain);
|
|
47
|
-
if (!existsSync(path)) continue;
|
|
48
|
-
try {
|
|
49
|
-
outcomes.push(JSON.parse(readFileSync(path, "utf8")) as ResearchOutcome);
|
|
50
|
-
} catch {
|
|
51
|
-
// Corrupted research artifact: skip it rather than failing the workflow.
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return outcomes;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
function researchContext(request: ResearchRequest): AgentRequest["context"] {
|
|
58
42
|
return {
|
|
59
43
|
task: request.instruction,
|
package/src/pi/commands.ts
CHANGED
|
@@ -17,8 +17,9 @@ import { transition } from "../state/task-state.ts";
|
|
|
17
17
|
import { AGENT_DIR_NAMES, KNOWLEDGE_FILES, knowledgeDir, type KnowledgeAgent } from "../knowledge/paths.ts";
|
|
18
18
|
import { readFirstExisting } from "../knowledge/store.ts";
|
|
19
19
|
import { overThreshold } from "../knowledge/compactor.ts";
|
|
20
|
-
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, lastQaAsks, waiveQa, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
21
|
-
import { applyStatus, registerRevealShortcut
|
|
20
|
+
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, lastQaAsks, waiveLint, waiveQa, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
21
|
+
import { applyStatus, registerRevealShortcut } from "./ui.ts";
|
|
22
|
+
import { isOn, registerSwitch, turnOff, turnOn } from "./switch.ts";
|
|
22
23
|
import { classifierSummary } from "./model-settings.ts";
|
|
23
24
|
import { keyStatus } from "../classifier/instance.ts";
|
|
24
25
|
import { kickoff, startPlannedTask } from "./start-task.ts";
|
|
@@ -51,7 +52,7 @@ const HELP = [
|
|
|
51
52
|
"/bot-lobby runs [taskId] Recent subagent runs: time, turns, tokens, cost, model",
|
|
52
53
|
"/bot-lobby settings Open the web lobby's Settings: per-agent model, effort and instructions",
|
|
53
54
|
"/bot-lobby config Show effective configuration",
|
|
54
|
-
"/bot-lobby
|
|
55
|
+
"/bot-lobby on|off Turn bot-lobby on or off for this session (ctrl+shift+m); a new session starts off",
|
|
55
56
|
"/bot-lobby claim <taskId> Take ownership of an orphaned task",
|
|
56
57
|
"/bot-lobby auto [on|off] Auto mode: the oracle drives this session's task without asking (alt+g)",
|
|
57
58
|
"/bot-lobby start-plan PLAN-… [auto] Start a planned task here; its agreed plan needs no approval",
|
|
@@ -61,7 +62,10 @@ const HELP = [
|
|
|
61
62
|
].join("\n");
|
|
62
63
|
|
|
63
64
|
/** Subcommands only win when no free-form text follows (so tasks still start). */
|
|
64
|
-
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "accept", "budget", "knowledge", "config", "settings", "minimize", "restore", "claim", "auto", "start-plan", "switch", "web"]);
|
|
65
|
+
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "accept", "budget", "knowledge", "config", "settings", "on", "off", "minimize", "restore", "claim", "auto", "start-plan", "switch", "web"]);
|
|
66
|
+
|
|
67
|
+
/** Subcommands that act on a task or open the lobby turn bot-lobby on first; the rest work while it is off. */
|
|
68
|
+
const TURNS_ON = new Set(["lobby", "settings", "resume", "approve", "amend", "decline", "accept", "claim", "auto", "start-plan", "switch", "web"]);
|
|
65
69
|
|
|
66
70
|
/** `Task-Change-Table-Font-27-09-2026`, or an older `TASK-add-login` id. */
|
|
67
71
|
function isTaskId(value: string | undefined): boolean {
|
|
@@ -241,10 +245,12 @@ function acceptWork(pi: ExtensionAPI, ctx: ExtensionCommandContext, configDir: s
|
|
|
241
245
|
return ctx.ui.notify(`bot-lobby: ${task.id} is still ${task.state}; there is no work to accept yet.`, "warning");
|
|
242
246
|
}
|
|
243
247
|
if (task.qaVerdict !== "pass" && !task.qaWaiver) waiveQa(task, lastQaAsks(task), "with /bot-lobby accept");
|
|
248
|
+
if (task.lint?.state === "failing") waiveLint(task, "with /bot-lobby accept");
|
|
244
249
|
saveTask(root, configDir, task);
|
|
245
250
|
applyStatus(ctx, root, configDir);
|
|
246
251
|
ctx.ui.notify(`bot-lobby: ${task.id} accepted as it is; the oracle will complete it.`, "info");
|
|
247
|
-
const
|
|
252
|
+
const waived = [task.qaWaiver ? "the QA gate" : "", task.lintWaiver ? "the lint gate" : ""].filter(Boolean);
|
|
253
|
+
const message = `The user accepted ${task.id}'s work as it is${waived.length ? ` (${waived.join(" and ")} ${waived.length === 1 ? "is" : "are"} waived)` : ""}. Call orchestrate action=complete now with a short summary that names anything QA or lint still asked for.`;
|
|
248
254
|
pi.sendUserMessage(message, ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
249
255
|
}
|
|
250
256
|
|
|
@@ -323,6 +329,7 @@ function autoCommand(ctx: ExtensionCommandContext, configDir: string, value: str
|
|
|
323
329
|
|
|
324
330
|
export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
325
331
|
registerRevealShortcut(pi, configDir);
|
|
332
|
+
registerSwitch(pi, configDir);
|
|
326
333
|
registerWebServer(pi);
|
|
327
334
|
pi.registerCommand("bot-lobby", {
|
|
328
335
|
description: "Structured multi-agent engineering orchestrator",
|
|
@@ -334,6 +341,8 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
334
341
|
handler: async (args, ctx) => {
|
|
335
342
|
const { sub, rest, restText, auto, budget, budgetError, track, task, isolation } = parseCommand(args ?? "");
|
|
336
343
|
if (budgetError) return ctx.ui.notify(`bot-lobby: ${budgetError}`, "warning");
|
|
344
|
+
// A request, the bare command or anything that drives a task needs bot-lobby on (`web stop` does not).
|
|
345
|
+
if (!sub || (TURNS_ON.has(sub) && !(sub === "web" && rest[0] === "stop"))) turnOn(pi, ctx, configDir);
|
|
337
346
|
if (!sub) {
|
|
338
347
|
if (!restText) return webCommand(ctx, undefined);
|
|
339
348
|
// A request one agent can do alone may go to the quick-fix agent once the oracle confirms; the rest start as tasks.
|
|
@@ -371,12 +380,15 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
371
380
|
return showKnowledge(ctx, configDir);
|
|
372
381
|
case "settings":
|
|
373
382
|
return webCommand(ctx, undefined);
|
|
383
|
+
case "off":
|
|
374
384
|
case "minimize":
|
|
375
|
-
|
|
376
|
-
|
|
385
|
+
if (!isOn()) return ctx.ui.notify("bot-lobby is already off.", "info");
|
|
386
|
+
await turnOff(ctx, configDir);
|
|
387
|
+
return;
|
|
388
|
+
case "on":
|
|
377
389
|
case "restore":
|
|
378
|
-
|
|
379
|
-
return
|
|
390
|
+
if (isOn()) return ctx.ui.notify("bot-lobby is already on.", "info");
|
|
391
|
+
return turnOn(pi, ctx, configDir);
|
|
380
392
|
case "claim":
|
|
381
393
|
return claimTaskCommand(ctx, configDir, rest[0]);
|
|
382
394
|
case "auto":
|
|
@@ -398,6 +410,9 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
398
410
|
|
|
399
411
|
pi.registerCommand("bot-lobby-settings", {
|
|
400
412
|
description: "Open the web lobby's settings: per-agent model, effort, and instructions",
|
|
401
|
-
handler: async (_args, ctx) =>
|
|
413
|
+
handler: async (_args, ctx) => {
|
|
414
|
+
turnOn(pi, ctx, configDir);
|
|
415
|
+
return webCommand(ctx, undefined);
|
|
416
|
+
},
|
|
402
417
|
});
|
|
403
418
|
}
|